ybox 0.9.8.1__py3-none-any.whl → 0.9.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. ybox/__init__.py +1 -1
  2. ybox/cmd.py +17 -1
  3. ybox/conf/completions/ybox.fish +2 -0
  4. ybox/conf/distros/arch/init-user.sh +2 -2
  5. ybox/conf/distros/arch/init.sh +1 -0
  6. ybox/conf/distros/arch/pkgdeps.py +2 -0
  7. ybox/conf/distros/deb-generic/pkgdeps.py +2 -1
  8. ybox/conf/profiles/apps.ini +10 -5
  9. ybox/conf/profiles/basic.ini +48 -23
  10. ybox/conf/profiles/dev.ini +4 -6
  11. ybox/conf/resources/entrypoint-cp.sh +1 -1
  12. ybox/conf/resources/entrypoint-root.sh +4 -3
  13. ybox/conf/resources/entrypoint-user.sh +5 -3
  14. ybox/conf/resources/entrypoint.sh +24 -22
  15. ybox/conf/resources/prime-run +0 -2
  16. ybox/conf/resources/run-in-dir +30 -16
  17. ybox/conf/resources/run-user-bash-cmd +17 -1
  18. ybox/conf/resources/ybox-systemd.template +24 -0
  19. ybox/config.py +9 -1
  20. ybox/env.py +18 -7
  21. ybox/migrate/{0.9.0-0.9.7:0.9.8.py → 0.9.0-0.9.10:0.9.11.py} +6 -5
  22. ybox/pkg/clean.py +1 -7
  23. ybox/pkg/info.py +1 -7
  24. ybox/pkg/inst.py +40 -22
  25. ybox/pkg/list.py +1 -6
  26. ybox/pkg/mark.py +1 -1
  27. ybox/pkg/repair.py +4 -0
  28. ybox/pkg/search.py +1 -7
  29. ybox/run/cmd.py +2 -1
  30. ybox/run/control.py +107 -25
  31. ybox/run/create.py +254 -63
  32. ybox/run/destroy.py +89 -4
  33. ybox/run/graphics.py +37 -17
  34. ybox/run/logs.py +2 -1
  35. ybox/run/ls.py +2 -1
  36. ybox/run/pkg.py +49 -7
  37. ybox/state.py +22 -3
  38. ybox/util.py +5 -5
  39. {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info}/METADATA +68 -34
  40. ybox-0.9.11.dist-info/RECORD +77 -0
  41. {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info}/WHEEL +1 -1
  42. ybox-0.9.8.1.dist-info/RECORD +0 -76
  43. {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info}/entry_points.txt +0 -0
  44. {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info/licenses}/LICENSE +0 -0
  45. {ybox-0.9.8.1.dist-info → ybox-0.9.11.dist-info}/top_level.txt +0 -0
ybox/env.py CHANGED
@@ -60,34 +60,41 @@ class Environ:
60
60
  :param home_dir: if a non-default user home directory has to be set
61
61
  """
62
62
  self._home_dir = home_dir or os.path.expanduser("~")
63
+ self._home_dir = self._home_dir.rstrip("/")
63
64
  self._docker_cmd = docker_cmd or get_docker_command()
64
65
  cmd_version = subprocess.check_output([self._docker_cmd, "--version"])
65
66
  self._uses_podman = "podman" in cmd_version.decode("utf-8").lower()
66
67
  # local user home might be in a different location than /home but target user in the
67
68
  # container will always be in /home with podman else /root for the root user with docker
68
69
  # as ensured by entrypoint-base.sh script
69
- target_uid = 0
70
+ current_user = getpass.getuser()
71
+ current_uid = pwd.getpwnam(current_user).pw_uid
70
72
  if self._uses_podman:
71
- self._target_user = getpass.getuser()
72
- target_uid = pwd.getpwnam(self._target_user).pw_uid
73
+ self._target_user = current_user
74
+ target_uid = current_uid
73
75
  self._target_home = f"/home/{self._target_user}"
74
76
  else:
75
77
  self._target_user = "root"
78
+ target_uid = 0
76
79
  self._target_home = "/root"
77
80
  # confirm that docker is being used in rootless mode (not required for podman because
78
81
  # it runs as rootless when run by a non-root user in any case without explicit sudo
79
82
  # which the ybox tools don't use)
80
83
  if (docker_ctx := subprocess.check_output(
81
84
  [self._docker_cmd, "context", "show"]).decode("utf-8")).strip() != "rootless":
82
- raise NotSupportedError("docker should use the rootless mode (see "
83
- "https://docs.docker.com/engine/security/rootless/) "
84
- f"but the current context is '{docker_ctx}'")
85
+ # check for DOCKER_HOST environment variable
86
+ expected_docker_host = f"unix:///run/user/{current_uid}/docker.sock"
87
+ if not docker_ctx or os.environ.get("DOCKER_HOST", "") != expected_docker_host:
88
+ raise NotSupportedError("docker should use the rootless mode (see "
89
+ "https://docs.docker.com/engine/security/rootless/) "
90
+ f"but the current context is '{docker_ctx}' and "
91
+ f"$DOCKER_HOST is not set to '{expected_docker_host}'")
85
92
  os.environ["TARGET_HOME"] = self._target_home
86
93
  self._user_base = user_base = site.getuserbase()
87
94
  target_user_base = f"{self._target_home}/.local"
88
95
  self._data_dir = f"{user_base}/share/ybox"
89
96
  self._target_data_dir = f"{target_user_base}/share/ybox"
90
- self._xdg_rt_dir = os.environ.get("XDG_RUNTIME_DIR", "")
97
+ self._xdg_rt_dir = os.environ.get("XDG_RUNTIME_DIR", "").rstrip("/")
91
98
  # the container user's one can be different because it is the root user for docker
92
99
  self._target_xdg_rt_dir = f"/run/user/{target_uid}"
93
100
  self._now = datetime.now()
@@ -148,6 +155,10 @@ class Environ:
148
155
  """if podman is the container manager being used"""
149
156
  return self._uses_podman
150
157
 
158
+ def systemd_user_conf_dir(self) -> str:
159
+ """standard configuration directory location of user specific systemd services"""
160
+ return f"{self._home_dir}/.config/systemd/user"
161
+
151
162
  @property
152
163
  def target_user(self) -> str:
153
164
  """username of the container user (which is the same as the current user for podman
@@ -20,11 +20,12 @@ copy_ybox_scripts_to_container(static_conf, distro_conf)
20
20
 
21
21
  # rename PKGMGR_CLEANUP to PKGMGR_CLEAN in pkgmgr.conf
22
22
  scripts_dir = static_conf.scripts_dir
23
- pkgmgr_conf = f"{scripts_dir}/pkgmgr.conf"
24
- with open(pkgmgr_conf, "r", encoding="utf-8") as pkgmgr_file:
25
- pkgmgr_data = pkgmgr_file.read()
26
- with open(pkgmgr_conf, "w", encoding="utf-8") as pkgmgr_file:
27
- pkgmgr_file.write(pkgmgr_data.replace("PKGMGR_CLEANUP", "PKGMGR_CLEAN"))
23
+ pkgmgr_conf = Path(f"{scripts_dir}/pkgmgr.conf")
24
+ if pkgmgr_conf.exists():
25
+ with pkgmgr_conf.open("r", encoding="utf-8") as pkgmgr_file:
26
+ pkgmgr_data = pkgmgr_file.read()
27
+ with pkgmgr_conf.open("w", encoding="utf-8") as pkgmgr_file:
28
+ pkgmgr_file.write(pkgmgr_data.replace("PKGMGR_CLEANUP", "PKGMGR_CLEAN"))
28
29
  # run entrypoint-root.sh again to refresh scripts and configuration
29
30
  subprocess.run([static_conf.env.docker_cmd, "exec", "-it", static_conf.box_name, "/usr/bin/sudo",
30
31
  "/bin/bash", f"{static_conf.target_scripts_dir}/entrypoint-root.sh"])
ybox/pkg/clean.py CHANGED
@@ -8,14 +8,10 @@ from configparser import SectionProxy
8
8
  from ybox.cmd import PkgMgr, build_shell_command, run_command
9
9
  from ybox.config import StaticConfiguration
10
10
  from ybox.print import print_info
11
- from ybox.state import RuntimeConfiguration, YboxStateManagement
12
11
 
13
12
 
14
- # noinspection PyUnusedLocal
15
13
  def clean_cache(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
16
- conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
17
- state: YboxStateManagement) -> int:
18
- # pylint: disable=unused-argument
14
+ conf: StaticConfiguration) -> int:
19
15
  """
20
16
  Clean package cache and related intermediate files.
21
17
 
@@ -23,8 +19,6 @@ def clean_cache(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
23
19
  :param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
24
20
  :param docker_cmd: the podman/docker executable to use
25
21
  :param conf: the :class:`StaticConfiguration` for the container
26
- :param runtime_conf: the `RuntimeConfiguration` of the container
27
- :param state: instance of `YboxStateManagement` having the state of all ybox containers
28
22
  :return: integer exit status of clean command where 0 represents success
29
23
  """
30
24
  print_info(f"Cleaning package cache in container '{conf.box_name}'")
ybox/pkg/info.py CHANGED
@@ -8,14 +8,10 @@ from configparser import SectionProxy
8
8
 
9
9
  from ybox.cmd import PkgMgr, page_command
10
10
  from ybox.config import StaticConfiguration
11
- from ybox.state import RuntimeConfiguration, YboxStateManagement
12
11
 
13
12
 
14
- # noinspection PyUnusedLocal
15
13
  def info_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
16
- conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
17
- state: YboxStateManagement) -> int:
18
- # pylint: disable=unused-argument
14
+ conf: StaticConfiguration) -> int:
19
15
  """
20
16
  Show detailed information of an installed or repository package(s).
21
17
 
@@ -23,8 +19,6 @@ def info_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: st
23
19
  :param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
24
20
  :param docker_cmd: the podman/docker executable to use
25
21
  :param conf: the :class:`StaticConfiguration` for the container
26
- :param runtime_conf: the `RuntimeConfiguration` of the container
27
- :param state: instance of `YboxStateManagement` having the state of all ybox containers
28
22
  :return: integer exit status of info command where 0 represents success
29
23
  """
30
24
  quiet_flag = pkgmgr[PkgMgr.QUIET_DETAILS_FLAG.value] if args.quiet else ""
ybox/pkg/inst.py CHANGED
@@ -24,10 +24,18 @@ from ybox.state import (CopyType, DependencyType, RuntimeConfiguration,
24
24
  from ybox.util import check_package, ini_file_reader, select_item_from_menu
25
25
 
26
26
  # match both "Exec=" and "TryExec=" lines (don't capture trailing newline)
27
- _EXEC_RE = re.compile(r"^(\s*(Try)?Exec\s*=\s*)(\S+)\s*(.*?)\s*$")
27
+ _EXEC_PATTERN = r"\s*((Try)?Exec\s*=\s*)(\S+)\s*(.*?)\s*"
28
+ # pattern to match icons with absolute paths and change them to just names
29
+ _ICON_PATH_PATTERN = r"\s*Icon\s*=\s*(/usr/share/(icons|pixmaps)/\S+)\s*"
30
+ # regex to match either of the two above
31
+ _EXEC_ICON_RE = re.compile(f"^{_EXEC_PATTERN}|{_ICON_PATH_PATTERN}$")
28
32
  # match !p and !a to replace executable program (third group above) and arguments respectively
29
33
  _FLAGS_RE = re.compile("![ap]")
30
- _LOCAL_BIN_DIRS = ["/usr/bin", "/bin", "/usr/sbin", "/sbin", "/usr/local/bin", "/usr/local/sbin"]
34
+ # environment variables passed through from host environment to podman/docker executable
35
+ _PASSTHROUGH_ENVVARS = ("XAUTHORITY", "DISPLAY", "WAYLAND_DISPLAY", "FREETYPE_PROPERTIES",
36
+ "SSH_AUTH_SOCK", "GPG_AGENT_INFO",
37
+ "__NV_PRIME_RENDER_OFFLOAD", "__GLX_VENDOR_LIBRARY_NAME",
38
+ "__VK_LAYER_NV_optimus", "VK_ICD_FILES", "VK_ICD_FILENAMES")
31
39
 
32
40
 
33
41
  def install_package(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
@@ -153,7 +161,10 @@ def _install_package(package: str, args: argparse.Namespace, install_cmd: str, l
153
161
  if not skip_desktop_files:
154
162
  copy_type |= CopyType.DESKTOP
155
163
  if not skip_executables:
156
- copy_type |= CopyType.EXECUTABLE
164
+ resp = input("Create wrapper(s) for application executable(s) of package "
165
+ f"'{package}'? (Y/n) ") if quiet == 0 else "Y"
166
+ if resp.strip().lower() != "n":
167
+ copy_type |= CopyType.EXECUTABLE
157
168
  # TODO: wrappers for newly installed required dependencies should also be created;
158
169
  # handle DependencyType.SUGGESTION if supported by underlying package manager
159
170
  app_flags: dict[str, str] = {}
@@ -223,13 +234,14 @@ def get_optional_deps(package: str, docker_cmd: str, container_name: str,
223
234
  # 2) redirect PKG: lines somewhere else like a common file: this can be done but will
224
235
  # likely be more messy than the code below (e.g. handle concurrent executions),
225
236
  # but still can be considered in future
237
+ # (reduced bufsize to show download progress better)
226
238
  with subprocess.Popen(build_shell_command(
227
239
  docker_cmd, container_name, f"{opt_deps_cmd} {package}"),
228
- stdout=subprocess.PIPE) as deps_result:
240
+ bufsize=256, stdout=subprocess.PIPE) as deps_result:
229
241
  line = bytearray()
230
242
  # possible end of lines
231
- eol1 = b"\r"[0]
232
- eol2 = b"\n"[0]
243
+ eol1 = ord(b"\r")
244
+ eol2 = ord(b"\n")
233
245
  buffered = 0
234
246
  assert deps_result.stdout is not None
235
247
  # readline does not work for in-place updates like from aria2
@@ -245,7 +257,7 @@ def get_optional_deps(package: str, docker_cmd: str, container_name: str,
245
257
  break
246
258
  else:
247
259
  line.append(char[0])
248
- if buffered >= 4: # flush frequently to show download progress, for example
260
+ if buffered >= 8: # flush frequently to show download progress, for example
249
261
  sys.stdout.flush()
250
262
  buffered = 0
251
263
  sys.stdout.flush()
@@ -419,7 +431,7 @@ def docker_cp_action(docker_cmd: str, box_name: str, src: str,
419
431
  with tempfile.TemporaryDirectory() as temp_dir:
420
432
  # use shell pipe and tar instead of python Popen and tarfile which will require much more
421
433
  # code unncessarily and may not be able to use `run_command`
422
- shell_cmd = (f"'{docker_cmd}' exec '{box_name}' tar -C '{src_dir}' -cpf - '{src_file}' | "
434
+ shell_cmd = (f"'{docker_cmd}' exec '{box_name}' tar -C '{src_dir}' -chpf - '{src_file}' | "
423
435
  f"tar -C '{temp_dir}' -xpf -")
424
436
  if (code := int(run_command(["/bin/sh", "-c", shell_cmd], exit_on_error=False,
425
437
  error_msg=f"copying of file from '{box_name}:{src}'"))) == 0:
@@ -430,9 +442,9 @@ def docker_cp_action(docker_cmd: str, box_name: str, src: str,
430
442
  def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticConfiguration,
431
443
  app_flags: dict[str, str], wrapper_files: list[str]) -> None:
432
444
  """
433
- For a desktop file, add "podman/docker exec ..." to its Exec/TryExec lines. Also read
434
- the additional flags for the command passed in `app_flags` and add them to an appropriate
435
- position in the Exec/TryExec lines.
445
+ For a desktop file, add "podman/docker exec ..." to its `Exec` lines. Also read the additional
446
+ flags for the command passed in `app_flags` and add them to an appropriate position in the
447
+ `Exec` lines. Also removes the `TryExec` lines that do not work in some desktop environments.
436
448
 
437
449
  :param filename: name of the desktop file being wrapped
438
450
  :param file: full path of the desktop file being wrapped
@@ -445,7 +457,13 @@ def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticCo
445
457
  # container name is added to desktop file to make it unique
446
458
  wrapper_name = f"ybox.{conf.box_name}.{filename}"
447
459
 
448
- def replace_executable(match: re.Match[str]) -> str:
460
+ def replace_exec_icon(match: re.Match[str]) -> str:
461
+ """replace Exec, TryExec and Icon lines appropriately for the host system"""
462
+ if not (exec_word := match.group(1)): # check for the case of `Icon=/usr/...`
463
+ return f"Icon={os.path.basename(match.group(5))}\n"
464
+ # remove TryExec lines that don't work in some desktop environments (like KDE plasma 5)
465
+ if match.group(2):
466
+ return ""
449
467
  program = match.group(3)
450
468
  args = match.group(4)
451
469
  # check for additional flags to be added
@@ -457,9 +475,9 @@ def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticCo
457
475
  else:
458
476
  full_cmd = program
459
477
  # pseudo-tty cannot be allocated with rootless docker outside of a terminal app
460
- return (f'{match.group(1)}{docker_cmd} exec -e=XAUTHORITY -e=DISPLAY '
461
- f'-e=FREETYPE_PROPERTIES {conf.box_name} /usr/local/bin/run-in-dir '
462
- f'"" {full_cmd}\n')
478
+ env_vars = " -e=".join(_PASSTHROUGH_ENVVARS)
479
+ return (f'{exec_word}{docker_cmd} exec -e={env_vars} {conf.box_name} '
480
+ f'/usr/local/bin/run-in-dir "" {full_cmd}\n')
463
481
 
464
482
  # the destination will be $HOME/.local/share/applications
465
483
  os.makedirs(conf.env.user_applications_dir, mode=Consts.default_directory_mode(),
@@ -470,7 +488,7 @@ def _wrap_desktop_file(filename: str, file: str, docker_cmd: str, conf: StaticCo
470
488
  def write_desktop_file(src: str) -> None:
471
489
  with open(wrapper_file, "w", encoding="utf-8") as wrapper_fd:
472
490
  with open(src, "r", encoding="utf-8") as src_fd:
473
- wrapper_fd.writelines(_EXEC_RE.sub(replace_executable, line) for line in src_fd)
491
+ wrapper_fd.writelines(_EXEC_ICON_RE.sub(replace_exec_icon, line) for line in src_fd)
474
492
  if docker_cp_action(docker_cmd, conf.box_name, file, write_desktop_file) == 0:
475
493
  wrapper_files.append(wrapper_file)
476
494
 
@@ -539,8 +557,8 @@ def _copy_app_icons(selected_icons: dict[str, tuple[float, str]], docker_cmd: st
539
557
  # copy from temporary file over the existing one, if any, to overwrite rather than move
540
558
  # (which will preserve all of its hard links, for example)
541
559
  exists = os.path.exists(target_icon_path)
542
- if docker_cp_action(docker_cmd, conf.box_name, icon_path,
543
- lambda src, dest=target_icon_path: shutil.copy2(src, dest)) == 0:
560
+ if docker_cp_action(docker_cmd, conf.box_name, icon_path, lambda src, dest=target_icon_path:
561
+ None if shutil.copy2(src, dest) else None) == 0: # "if" for pyright
544
562
  # skip registration of icon file it already existed and was overwritten so that
545
563
  # it is not removed on package uninstall
546
564
  if not exists:
@@ -568,9 +586,9 @@ def _can_wrap_executable(filename: str, file: str, conf: StaticConfiguration, qu
568
586
  print_warn(f"Skipping local wrapper for {file}")
569
587
  return False
570
588
  # also check if creating user executable will override system executable
571
- for bin_dir in _LOCAL_BIN_DIRS:
589
+ for bin_dir in Consts.sys_bin_dirs():
572
590
  sys_exec = f"{bin_dir}/{filename}"
573
- if os.path.exists(sys_exec):
591
+ if os.access(sys_exec, os.X_OK):
574
592
  resp = input(f"Target file {wrapper_exec} will override system installed "
575
593
  f"{sys_exec}. Continue? (y/N) ") if quiet < 2 else "N"
576
594
  if resp.strip().lower() != "y":
@@ -604,9 +622,9 @@ def _wrap_executable(filename: str, file: str, docker_cmd: str, conf: StaticConf
604
622
  lambda f_match: _replace_flags(f_match, flags, f'"{file}"', '"$@"'), flags)
605
623
  else:
606
624
  full_cmd = f'/usr/local/bin/run-in-dir "`pwd`" "{file}" "$@"'
625
+ env_vars = " -e=".join(_PASSTHROUGH_ENVVARS)
607
626
  exec_content = ("#!/bin/sh\n",
608
- f"exec {docker_cmd} exec -it -e=XAUTHORITY -e=DISPLAY "
609
- f"-e=FREETYPE_PROPERTIES {conf.box_name} ", full_cmd)
627
+ f"exec {docker_cmd} exec -it -e={env_vars} {conf.box_name} ", full_cmd)
610
628
  with open(wrapper_exec, "w", encoding="utf-8") as wrapper_fd:
611
629
  wrapper_fd.writelines(exec_content)
612
630
  os.chmod(wrapper_exec, mode=0o755, follow_symlinks=True)
ybox/pkg/list.py CHANGED
@@ -164,11 +164,8 @@ def _format_long_line(line: str, separator: str, dep_of_width: int,
164
164
  return name, version, dep_of, description
165
165
 
166
166
 
167
- # noinspection PyUnusedLocal
168
167
  def list_files(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
169
- conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
170
- state: YboxStateManagement) -> int:
171
- # pylint: disable=unused-argument
168
+ conf: StaticConfiguration) -> int:
172
169
  """
173
170
  List the files of a package installed in a container including those not managed by `ybox-pkg`.
174
171
 
@@ -176,8 +173,6 @@ def list_files(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
176
173
  :param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
177
174
  :param docker_cmd: the podman/docker executable to use
178
175
  :param conf: the :class:`StaticConfiguration` for the container
179
- :param runtime_conf: the `RuntimeConfiguration` of the container
180
- :param state: instance of `YboxStateManagement` having the state of all ybox containers
181
176
  :return: integer exit status of list package files command where 0 represents success
182
177
  """
183
178
  package: str = args.package
ybox/pkg/mark.py CHANGED
@@ -30,7 +30,7 @@ def mark_package(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str
30
30
  mark_explicit: bool = args.explicit
31
31
  mark_dependency_of: str = args.dependency_of or ""
32
32
  if not mark_explicit ^ bool(mark_dependency_of):
33
- print_error("ybox-pkg mark: exactly one of -e or -D option must be specified "
33
+ print_error("ybox-pkg mark: exactly one of -e or -d option must be specified "
34
34
  f"(explicit={mark_explicit}, dependency-of={mark_dependency_of})")
35
35
  return 1
36
36
  # check that the package(s) are installed and replace with actual installed name
ybox/pkg/repair.py CHANGED
@@ -14,6 +14,10 @@ from ybox.print import (fgcolor, print_color, print_error, print_info,
14
14
  print_warn)
15
15
  from ybox.state import RuntimeConfiguration, YboxStateManagement
16
16
 
17
+ # TODO: SW: repair should work even if no containers are active (just remove the locks)
18
+ # Also, just checking lock file existing lock file existence is not enough (e.g. for dpkg/apt)
19
+ # and may need to check if they are locked by any process after kill so define this in distro.ini
20
+
17
21
 
18
22
  def repair_package_state(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
19
23
  conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
ybox/pkg/search.py CHANGED
@@ -8,14 +8,10 @@ from configparser import SectionProxy
8
8
 
9
9
  from ybox.cmd import PkgMgr, page_command
10
10
  from ybox.config import StaticConfiguration
11
- from ybox.state import RuntimeConfiguration, YboxStateManagement
12
11
 
13
12
 
14
- # noinspection PyUnusedLocal
15
13
  def search_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
16
- conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
17
- state: YboxStateManagement) -> int:
18
- # pylint: disable=unused-argument
14
+ conf: StaticConfiguration) -> int:
19
15
  """
20
16
  Uninstall package specified by `args.package` on a ybox container with given podman/docker
21
17
  command. Additional flags honored are `args.quiet` to bypass user confirmation during
@@ -27,8 +23,6 @@ def search_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd:
27
23
  :param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
28
24
  :param docker_cmd: the podman/docker executable to use
29
25
  :param conf: the :class:`StaticConfiguration` for the container
30
- :param runtime_conf: the `RuntimeConfiguration` of the container
31
- :param state: instance of `YboxStateManagement` having the state of all ybox containers
32
26
  :return: integer exit status of search command where 0 represents success
33
27
  """
34
28
  quiet_flag = pkgmgr[PkgMgr.QUIET_DETAILS_FLAG.value] if args.quiet else ""
ybox/run/cmd.py CHANGED
@@ -5,7 +5,7 @@ Code for the `ybox-cmd` script that is used to execute programs in an active ybo
5
5
  import argparse
6
6
  import sys
7
7
 
8
- from ybox.cmd import run_command
8
+ from ybox.cmd import parser_version_check, run_command
9
9
  from ybox.env import get_docker_command
10
10
 
11
11
 
@@ -51,4 +51,5 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
51
51
  parser.add_argument("container_name", type=str, help="name of the active ybox")
52
52
  parser.add_argument("command", nargs="*", default="/bin/bash",
53
53
  help="run the given command (default is /bin/bash)")
54
+ parser_version_check(parser, argv)
54
55
  return parser.parse_args(argv)
ybox/run/control.py CHANGED
@@ -6,51 +6,70 @@ import argparse
6
6
  import sys
7
7
  import time
8
8
 
9
- from ybox.cmd import check_active_ybox, get_ybox_state, run_command
9
+ from ybox.cmd import (check_active_ybox, get_ybox_state, parser_version_check,
10
+ run_command)
10
11
  from ybox.config import StaticConfiguration
11
12
  from ybox.env import Environ, get_docker_command
12
13
  from ybox.print import fgcolor, print_color, print_error
13
14
  from ybox.util import wait_for_ybox_container
14
15
 
16
+ # TODO: SW: add backup/restore of the running image with the option to backup $HOME of the
17
+ # container user, and shared root warning if it is being used
18
+
15
19
 
16
20
  def main() -> None:
17
21
  """main function for `ybox-control` script"""
18
22
  main_argv(sys.argv[1:])
19
23
 
20
24
 
21
- def start_container(docker_cmd: str, container_name: str):
25
+ def start_container(docker_cmd: str, args: argparse.Namespace):
22
26
  """
23
27
  Start an existing ybox container.
24
28
 
25
29
  :param docker_cmd: the podman/docker executable to use
26
- :param container_name: name of the container
30
+ :param args: arguments having all attributes passed by the user
27
31
  """
32
+ container_name = args.container
28
33
  if status := get_ybox_state(docker_cmd, container_name, (), exit_on_error=False):
29
34
  if status[0] == "running":
30
- print_color(f"Ybox container '{container_name}' already active", fg=fgcolor.cyan)
35
+ print_color(f"ybox container '{container_name}' already active", fg=fgcolor.cyan)
31
36
  else:
32
37
  print_color(f"Starting ybox container '{container_name}'", fg=fgcolor.cyan)
33
38
  run_command([docker_cmd, "container", "start", container_name],
34
39
  error_msg="container start")
35
40
  conf = StaticConfiguration(Environ(docker_cmd), status[1], container_name)
36
- wait_for_ybox_container(docker_cmd, conf)
41
+ wait_for_ybox_container(docker_cmd, conf, args.timeout)
37
42
  else:
38
43
  print_error(f"No ybox container '{container_name}' found")
39
44
  sys.exit(1)
40
45
 
41
46
 
42
- def stop_container(docker_cmd: str, container_name: str, fail_on_error: bool):
47
+ def stop_container(docker_cmd: str, args: argparse.Namespace):
43
48
  """
44
- Stop a ybox container.
49
+ Stop an active ybox container.
50
+
51
+ :param docker_cmd: the podman/docker executable to use
52
+ :param args: arguments having all attributes passed by the user
53
+ """
54
+ _stop_container(docker_cmd, args.container, args.timeout,
55
+ fail_on_error=not args.ignore_stopped)
56
+
57
+
58
+ def _stop_container(docker_cmd: str, container_name: str, timeout: int,
59
+ fail_on_error: bool):
60
+ """
61
+ Stop an active ybox container.
45
62
 
46
63
  :param docker_cmd: the podman/docker executable to use
47
64
  :param container_name: name of the container
65
+ :param timeout: seconds to wait for container to stop before killing the container
48
66
  :param fail_on_error: if True then show error message on failure to stop else ignore
49
67
  """
50
68
  if check_active_ybox(docker_cmd, container_name):
51
69
  print_color(f"Stopping ybox container '{container_name}'", fg=fgcolor.cyan)
52
- run_command([docker_cmd, "container", "stop", container_name], error_msg="container stop")
53
- for _ in range(120):
70
+ run_command([docker_cmd, "container", "stop", "-t", str(timeout), container_name],
71
+ error_msg="container stop")
72
+ for _ in range(timeout * 2):
54
73
  time.sleep(0.5)
55
74
  if get_ybox_state(docker_cmd, container_name, ("exited", "stopped"),
56
75
  exit_on_error=False, state_msg=" stopped"):
@@ -63,6 +82,42 @@ def stop_container(docker_cmd: str, container_name: str, fail_on_error: bool):
63
82
  print_color(f"No active ybox container '{container_name}' found", fg=fgcolor.cyan)
64
83
 
65
84
 
85
+ def restart_container(docker_cmd: str, args: argparse.Namespace):
86
+ """
87
+ Restart an existing ybox container.
88
+
89
+ :param docker_cmd: the podman/docker executable to use
90
+ :param args: arguments having all attributes passed by the user
91
+ """
92
+ _stop_container(docker_cmd, args.container, timeout=int(args.timeout / 2), fail_on_error=False)
93
+ start_container(docker_cmd, args)
94
+
95
+
96
+ def show_container_status(docker_cmd: str, args: argparse.Namespace) -> None:
97
+ """
98
+ Show container status which will be a string like running/exited.
99
+
100
+ :param docker_cmd: the podman/docker executable to use
101
+ :param args: arguments having all attributes passed by the user
102
+ """
103
+ container_name = args.container
104
+ if status := get_ybox_state(docker_cmd, container_name, (), exit_on_error=False):
105
+ print(status[0])
106
+ else:
107
+ print_error(f"No ybox container '{container_name}' found")
108
+
109
+
110
+ def wait_for_container_stop(docker_cmd: str, args: argparse.Namespace) -> None:
111
+ """
112
+ Wait for an active container to stop.
113
+
114
+ :param docker_cmd: the podman/docker executable to use
115
+ :param args: arguments having all attributes passed by the user
116
+ """
117
+ while check_active_ybox(docker_cmd, args.container):
118
+ time.sleep(2)
119
+
120
+
66
121
  def main_argv(argv: list[str]) -> None:
67
122
  """
68
123
  Main entrypoint of `ybox-control` that takes a list of arguments which are usually the
@@ -73,19 +128,7 @@ def main_argv(argv: list[str]) -> None:
73
128
  """
74
129
  args = parse_args(argv)
75
130
  docker_cmd = get_docker_command()
76
- container_name = args.container_name
77
- if args.action == "start":
78
- start_container(docker_cmd, container_name)
79
- elif args.action == "stop":
80
- stop_container(docker_cmd, container_name, fail_on_error=True)
81
- elif args.action == "restart":
82
- stop_container(docker_cmd, container_name, fail_on_error=False)
83
- start_container(docker_cmd, container_name)
84
- elif args.action == "status":
85
- if status := get_ybox_state(docker_cmd, container_name, (), exit_on_error=False):
86
- print(status[0])
87
- else:
88
- print_error(f"No ybox container '{container_name}' found")
131
+ args.func(docker_cmd, args)
89
132
 
90
133
 
91
134
  def parse_args(argv: list[str]) -> argparse.Namespace:
@@ -96,7 +139,46 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
96
139
  :return: the result of parsing using the `argparse` library as a :class:`argparse.Namespace`
97
140
  """
98
141
  parser = argparse.ArgumentParser(description="control ybox containers")
99
- parser.add_argument("action", choices=["start", "stop", "restart", "status"],
100
- help="action to perform")
101
- parser.add_argument("container_name", help="name of the ybox")
142
+ operations = parser.add_subparsers(title="Operations", required=True, metavar="OPERATION",
143
+ help="DESCRIPTION")
144
+
145
+ start = operations.add_parser("start", help="start a ybox container")
146
+ _add_subparser_args(start, 60, "time in seconds to wait for a container to start")
147
+ start.set_defaults(func=start_container)
148
+
149
+ stop = operations.add_parser("stop", help="stop a ybox container")
150
+ _add_subparser_args(stop, 10,
151
+ "time in seconds to wait for a container to stop before killing it")
152
+ stop.add_argument("-I", "--ignore-stopped", action="store_true",
153
+ help="don't fail on an already stopped container")
154
+ stop.set_defaults(func=stop_container)
155
+
156
+ restart = operations.add_parser("restart", help="restart a ybox container")
157
+ _add_subparser_args(restart, 60, "time in seconds to wait for a container to restart")
158
+ restart.set_defaults(func=restart_container)
159
+
160
+ status = operations.add_parser("status", help="show status of a ybox container")
161
+ _add_subparser_args(status, 0, "")
162
+ status.set_defaults(func=show_container_status)
163
+
164
+ wait = operations.add_parser("wait", help="wait for an active ybox container to stop")
165
+ _add_subparser_args(wait, 0, "")
166
+ wait.set_defaults(func=wait_for_container_stop)
167
+
168
+ parser_version_check(parser, argv)
102
169
  return parser.parse_args(argv)
170
+
171
+
172
+ def _add_subparser_args(subparser: argparse.ArgumentParser, timeout_default: int,
173
+ timeout_help: str) -> None:
174
+ """
175
+ Add arguments for the sub-operation of the ybox-control command.
176
+
177
+ :param subparser: the :class:`argparse.ArgumentParser` object for the sub-command
178
+ :param timeout_default: default value for the -t/--timeout argument, or 0 to skip the argument
179
+ :param timeout_help: help string for the -t/--timeout argument
180
+ """
181
+ if timeout_default != 0:
182
+ subparser.add_argument("-t", "--timeout", type=int, default=timeout_default,
183
+ help=timeout_help)
184
+ subparser.add_argument("container", help="name of the ybox")