%PDF- %PDF-
| Direktori : /lib/python3/dist-packages/nala/ |
| Current File : //lib/python3/dist-packages/nala/utils.py |
# __
# ____ _____ | | _____
# / \\__ \ | | \__ \
# | | \/ __ \| |__/ __ \_
# |___| (____ /____(____ /
# \/ \/ \/
#
# Copyright (C) 2010 - 2021 Tatsuhiro Tsujikawa
# Copyright (C) 2021, 2022 Blake Lee
#
# This file is part of nala
# nala is based upon apt-metalink https://github.com/tatsuhiro-t/apt-metalink
#
# nala is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# nala is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with nala. If not, see <https://www.gnu.org/licenses/>.
"""Where Utilities who don't have a special home come together."""
from __future__ import annotations
import contextlib
import os
import re
import signal
import sys
import termios
import tty
from dataclasses import dataclass, field
from datetime import datetime
from fcntl import LOCK_EX, LOCK_NB, lockf
from pathlib import Path
from types import FrameType
from typing import TYPE_CHECKING, Any, Generator, Iterable, Pattern
from apt.package import Package, Version
from nala import TERMUX, _, color, console
from nala.constants import (
ERROR_PREFIX,
HANDLER,
NALA_DEBUGLOG,
NALA_DIR,
NALA_LOCK_FILE,
NALA_LOGDIR,
)
from nala.options import arguments
from nala.rich import from_ansi
if TYPE_CHECKING:
from nala.debfile import NalaDebPackage
from nala.fetch import FetchLive
LOCK_FILE = None
# NOTE: Answers for the Question prompt "[Y/n]"
YES_NO = _("Y/y N/n").split()
YES = YES_NO[0].split("/")
NO = YES_NO[1].split("/")
class Terminal:
"""Represent the user terminal."""
# Term Constants
STDIN = 0
STDOUT = 1
STDERR = 2
# Control Codes
CURSER_UP = b"\x1b[1A"
CLEAR_LINE = b"\x1b[2k"
CLEAR = b"\x1b[2J"
CLEAR_FROM_CURRENT_TO_END = b"\x1b[K"
BACKSPACE = b"\x08"
HOME = b"\x1b[H"
ENABLE_BRACKETED_PASTE = b"\x1b[?2004h"
DISABLE_BRACKETED_PASTE = b"\x1b[?2004l"
ENABLE_ALT_SCREEN = b"\x1b[?1049h"
DISABLE_ALT_SCREEN = b"\x1b[?1049l"
SHOW_CURSOR = b"\x1b[?25h"
HIDE_CURSOR = b"\x1b[?25l"
SET_CURSER = b"\x1b[?1l"
SAVE_TERM = b"\x1b[22;0;0t"
RESTORE_TERM = b"\x1b[23;0;0t"
APPLICATION_KEYPAD = b"\x1b="
NORMAL_KEYPAD = b"\x1b>"
CR = b"\r"
LF = b"\n"
CRLF = b"\r\n"
def __init__(self) -> None:
"""Represent the user terminal."""
self.console = console
self.mode: list[int | list[bytes | int]] = []
self.term_type: str = os.environ.get("TERM", "").lower()
self.locale: str = ""
self.set_environment()
def __repr__(self) -> str:
"""Represent state of the user terminal as a string."""
kwarg = "\n ".join(
(f"{key} = {value},") for key, value in self.__dict__.items()
)
return f"Terminal = [\n {kwarg}\n]"
def set_environment(self) -> None:
"""Check and set various environment variables."""
# Termios can't run if we aren't in a terminal
# Just catch the exception and continue.
if self.can_format():
self.mode = termios.tcgetattr(self.STDIN)
if self.lines < 13 or self.columns < 31:
print(
_("Terminal can't support dialog, falling back to readline"),
file=sys.stderr,
)
os.environ["DEBIAN_FRONTEND"] = "readline"
# Readline is too hard to support with our fancy formatting
if os.environ.get("DEBIAN_FRONTEND") == "readline":
arguments.raw_dpkg = True
os.environ["DPKG_COLORS"] = "never"
self.locale = os.environ.get("LANG", "")
# We have to set lang as C so we get predictable output from dpkg.
os.environ["LANG"] = "C" if self.console.options.ascii_only else "C.UTF-8"
@property
def columns(self) -> int:
"""Return termindal width."""
return self.console.width
@property
def lines(self) -> int:
"""Return terminal height."""
return self.console.height
def can_format(self) -> bool:
"""Return if we're allowed to do anything fancy."""
return (
os.isatty(self.STDOUT)
and os.isatty(self.STDIN)
and self.term_type not in ("dumb", "unknown")
)
def restore_mode(self) -> None:
"""Restore the mode the Terminal was initialized with."""
if self.can_format():
termios.tcsetattr(self.STDIN, termios.TCSAFLUSH, self.mode)
def restore_locale(self) -> None:
"""Restore the locale to it's original value."""
os.environ["LANG"] = self.locale
def set_raw(self) -> None:
"""Set terminal raw."""
if self.can_format():
tty.setraw(self.STDIN)
def write(self, data: bytes) -> None:
"""Write bytes directly to stdout."""
os.write(self.STDOUT, data)
def is_xterm(self) -> bool:
"""Return True if we're in an xterm, False otherwise."""
return "xterm" in self.term_type
@staticmethod
def is_su() -> bool:
"""Return True if we're super user and False if we're not."""
return TERMUX or os.geteuid() == 0
class DelayedKeyboardInterrupt:
"""Context manager to delay KeyboardInterrupt.
Keyboard Interrupts will be delayed until out of scope.
"""
def __init__(self) -> None:
"""Context manager to delay KeyboardInterrupt."""
self.signal_received: tuple[int, FrameType | None] | bool
self.old_handler: HANDLER
def __enter__(self) -> None:
"""Enter context."""
self.signal_received = False
self.old_handler = signal.signal(signal.SIGINT, self.handler)
def handler(self, sig: int, frame: FrameType | None) -> None:
"""Handle sigint signals."""
self.signal_received = (sig, frame)
dprint("SIGINT received. Delaying KeyboardInterrupt.")
def __exit__(self, _type: None, _value: None, _traceback: None) -> None:
"""Exit context."""
signal.signal(signal.SIGINT, self.old_handler)
if isinstance(self.signal_received, tuple) and callable(self.old_handler):
self.old_handler(*self.signal_received)
@dataclass
class PackageHandler: # pylint: disable=too-many-instance-attributes
"""Class for storing package lists."""
autoremoved: set[str] = field(default_factory=set)
user_explicit: list[Package] = field(default_factory=list)
local_debs: list[NalaDebPackage] = field(default_factory=list)
not_needed: list[NalaPackage] = field(default_factory=list)
delete_pkgs: list[NalaPackage] = field(default_factory=list)
install_pkgs: list[NalaPackage] = field(default_factory=list)
reinstall_pkgs: list[NalaPackage] = field(default_factory=list)
upgrade_pkgs: list[NalaPackage] = field(default_factory=list)
autoremove_pkgs: list[NalaPackage] = field(default_factory=list)
autoremove_config: list[NalaPackage] = field(default_factory=list)
delete_config: list[NalaPackage] = field(default_factory=list)
recommend_pkgs: list[NalaPackage | list[NalaPackage]] = field(default_factory=list)
suggest_pkgs: list[NalaPackage | list[NalaPackage]] = field(default_factory=list)
configure_pkgs: list[NalaPackage] = field(default_factory=list)
downgrade_pkgs: list[NalaPackage] = field(default_factory=list)
def no_summary(
self, pkg_set: list[NalaPackage] | list[NalaPackage | list[NalaPackage]]
) -> bool:
"""Return True if we shouldn't print a summary for the package set."""
return pkg_set in (self.suggest_pkgs, self.recommend_pkgs, self.not_needed)
def all_pkgs(self) -> Generator[NalaPackage | NalaDebPackage, None, None]:
"""Return a list of all the packages to be altered."""
yield from (
self.delete_pkgs
+ self.autoremove_pkgs
+ self.install_pkgs
+ self.reinstall_pkgs
+ self.downgrade_pkgs
+ self.upgrade_pkgs
+ self.configure_pkgs
+ self.autoremove_config
+ self.delete_config
)
yield from self.local_debs
def dpkg_progress_total(self) -> int:
"""Calculate our total operations for the dpkg progress bar."""
return (
len(
self.delete_pkgs
+ self.autoremove_pkgs
+ self.install_pkgs
+ self.reinstall_pkgs
+ self.downgrade_pkgs
+ self.upgrade_pkgs
+ self.configure_pkgs
)
* 2
# For local deb installs we add 1 more because of having to start
# and stop InstallProgress an extra time for each package
+ len(self.local_debs)
# Purging configuration files only have 1 message
+ len(self.autoremove_config + self.delete_config)
# This last +1 for the ending of dpkg itself
+ 1
)
@dataclass
class NalaPackage:
"""Class that represents a Nala package."""
name: str
version: str
size: int
old_version: str | None = None
@property
def unit_size(self) -> str:
"""Return the size as a readable unit. Example 12 MB."""
return unit_str(self.size)
term = Terminal()
def command_help(wrong: str, correct: str, update: bool | None) -> None:
"""Check if the user typed a common mistake of a command."""
command_ask = False
if arguments.command == "history" and arguments.history == wrong:
arguments.command = correct
wrong = f"history {wrong}"
command_ask = True
elif arguments.command == wrong:
arguments.command = correct
command_ask = True
if command_ask:
arguments.set_update(update)
if not ask(
_("{command} is not a command\nDid you mean {correction}?").format(
command=color(wrong, "YELLOW"),
correction=color(f"nala {correct}", "YELLOW"),
)
):
sys.exit(1)
def ask(question: str, fetch: FetchLive | None = None) -> bool:
"""Ask the user {question}.
resp = input(f'{question}? [Y/n]
Y returns True
N returns False
"""
with contextlib.suppress(AttributeError):
if arguments.assume_yes:
return True
if arguments.assume_no:
return False
while True:
resp = input(f"{question} [{YES[0]}/{NO[1]}] ")
if resp in (YES[0], YES[1], ""):
return True
if resp in NO:
return False
if fetch:
fetch.errors += 1
print(_("Not a valid choice kiddo"))
def compile_regex(regex: str) -> Pattern[str]:
"""Compile regex and exit on failure."""
try:
return re.compile(regex, re.IGNORECASE)
except re.error as error:
sys.exit(
_(
"{error} failed regex compilation '{error_msg} at position {position}'"
).format(error=ERROR_PREFIX, error_msg=error.msg, position=error.pos)
)
def sudo_check(args: Iterable[str] | None = None) -> None:
"""Check for root and exit if not root."""
if not term.is_su():
if arguments.command == "install" and arguments.fix_broken and not args:
sys.exit(
_("{error} Nala needs root to fix broken packages").format(
error=ERROR_PREFIX
)
)
sys.exit(
_("{error} Nala needs root to {command}").format(
error=ERROR_PREFIX, command=arguments.command
)
)
# Make sure our directories exist
NALA_DIR.mkdir(exist_ok=True)
NALA_LOGDIR.mkdir(exist_ok=True)
NALA_LOCK_FILE.touch(exist_ok=True)
global LOCK_FILE # pylint: disable=global-statement
LOCK_FILE = NALA_LOCK_FILE.open("r+", encoding="ascii")
current_pid = os.getpid()
last_pid = LOCK_FILE.read()
try:
dprint("Setting Lock")
lockf(LOCK_FILE, LOCK_EX | LOCK_NB)
LOCK_FILE.seek(0)
LOCK_FILE.write(f"{current_pid}")
LOCK_FILE.truncate()
dprint("Lock Set")
except OSError:
sys.exit(
_("{error} Nala is already running another instance {last_pid}").format(
error=ERROR_PREFIX, last_pid=color(last_pid, "YELLOW")
)
)
def get_date() -> str:
"""Return the formatted Date and Time."""
return f"{datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')}"
def unit_str(val: int) -> str:
"""Check integer and figure out what format it should be.
`unit_str` will return a string with a leading space like " 12 MB".
You need to strip `unit_str` if you do not want the space.
"""
if arguments.config.get_bool("filesize_binary", False):
base = 1024
size = ("Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
else:
base = 1000
size = ("Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
if val > base**3:
return f"{val/base**3 :.1f} {size[3]}"
if val > base**2:
return f"{val/base**2 :.1f} {size[2]}"
if val > base:
return f"{round(val/1000) :.0f} {size[1]}"
return f"{val :.0f} {size[0]}"
def iter_remove(path: Path) -> None:
"""Iterate the directory supplied and remove all files."""
vprint(_("Removing files in {dir}").format(dir=path))
for file in path.iterdir():
if file.is_file():
vprint(_("Removed: {filename}").format(filename=file))
file.unlink(missing_ok=True)
def get_version(
pkg: Package, cand_first: bool = False, inst_first: bool = False
) -> Version | tuple[Version, ...]:
"""Get the version, any version of a package."""
if not cand_first and arguments.all_versions:
return tuple(pkg.versions)
if cand_first:
return pkg.candidate or pkg.installed or pkg.versions[0]
if inst_first:
return pkg.installed or pkg.candidate or pkg.versions[0]
for version in pkg.versions:
return version
# It would be really weird if we ever actually hit this error
sys.exit(
_("{error} can't find version for {package}").format(
error=ERROR_PREFIX, package=pkg.name
)
)
def get_pkg_version(
pkg: Package, cand_first: bool = False, inst_first: bool = False
) -> Version:
"""Get the version."""
if cand_first:
return pkg.candidate or pkg.installed or pkg.versions[0]
if inst_first:
return pkg.installed or pkg.candidate or pkg.versions[0]
for version in pkg.versions:
return version
# It would be really weird if we ever actually hit this error
sys.exit(
_("{error} can't find version for {package}").format(
error=ERROR_PREFIX, package=pkg.name
)
)
def get_pkg_name(candidate: Version) -> str:
"""Return the package name.
Checks if we need and epoch in the path.
"""
if ":" in candidate.version:
index = candidate.version.index(":")
epoch = f"_{candidate.version[:index]}%3a"
return Path(candidate.filename).name.replace("_", epoch, 1)
return Path(candidate.filename).name
def pkg_candidate(pkg: Package) -> Version:
"""Type enforce package candidate."""
assert pkg.candidate
return pkg.candidate
def pkg_installed(pkg: Package) -> Version:
"""Type enforce package installed."""
assert pkg.installed
return pkg.installed
def dedupe_list(original: Iterable[str]) -> list[str]:
"""Deduplicate a list.
Useful for when we want to maintain the list order and can't use set()
"""
dedupe = []
for item in original:
if item not in dedupe:
dedupe.append(item)
return dedupe
def vprint(msg: object) -> None:
"""Print message if verbose."""
if arguments.verbose or arguments.debug:
print(msg)
if arguments.debug:
dprint(from_ansi(f"{msg}").plain, from_verbose=True)
sys.__stdout__.flush()
def dprint(msg: object, from_verbose: bool = False) -> None:
"""Print message if debugging, write to log if root."""
if not arguments.debug:
return
if not from_verbose:
print(f"DEBUG: {msg}")
if term.is_su():
with open(NALA_DEBUGLOG, "a", encoding="utf-8") as logfile:
logfile.write(f"[{get_date()}] DEBUG: {msg}\n")
def eprint(*args: Any, **kwargs: Any) -> None:
"""Print message to stderr."""
print(*args, file=sys.stderr, **kwargs)