Skip to content

Commit

Permalink
Merge pull request #206 from xen0n/issue200
Browse files Browse the repository at this point in the history
Support self-cleaning of various data
  • Loading branch information
xen0n authored Sep 29, 2024
2 parents 09f1e45 + ecb949d commit e9860e9
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 24 deletions.
63 changes: 56 additions & 7 deletions ruyi/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import atexit
import os
import sys
from typing import Callable, List
from typing import Callable, IO, List, Protocol

from ..config import GlobalConfig

Expand All @@ -24,6 +24,18 @@ def is_called_as_ruyi(argv0: str) -> bool:
CLIEntrypoint = Callable[[GlobalConfig, argparse.Namespace], int]


class _PrintHelp(Protocol):
def print_help(self, file: IO[str] | None = None) -> None: ...


def _wrap_help(x: _PrintHelp) -> CLIEntrypoint:
def _wrapped_(gc: GlobalConfig, args: argparse.Namespace) -> int:
x.print_help()
return 0

return _wrapped_


def init_argparse() -> argparse.ArgumentParser:
from ..device.provision_cli import cli_device_provision
from ..mux.venv.venv_cli import cli_venv
Expand All @@ -34,7 +46,7 @@ def init_argparse() -> argparse.ArgumentParser:
from ..ruyipkg.profile_cli import cli_list_profiles
from ..ruyipkg.update_cli import cli_update
from ..version import RUYI_SEMVER
from .self_cli import cli_self_uninstall
from .self_cli import cli_self_clean, cli_self_uninstall
from .version_cli import cli_version

native_host_str = get_native_host()
Expand All @@ -43,7 +55,7 @@ def init_argparse() -> argparse.ArgumentParser:
prog=RUYI_ENTRYPOINT_NAME,
description=f"RuyiSDK Package Manager {RUYI_SEMVER}",
)
root.set_defaults(func=lambda _: root.print_help())
root.set_defaults(func=_wrap_help(root), tele_key="<bare>")

root.add_argument(
"-V",
Expand All @@ -69,7 +81,7 @@ def init_argparse() -> argparse.ArgumentParser:
"device",
help="Manage devices",
)
device.set_defaults(func=lambda _: device.print_help())
device.set_defaults(func=_wrap_help(device), tele_key="device")
devicesp = device.add_subparsers(
title="subcommands",
)
Expand Down Expand Up @@ -150,7 +162,7 @@ def init_argparse() -> argparse.ArgumentParser:
"news",
help="List and read news items from configured repository",
)
news.set_defaults(func=lambda _: news.print_help())
news.set_defaults(func=_wrap_help(news), tele_key="news")
newssp = news.add_subparsers(title="subcommands")

news_list = newssp.add_parser(
Expand Down Expand Up @@ -239,7 +251,7 @@ def init_argparse() -> argparse.ArgumentParser:
# help=argparse.SUPPRESS,
help="(NOT FOR REGULAR USERS) Subcommands for managing Ruyi repos",
)
admin.set_defaults(func=lambda _: admin.print_help())
admin.set_defaults(func=_wrap_help(admin), tele_key="admin")
adminsp = admin.add_subparsers(
title="subcommands",
)
Expand Down Expand Up @@ -290,11 +302,48 @@ def init_argparse() -> argparse.ArgumentParser:
"self",
help="Manage this Ruyi installation",
)
self.set_defaults(func=lambda _: self.print_help())
self.set_defaults(func=_wrap_help(self), tele_key="self")
selfsp = self.add_subparsers(
title="subcommands",
)

self_clean = selfsp.add_parser(
"clean",
help="Remove various Ruyi-managed data to reclaim storage",
)
self_clean.add_argument(
"--quiet",
"-q",
action="store_true",
help="Do not print out the actions being performed",
)
self_clean.add_argument(
"--distfiles",
action="store_true",
help="Remove all downloaded distfiles if any",
)
self_clean.add_argument(
"--installed-pkgs",
action="store_true",
help="Remove all installed packages if any",
)
self_clean.add_argument(
"--progcache",
action="store_true",
help="Clear the Ruyi program cache",
)
self_clean.add_argument(
"--repo",
action="store_true",
help="Remove the Ruyi repo if located in Ruyi-managed cache directory",
)
self_clean.add_argument(
"--telemetry",
action="store_true",
help="Remove all telemetry data recorded if any",
)
self_clean.set_defaults(func=cli_self_clean, tele_key="self clean")

self_uninstall = selfsp.add_parser(
"uninstall",
help="Uninstall Ruyi",
Expand Down
120 changes: 107 additions & 13 deletions ruyi/cli/self_cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse
import os
import pathlib
import shutil

import ruyi
Expand All @@ -20,6 +21,34 @@
"""


def cli_self_clean(cfg: config.GlobalConfig, args: argparse.Namespace) -> int:
quiet: bool = args.quiet
distfiles: bool = args.distfiles
installed_pkgs: bool = args.installed_pkgs
progcache: bool = args.progcache
repo: bool = args.repo
telemetry: bool = args.telemetry

if not any([distfiles, installed_pkgs, progcache, repo, telemetry]):
log.F("no data specified for cleaning")
log.I(
"please check [yellow]ruyi self clean --help[/] for a list of cleanable data"
)
return 1

_do_reset(
cfg,
quiet=quiet,
distfiles=distfiles,
installed_pkgs=installed_pkgs,
progcache=progcache,
repo=repo,
telemetry=telemetry,
)

return 0


def cli_self_uninstall(cfg: config.GlobalConfig, args: argparse.Namespace) -> int:
purge: bool = args.purge
consent: bool = args.consent
Expand All @@ -46,24 +75,89 @@ def cli_self_uninstall(cfg: config.GlobalConfig, args: argparse.Namespace) -> in
else:
log.I("uninstallation consent given over CLI, proceeding")

if purge:
log.I("removing installed packages")
_do_reset(
cfg,
quiet=False,
installed_pkgs=purge,
all_state=purge,
all_cache=purge,
self_binary=True,
)

log.I("[yellow]ruyi[/yellow] is uninstalled")

return 0


def _do_reset(
cfg: config.GlobalConfig,
quiet: bool = False,
*,
installed_pkgs: bool = False,
all_state: bool = False,
telemetry: bool = False, # ignored if all_state=True
all_cache: bool = False,
distfiles: bool = False, # ignored if all_cache=True
progcache: bool = False, # ignored if all_cache=True
repo: bool = False, # ignored if all_cache=True
self_binary: bool = False,
) -> None:
def status(s: str) -> None:
if quiet:
return
log.I(s)

if installed_pkgs:
status("removing installed packages")
shutil.rmtree(cfg.data_root, True)

log.I("removing state data")
if all_state:
status("removing state data")
shutil.rmtree(cfg.state_root, True)
else:
if telemetry:
status("removing all telemetry data")
shutil.rmtree(cfg.telemetry_root, True)

log.I("removing cached data")
if all_cache:
status("removing cached data")
shutil.rmtree(cfg.cache_root, True)
else:
if distfiles:
status("removing downloaded distfiles")
# TODO: deduplicate the path derivation
shutil.rmtree(os.path.join(cfg.cache_root, "distfiles"), True)

log.I("removing the ruyi binary")
try:
os.unlink(ruyi.self_exe())
except FileNotFoundError:
# we might have already removed ourselves during the purge; nothing to
# do now.
pass
if progcache:
status("clearing the Ruyi program cache")
# TODO: deduplicate the path derivation
shutil.rmtree(os.path.join(cfg.cache_root, "progcache"), True)

log.I("[yellow]ruyi[/yellow] is uninstalled")
if repo:
# for safety, don't remove the repo if it's outside of Ruyi's XDG
# cache root
repo_dir = pathlib.Path(cfg.get_repo_dir()).resolve()
cache_root = pathlib.Path(cfg.cache_root).resolve()

return 0
repo_is_below_cache_root = False
for p in repo_dir.parents:
if p == cache_root:
repo_is_below_cache_root = True
break

if not repo_is_below_cache_root:
log.W(
"not removing the Ruyi repo: it is outside of the Ruyi cache directory"
)
else:
status("removing the Ruyi repo")
shutil.rmtree(repo_dir, True)

if self_binary:
status("removing the ruyi binary")
try:
os.unlink(ruyi.self_exe())
except FileNotFoundError:
# we might have already removed ourselves during the purge; nothing to
# do now.
pass
7 changes: 5 additions & 2 deletions ruyi/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ def news_read_status(self) -> NewsReadStatusStore:
self._news_read_status_store = NewsReadStatusStore(filename)
return self._news_read_status_store

@property
def telemetry_root(self) -> os.PathLike[Any]:
return pathlib.Path(self.ensure_state_dir()) / "telemetry"

@property
def telemetry(self) -> TelemetryStore | None:
if self.telemetry_mode == "off":
Expand All @@ -147,8 +151,7 @@ def telemetry(self) -> TelemetryStore | None:
return self._telemetry_store

local_mode = self.telemetry_mode == "local"
dirname = os.path.join(self.ensure_state_dir(), "telemetry")
self._telemetry_store = TelemetryStore(dirname, local_mode)
self._telemetry_store = TelemetryStore(self.telemetry_root, local_mode)
return self._telemetry_store

@property
Expand Down
5 changes: 3 additions & 2 deletions ruyi/telemetry/store.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import json
import os
import pathlib
import time
from typing import TypedDict
from typing import Any, TypedDict
import uuid

from .. import log
Expand All @@ -15,7 +16,7 @@ class TelemetryEvent(TypedDict):


class TelemetryStore:
def __init__(self, store_root: str, local: bool) -> None:
def __init__(self, store_root: os.PathLike[Any], local: bool) -> None:
self.store_root = pathlib.Path(store_root)
self.local = local
self._events: list[TelemetryEvent] = []
Expand Down

0 comments on commit e9860e9

Please sign in to comment.