mirror of
https://github.com/HorlogeSkynet/archey4
synced 2024-11-24 04:00:10 +01:00
923990c3c6
Replaces callback with property
184 lines
7.6 KiB
Python
184 lines
7.6 KiB
Python
"""Uptime detection class"""
|
|
|
|
import re
|
|
import time
|
|
from contextlib import suppress
|
|
from datetime import timedelta
|
|
from functools import cached_property
|
|
from subprocess import PIPE, run
|
|
|
|
from archey.entry import Entry
|
|
from archey.exceptions import ArcheyException
|
|
|
|
|
|
class Uptime(Entry):
|
|
"""Returns a pretty-formatted string representing the host uptime"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
uptime_seconds = int(self._get_uptime_delta().total_seconds())
|
|
|
|
days, uptime_seconds = divmod(uptime_seconds, 86400)
|
|
hours, uptime_seconds = divmod(uptime_seconds, 3600)
|
|
minutes, seconds = divmod(uptime_seconds, 60)
|
|
|
|
self.value = {"days": days, "hours": hours, "minutes": minutes, "seconds": seconds}
|
|
|
|
def _get_uptime_delta(self) -> timedelta:
|
|
"""
|
|
Returns a `datetime.timedelta` instance containing the machine uptime.
|
|
Tries a variety of methods, increasing compatibility for a wide range of systems.
|
|
"""
|
|
# Try the /proc/uptime file
|
|
try:
|
|
return self._proc_file_uptime()
|
|
except OSError:
|
|
# Can't read /proc/uptime.
|
|
# Not GNU/Linux ? Limited permissions ?
|
|
pass
|
|
|
|
# Try the python `time` module clocks
|
|
try:
|
|
return self._clock_uptime()
|
|
except RuntimeError:
|
|
# Probably Python <3.7, or not *NIX
|
|
pass
|
|
|
|
# FUTURE: Windows support could be added here with the `wmi` or `pywin32` module.
|
|
|
|
# Fall back to the `uptime` command.
|
|
return self._parse_uptime_cmd()
|
|
|
|
@staticmethod
|
|
def _proc_file_uptime() -> timedelta:
|
|
"""Tries to get uptime using the `/proc/uptime` file"""
|
|
with open("/proc/uptime", encoding="ASCII") as f_uptime:
|
|
return timedelta(seconds=float(f_uptime.read().split()[0]))
|
|
|
|
@staticmethod
|
|
def _clock_uptime() -> timedelta:
|
|
"""Tries to get uptime using the clocks from the Python `time` module"""
|
|
# Try: Linux and BSD uptime clocks.
|
|
for clock in ("CLOCK_BOOTTIME", "CLOCK_UPTIME"):
|
|
with suppress(AttributeError):
|
|
return timedelta(seconds=time.clock_gettime(getattr(time, clock)))
|
|
|
|
# Probably Python <3.7, or just not one of the above OSes
|
|
raise RuntimeError
|
|
|
|
def _parse_uptime_cmd(self) -> timedelta:
|
|
"""Tries to get uptime by parsing the `uptime` command"""
|
|
try:
|
|
uptime_output = run("uptime", env={"LANG": "C"}, stdout=PIPE, stderr=PIPE, check=True)
|
|
except FileNotFoundError as error:
|
|
raise ArcheyException("Couldn't find `uptime` command on this system.") from error
|
|
|
|
# Log any `uptime` error messages at warning level.
|
|
if uptime_output.stderr:
|
|
for line in uptime_output.stderr.splitlines():
|
|
self._logger.warning("[uptime]: %s", line.decode())
|
|
|
|
# Unfortunately the output is not designed to be machine-readable...
|
|
uptime_match = re.search(
|
|
rb"""
|
|
up\s+? # match the `up` preceding the uptime (anchor the start of the regex)
|
|
(?: # non-capture group for days section.
|
|
(?P<days> # 'days' named capture group, captures the days digits.
|
|
\d+?
|
|
)
|
|
\s+? # match whitespace,
|
|
days? # 'day' or 'days',
|
|
[,\s]+? # then a comma (if present) followed by more whitespace.
|
|
)? # match the days non-capture group 0 or 1 times.
|
|
(?: # non-capture group for hours & minutes section.
|
|
(?: # non-capture group for just hours section.
|
|
(?P<hours> # 'hours' named capture group, captures the hours digits.
|
|
\d+?
|
|
)
|
|
(?: # non-capture group for hours:minutes colon or 'hrs' text...
|
|
: # i.e. hours followed by either a single colon
|
|
| # OR
|
|
\s+? # 1 or more whitespace chars non-greedily,
|
|
hrs? # followed by 'hr' or 'hrs'.
|
|
)
|
|
)? # match the hours non-capture group 0 or 1 times.
|
|
(?: # non-capture group for minutes section.
|
|
(?P<minutes> # 'minutes' named capture group, captures the minutes digits.
|
|
\d+?
|
|
)
|
|
(?: # non-capture group for 'min' or 'mins' text.
|
|
\s+? # match whitespace,
|
|
mins? # followed by 'min' or 'mins'.
|
|
)? # match the text 0 or 1 times (0 times is for the hh:mm format case).
|
|
(?! # negative lookahead group
|
|
\d+ # this prevents matching seconds digits as minutes...
|
|
\s+? # since we only non-greedily match minutes digits earlier.
|
|
secs? # here's the part that matches the 'sec' or 'secs' text.
|
|
) # the minutes group is discarded if this lookahead matches!
|
|
)? # match the minutes non-capture group 0 or 1 times.
|
|
)? # match the entire hours & minutes non-capture group 0 or 1 times.
|
|
(?: # non-capture group for seconds.
|
|
(?P<seconds> # 'seconds' named capture group, captures the seconds digits.
|
|
\d+?
|
|
)
|
|
\s+? # match whitespace,
|
|
secs? # then 'sec' or 'secs'.
|
|
)? # match the seconds non-capture group 0 or 1 times.
|
|
[,\s]*? # after the groups, match a comma and/or whitespace 0 or more times,
|
|
\d+? # one or more digits for the user count,
|
|
\s+? # whitespace between the user count and the text 'user',
|
|
user # and the text 'user' (to anchor the end of the expression).
|
|
""",
|
|
uptime_output.stdout,
|
|
re.VERBOSE,
|
|
)
|
|
if not uptime_match:
|
|
raise ArcheyException("Couldn't parse `uptime` output, please open an issue.")
|
|
|
|
# Only `days`, `hours`, `minutes` or `seconds` could have been captured.
|
|
# `timedelta` directly accepts them as arguments.
|
|
uptime_args = uptime_match.groupdict()
|
|
return timedelta(
|
|
days=int(uptime_args.get("days") or 0),
|
|
hours=int(uptime_args.get("hours") or 0),
|
|
minutes=int(uptime_args.get("minutes") or 0),
|
|
seconds=int(uptime_args.get("seconds") or 0),
|
|
)
|
|
|
|
@cached_property
|
|
def pretty_value(self) -> [(str, str)]:
|
|
"""Pretty-formats the uptime to a string."""
|
|
days = self.value["days"]
|
|
hours = self.value["hours"]
|
|
minutes = self.value["minutes"]
|
|
|
|
uptime = ""
|
|
if days:
|
|
uptime += str(days) + " day"
|
|
if days > 1:
|
|
uptime += "s"
|
|
|
|
if hours or minutes:
|
|
if bool(hours) != bool(minutes):
|
|
uptime += " and "
|
|
else:
|
|
uptime += ", "
|
|
|
|
if hours:
|
|
uptime += str(hours) + " hour"
|
|
if hours > 1:
|
|
uptime += "s"
|
|
|
|
if minutes:
|
|
uptime += " and "
|
|
|
|
if minutes:
|
|
uptime += str(minutes) + " minute"
|
|
if minutes > 1:
|
|
uptime += "s"
|
|
elif not days and not hours:
|
|
uptime = "< 1 minute"
|
|
|
|
return [(self.name, uptime)]
|