machineconfig 1.97__py3-none-any.whl → 2.1__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.

Potentially problematic release.


This version of machineconfig might be problematic. Click here for more details.

Files changed (268) hide show
  1. machineconfig/cluster/cloud_manager.py +22 -29
  2. machineconfig/cluster/data_transfer.py +2 -3
  3. machineconfig/cluster/distribute.py +0 -2
  4. machineconfig/cluster/file_manager.py +4 -5
  5. machineconfig/cluster/job_params.py +1 -4
  6. machineconfig/cluster/loader_runner.py +8 -11
  7. machineconfig/cluster/remote_machine.py +4 -5
  8. machineconfig/cluster/script_execution.py +2 -2
  9. machineconfig/cluster/script_notify_upon_completion.py +0 -1
  10. machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +4 -6
  11. machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -1
  12. machineconfig/cluster/sessions_managers/enhanced_command_runner.py +35 -75
  13. machineconfig/cluster/sessions_managers/wt_local.py +113 -185
  14. machineconfig/cluster/sessions_managers/wt_local_manager.py +127 -197
  15. machineconfig/cluster/sessions_managers/wt_remote.py +60 -67
  16. machineconfig/cluster/sessions_managers/wt_remote_manager.py +110 -149
  17. machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +61 -64
  18. machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +72 -172
  19. machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +27 -60
  20. machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +58 -137
  21. machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +46 -74
  22. machineconfig/cluster/sessions_managers/zellij_local.py +91 -147
  23. machineconfig/cluster/sessions_managers/zellij_local_manager.py +165 -190
  24. machineconfig/cluster/sessions_managers/zellij_remote.py +51 -58
  25. machineconfig/cluster/sessions_managers/zellij_remote_manager.py +40 -46
  26. machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +19 -17
  27. machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +30 -31
  28. machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +64 -134
  29. machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +7 -11
  30. machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +27 -55
  31. machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +14 -13
  32. machineconfig/cluster/templates/cli_click.py +0 -1
  33. machineconfig/cluster/templates/cli_gooey.py +0 -2
  34. machineconfig/cluster/templates/cli_trogon.py +0 -1
  35. machineconfig/cluster/templates/run_cloud.py +0 -1
  36. machineconfig/cluster/templates/run_cluster.py +0 -1
  37. machineconfig/cluster/templates/run_remote.py +0 -1
  38. machineconfig/cluster/templates/utils.py +27 -11
  39. machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
  40. machineconfig/jobs/linux/msc/cli_agents.sh +16 -0
  41. machineconfig/jobs/python/check_installations.py +9 -9
  42. machineconfig/jobs/python/create_bootable_media.py +0 -2
  43. machineconfig/jobs/python/python_cargo_build_share.py +2 -2
  44. machineconfig/jobs/python/python_ve_symlink.py +9 -11
  45. machineconfig/jobs/python/tasks.py +0 -1
  46. machineconfig/jobs/python/vscode/api.py +5 -5
  47. machineconfig/jobs/python/vscode/link_ve.py +20 -21
  48. machineconfig/jobs/python/vscode/select_interpreter.py +28 -29
  49. machineconfig/jobs/python/vscode/sync_code.py +14 -18
  50. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  51. machineconfig/jobs/python_custom_installers/archive/ngrok.py +15 -15
  52. machineconfig/jobs/python_custom_installers/dev/aider.py +10 -18
  53. machineconfig/jobs/python_custom_installers/dev/alacritty.py +12 -21
  54. machineconfig/jobs/python_custom_installers/dev/brave.py +13 -22
  55. machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +13 -20
  56. machineconfig/jobs/python_custom_installers/dev/code.py +17 -24
  57. machineconfig/jobs/python_custom_installers/dev/cursor.py +10 -21
  58. machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +12 -11
  59. machineconfig/jobs/python_custom_installers/dev/espanso.py +19 -23
  60. machineconfig/jobs/python_custom_installers/dev/goes.py +9 -16
  61. machineconfig/jobs/python_custom_installers/dev/lvim.py +13 -21
  62. machineconfig/jobs/python_custom_installers/dev/nerdfont.py +15 -22
  63. machineconfig/jobs/python_custom_installers/dev/redis.py +15 -23
  64. machineconfig/jobs/python_custom_installers/dev/wezterm.py +15 -22
  65. machineconfig/jobs/python_custom_installers/dev/winget.py +32 -50
  66. machineconfig/jobs/python_custom_installers/docker.py +15 -24
  67. machineconfig/jobs/python_custom_installers/gh.py +18 -26
  68. machineconfig/jobs/python_custom_installers/hx.py +33 -17
  69. machineconfig/jobs/python_custom_installers/warp-cli.py +15 -23
  70. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  71. machineconfig/jobs/python_generic_installers/config.json +412 -389
  72. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  73. machineconfig/jobs/python_windows_installers/dev/config.json +1 -1
  74. machineconfig/jobs/windows/archive/archive_pygraphviz.ps1 +1 -1
  75. machineconfig/jobs/windows/msc/cli_agents.bat +0 -0
  76. machineconfig/jobs/windows/msc/cli_agents.ps1 +0 -0
  77. machineconfig/jobs/windows/start_terminal.ps1 +1 -1
  78. machineconfig/logger.py +50 -0
  79. machineconfig/profile/create.py +50 -36
  80. machineconfig/profile/create_hardlinks.py +33 -26
  81. machineconfig/profile/shell.py +87 -60
  82. machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  83. machineconfig/scripts/cloud/init.sh +2 -2
  84. machineconfig/scripts/linux/checkout_versions +1 -1
  85. machineconfig/scripts/linux/choose_wezterm_theme +1 -1
  86. machineconfig/scripts/linux/cloud_copy +1 -1
  87. machineconfig/scripts/linux/cloud_manager +1 -1
  88. machineconfig/scripts/linux/cloud_mount +1 -1
  89. machineconfig/scripts/linux/cloud_repo_sync +1 -1
  90. machineconfig/scripts/linux/cloud_sync +1 -1
  91. machineconfig/scripts/linux/croshell +1 -1
  92. machineconfig/scripts/linux/devops +3 -5
  93. machineconfig/scripts/linux/fire +2 -1
  94. machineconfig/scripts/linux/fire_agents +3 -3
  95. machineconfig/scripts/linux/ftpx +1 -1
  96. machineconfig/scripts/linux/gh_models +1 -1
  97. machineconfig/scripts/linux/kill_process +1 -1
  98. machineconfig/scripts/linux/mcinit +2 -2
  99. machineconfig/scripts/linux/repos +1 -1
  100. machineconfig/scripts/linux/scheduler +1 -1
  101. machineconfig/scripts/linux/start_slidev +1 -1
  102. machineconfig/scripts/linux/start_terminals +1 -1
  103. machineconfig/scripts/linux/url2md +1 -1
  104. machineconfig/scripts/linux/warp-cli.sh +122 -0
  105. machineconfig/scripts/linux/wifi_conn +1 -1
  106. machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
  107. machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
  108. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  109. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
  110. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  111. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
  112. machineconfig/scripts/python/ai/__init__.py +0 -0
  113. machineconfig/scripts/python/ai/__pycache__/__init__.cpython-313.pyc +0 -0
  114. machineconfig/scripts/python/ai/__pycache__/generate_files.cpython-313.pyc +0 -0
  115. machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-313.pyc +0 -0
  116. machineconfig/scripts/python/ai/chatmodes/Thinking-Beast-Mode.chatmode.md +337 -0
  117. machineconfig/scripts/python/ai/chatmodes/Ultimate-Transparent-Thinking-Beast-Mode.chatmode.md +644 -0
  118. machineconfig/scripts/python/ai/chatmodes/deepResearch.chatmode.md +81 -0
  119. machineconfig/scripts/python/ai/configs/.gemini/settings.json +81 -0
  120. machineconfig/scripts/python/ai/generate_files.py +84 -0
  121. machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +45 -0
  122. machineconfig/scripts/python/ai/mcinit.py +107 -0
  123. machineconfig/scripts/python/ai/prompts/allLintersAndTypeCheckers.prompt.md +5 -0
  124. machineconfig/scripts/python/ai/prompts/research-report-skeleton.prompt.md +38 -0
  125. machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +52 -0
  126. machineconfig/scripts/python/archive/tmate_conn.py +5 -5
  127. machineconfig/scripts/python/archive/tmate_start.py +3 -3
  128. machineconfig/scripts/python/choose_wezterm_theme.py +2 -2
  129. machineconfig/scripts/python/cloud_copy.py +20 -19
  130. machineconfig/scripts/python/cloud_mount.py +10 -8
  131. machineconfig/scripts/python/cloud_repo_sync.py +15 -15
  132. machineconfig/scripts/python/cloud_sync.py +1 -1
  133. machineconfig/scripts/python/croshell.py +18 -16
  134. machineconfig/scripts/python/devops.py +6 -6
  135. machineconfig/scripts/python/devops_add_identity.py +9 -7
  136. machineconfig/scripts/python/devops_add_ssh_key.py +19 -19
  137. machineconfig/scripts/python/devops_backup_retrieve.py +14 -14
  138. machineconfig/scripts/python/devops_devapps_install.py +3 -3
  139. machineconfig/scripts/python/devops_update_repos.py +141 -53
  140. machineconfig/scripts/python/dotfile.py +3 -3
  141. machineconfig/scripts/python/fire_agents.py +202 -41
  142. machineconfig/scripts/python/fire_jobs.py +20 -21
  143. machineconfig/scripts/python/ftpx.py +4 -3
  144. machineconfig/scripts/python/gh_models.py +94 -94
  145. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
  146. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
  147. machineconfig/scripts/python/helpers/cloud_helpers.py +3 -3
  148. machineconfig/scripts/python/helpers/helpers2.py +3 -3
  149. machineconfig/scripts/python/helpers/helpers4.py +8 -7
  150. machineconfig/scripts/python/helpers/helpers5.py +7 -7
  151. machineconfig/scripts/python/helpers/repo_sync_helpers.py +2 -2
  152. machineconfig/scripts/python/mount_nfs.py +4 -3
  153. machineconfig/scripts/python/mount_nw_drive.py +4 -4
  154. machineconfig/scripts/python/mount_ssh.py +4 -3
  155. machineconfig/scripts/python/repos.py +9 -9
  156. machineconfig/scripts/python/scheduler.py +1 -1
  157. machineconfig/scripts/python/start_slidev.py +9 -8
  158. machineconfig/scripts/python/start_terminals.py +1 -1
  159. machineconfig/scripts/python/viewer.py +40 -40
  160. machineconfig/scripts/python/wifi_conn.py +65 -66
  161. machineconfig/scripts/python/wsl_windows_transfer.py +2 -2
  162. machineconfig/scripts/windows/checkout_version.ps1 +1 -3
  163. machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -3
  164. machineconfig/scripts/windows/cloud_copy.ps1 +2 -6
  165. machineconfig/scripts/windows/cloud_manager.ps1 +1 -1
  166. machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -2
  167. machineconfig/scripts/windows/cloud_sync.ps1 +2 -2
  168. machineconfig/scripts/windows/croshell.ps1 +2 -2
  169. machineconfig/scripts/windows/devops.ps1 +1 -4
  170. machineconfig/scripts/windows/dotfile.ps1 +1 -3
  171. machineconfig/scripts/windows/fire.ps1 +1 -1
  172. machineconfig/scripts/windows/ftpx.ps1 +2 -2
  173. machineconfig/scripts/windows/gpt.ps1 +1 -1
  174. machineconfig/scripts/windows/kill_process.ps1 +1 -2
  175. machineconfig/scripts/windows/mcinit.ps1 +2 -2
  176. machineconfig/scripts/windows/mount_nfs.ps1 +1 -1
  177. machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
  178. machineconfig/scripts/windows/pomodoro.ps1 +1 -1
  179. machineconfig/scripts/windows/py2exe.ps1 +1 -3
  180. machineconfig/scripts/windows/repos.ps1 +1 -1
  181. machineconfig/scripts/windows/scheduler.ps1 +1 -1
  182. machineconfig/scripts/windows/snapshot.ps1 +2 -2
  183. machineconfig/scripts/windows/start_slidev.ps1 +1 -1
  184. machineconfig/scripts/windows/start_terminals.ps1 +1 -1
  185. machineconfig/scripts/windows/wifi_conn.ps1 +1 -1
  186. machineconfig/scripts/windows/wsl_windows_transfer.ps1 +1 -3
  187. machineconfig/settings/lf/linux/lfrc +1 -1
  188. machineconfig/settings/linters/.ruff.toml +2 -2
  189. machineconfig/settings/linters/.ruff_cache/.gitignore +2 -0
  190. machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +1 -0
  191. machineconfig/settings/lvim/windows/archive/config_additional.lua +1 -1
  192. machineconfig/settings/shells/ipy/profiles/default/startup/playext.py +71 -71
  193. machineconfig/settings/shells/wt/settings.json +8 -8
  194. machineconfig/settings/svim/linux/init.toml +1 -1
  195. machineconfig/settings/svim/windows/init.toml +1 -1
  196. machineconfig/setup_linux/web_shortcuts/croshell.sh +0 -54
  197. machineconfig/setup_linux/web_shortcuts/interactive.sh +6 -6
  198. machineconfig/setup_linux/web_shortcuts/tmp.sh +2 -0
  199. machineconfig/setup_windows/web_shortcuts/all.ps1 +2 -2
  200. machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +1 -1
  201. machineconfig/setup_windows/web_shortcuts/croshell.ps1 +1 -1
  202. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +5 -5
  203. machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1 +51 -15
  204. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +75 -18
  205. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +52 -42
  206. machineconfig/utils/ai/browser_user_wrapper.py +5 -5
  207. machineconfig/utils/ai/generate_file_checklist.py +19 -22
  208. machineconfig/utils/ai/url2md.py +5 -3
  209. machineconfig/utils/cloud/onedrive/setup_oauth.py +5 -4
  210. machineconfig/utils/cloud/onedrive/transaction.py +192 -227
  211. machineconfig/utils/code.py +71 -43
  212. machineconfig/utils/installer.py +77 -85
  213. machineconfig/utils/installer_utils/installer_abc.py +29 -17
  214. machineconfig/utils/installer_utils/installer_class.py +188 -83
  215. machineconfig/utils/io_save.py +3 -15
  216. machineconfig/utils/links.py +22 -11
  217. machineconfig/utils/notifications.py +197 -0
  218. machineconfig/utils/options.py +38 -25
  219. machineconfig/utils/path.py +18 -6
  220. machineconfig/utils/path_reduced.py +637 -316
  221. machineconfig/utils/procs.py +69 -63
  222. machineconfig/utils/scheduling.py +11 -13
  223. machineconfig/utils/ssh.py +351 -0
  224. machineconfig/utils/terminal.py +225 -0
  225. machineconfig/utils/utils.py +13 -12
  226. machineconfig/utils/utils2.py +43 -10
  227. machineconfig/utils/utils5.py +242 -46
  228. machineconfig/utils/ve.py +11 -6
  229. {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/METADATA +15 -9
  230. {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/RECORD +232 -235
  231. machineconfig/cluster/self_ssh.py +0 -57
  232. machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
  233. machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
  234. machineconfig/jobs/python/archive/python_tools.txt +0 -12
  235. machineconfig/jobs/python/vscode/__pycache__/select_interpreter.cpython-311.pyc +0 -0
  236. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  237. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  238. machineconfig/jobs/python_generic_installers/update.py +0 -3
  239. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  240. machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
  241. machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
  242. machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
  243. machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  244. machineconfig/scripts/linux/activate_ve +0 -87
  245. machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
  246. machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
  247. machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
  248. machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
  249. machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
  250. machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
  251. machineconfig/scripts/python/__pycache__/devops_backup_retrieve.cpython-311.pyc +0 -0
  252. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
  253. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
  254. machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
  255. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
  256. machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
  257. machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
  258. machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
  259. machineconfig/scripts/python/ai/init.py +0 -56
  260. machineconfig/scripts/python/ai/rules/python/dev.md +0 -31
  261. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
  262. machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
  263. machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
  264. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
  265. machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
  266. machineconfig/scripts/windows/activate_ve.ps1 +0 -54
  267. {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/WHEEL +0 -0
  268. {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,7 @@
2
2
  """
3
3
  Process monitoring and status checking utilities for remote commands.
4
4
  """
5
+
5
6
  import json
6
7
  import shlex
7
8
  import logging
@@ -13,91 +14,48 @@ logger = logging.getLogger(__name__)
13
14
 
14
15
  class ProcessMonitor:
15
16
  """Handles process status checking and verification on remote machines."""
16
-
17
+
17
18
  def __init__(self, remote_executor: RemoteExecutor):
18
19
  self.remote_executor = remote_executor
19
-
20
- def check_command_status(self, tab_name: str, tab_config: Dict[str, Tuple[str, str]],
21
- use_verification: bool = True) -> Dict[str, Any]:
20
+
21
+ def check_command_status(self, tab_name: str, tab_config: Dict[str, Tuple[str, str]], use_verification: bool = True) -> Dict[str, Any]:
22
22
  """Check command status with optional process verification."""
23
23
  if tab_name not in tab_config:
24
- return {
25
- "status": "unknown",
26
- "error": f"Tab '{tab_name}' not found in tracked configuration",
27
- "running": False,
28
- "pid": None,
29
- "command": None,
30
- "remote": self.remote_executor.remote_name
31
- }
32
-
24
+ return {"status": "unknown", "error": f"Tab '{tab_name}' not found in tracked configuration", "running": False, "pid": None, "command": None, "remote": self.remote_executor.remote_name}
25
+
33
26
  # Use the verified method by default for more accurate results
34
27
  if use_verification:
35
28
  return self.get_verified_process_status(tab_name, tab_config)
36
-
29
+
37
30
  return self._basic_process_check(tab_name, tab_config)
38
-
31
+
39
32
  def _basic_process_check(self, tab_name: str, tab_config: Dict[str, Tuple[str, str]]) -> Dict[str, Any]:
40
33
  """Basic process checking without verification."""
41
34
  _, command = tab_config[tab_name]
42
-
35
+
43
36
  try:
44
37
  check_script = self._create_process_check_script(command)
45
- remote_cmd = f"$HOME/venvs/ve/bin/python -c {shlex.quote(check_script)}"
38
+ remote_cmd = f"$HOME/code/machineconfig/.venv/bin/python -c {shlex.quote(check_script)}"
46
39
  result = self.remote_executor.run_command(remote_cmd, timeout=15)
47
-
40
+
48
41
  if result.returncode == 0:
49
42
  try:
50
43
  matching_processes = json.loads(result.stdout.strip())
51
-
44
+
52
45
  if matching_processes:
53
- return {
54
- "status": "running",
55
- "running": True,
56
- "processes": matching_processes,
57
- "command": command,
58
- "tab_name": tab_name,
59
- "remote": self.remote_executor.remote_name
60
- }
46
+ return {"status": "running", "running": True, "processes": matching_processes, "command": command, "tab_name": tab_name, "remote": self.remote_executor.remote_name}
61
47
  else:
62
- return {
63
- "status": "not_running",
64
- "running": False,
65
- "processes": [],
66
- "command": command,
67
- "tab_name": tab_name,
68
- "remote": self.remote_executor.remote_name
69
- }
48
+ return {"status": "not_running", "running": False, "processes": [], "command": command, "tab_name": tab_name, "remote": self.remote_executor.remote_name}
70
49
  except json.JSONDecodeError as e:
71
50
  logger.error(f"Failed to parse remote process check output: {e}")
72
- return {
73
- "status": "error",
74
- "error": f"Failed to parse remote output: {e}",
75
- "running": False,
76
- "command": command,
77
- "tab_name": tab_name,
78
- "remote": self.remote_executor.remote_name
79
- }
51
+ return {"status": "error", "error": f"Failed to parse remote output: {e}", "running": False, "command": command, "tab_name": tab_name, "remote": self.remote_executor.remote_name}
80
52
  else:
81
- return {
82
- "status": "error",
83
- "error": f"Remote command failed: {result.stderr}",
84
- "running": False,
85
- "command": command,
86
- "tab_name": tab_name,
87
- "remote": self.remote_executor.remote_name
88
- }
89
-
53
+ return {"status": "error", "error": f"Remote command failed: {result.stderr}", "running": False, "command": command, "tab_name": tab_name, "remote": self.remote_executor.remote_name}
54
+
90
55
  except Exception as e:
91
56
  logger.error(f"Error checking command status for tab '{tab_name}': {e}")
92
- return {
93
- "status": "error",
94
- "error": str(e),
95
- "running": False,
96
- "command": command,
97
- "tab_name": tab_name,
98
- "remote": self.remote_executor.remote_name
99
- }
100
-
57
+ return {"status": "error", "error": str(e), "running": False, "command": command, "tab_name": tab_name, "remote": self.remote_executor.remote_name}
58
+
101
59
  def _create_process_check_script(self, command: str) -> str:
102
60
  """Create Python script for checking processes on remote machine."""
103
61
  return f"""
@@ -110,25 +68,25 @@ def check_process():
110
68
  full_command = '{command}'
111
69
  cmd_parts = [part for part in full_command.split() if len(part) > 2]
112
70
  current_pid = os.getpid()
113
-
71
+
114
72
  primary_cmd = cmd_parts[0] if cmd_parts else ''
115
-
73
+
116
74
  for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'status', 'create_time']):
117
75
  try:
118
76
  if proc.info['pid'] == current_pid:
119
77
  continue
120
-
78
+
121
79
  if proc.info['cmdline'] and len(proc.info['cmdline']) > 0:
122
80
  cmdline_str = ' '.join(proc.info['cmdline'])
123
-
81
+
124
82
  if 'check_process()' in cmdline_str or 'psutil.process_iter' in cmdline_str:
125
83
  continue
126
-
84
+
127
85
  matches_primary = primary_cmd in cmdline_str
128
86
  matches_parts = sum(1 for part in cmd_parts[1:] if part in cmdline_str)
129
-
87
+
130
88
  if (matches_primary and matches_parts >= 2) or \\
131
- (full_command in cmdline_str and not any(python_indicator in cmdline_str.lower()
89
+ (full_command in cmdline_str and not any(python_indicator in cmdline_str.lower()
132
90
  for python_indicator in ['python -c', 'import psutil', 'def check_process'])):
133
91
  matching_processes.append({{
134
92
  "pid": proc.info['pid'],
@@ -140,41 +98,35 @@ def check_process():
140
98
  }})
141
99
  except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
142
100
  continue
143
-
101
+
144
102
  return matching_processes
145
103
 
146
104
  if __name__ == "__main__":
147
105
  processes = check_process()
148
106
  print(json.dumps(processes))
149
107
  """
150
-
108
+
151
109
  def force_fresh_process_check(self, tab_name: str, tab_config: Dict[str, Tuple[str, str]]) -> Dict[str, Any]:
152
110
  """Force a fresh process check with additional validation."""
153
111
  if tab_name not in tab_config:
154
- return {
155
- "status": "unknown",
156
- "error": f"Tab '{tab_name}' not found in tracked configuration",
157
- "running": False,
158
- "command": None,
159
- "remote": self.remote_executor.remote_name
160
- }
161
-
112
+ return {"status": "unknown", "error": f"Tab '{tab_name}' not found in tracked configuration", "running": False, "command": None, "remote": self.remote_executor.remote_name}
113
+
162
114
  _, command = tab_config[tab_name]
163
-
115
+
164
116
  try:
165
117
  # Get timestamp for freshness validation
166
118
  timestamp_result = self.remote_executor.run_command("date +%s", timeout=5)
167
119
  check_timestamp = timestamp_result.stdout.strip() if timestamp_result.returncode == 0 else "unknown"
168
-
120
+
169
121
  check_script = self._create_fresh_check_script(command)
170
- remote_cmd = f"$HOME/venvs/ve/bin/python -c {shlex.quote(check_script)}"
122
+ remote_cmd = f"$HOME/code/machineconfig/.venv/bin/python -c {shlex.quote(check_script)}"
171
123
  result = self.remote_executor.run_command(remote_cmd, timeout=15)
172
-
124
+
173
125
  if result.returncode == 0:
174
126
  try:
175
127
  check_result = json.loads(result.stdout.strip())
176
128
  matching_processes = check_result.get("processes", [])
177
-
129
+
178
130
  return {
179
131
  "status": "running" if matching_processes else "not_running",
180
132
  "running": bool(matching_processes),
@@ -183,45 +135,23 @@ if __name__ == "__main__":
183
135
  "tab_name": tab_name,
184
136
  "remote": self.remote_executor.remote_name,
185
137
  "check_timestamp": check_timestamp,
186
- "method": "force_fresh_check"
138
+ "method": "force_fresh_check",
187
139
  }
188
140
  except json.JSONDecodeError as e:
189
141
  logger.error(f"Failed to parse fresh check output: {e}")
190
- return {
191
- "status": "error",
192
- "error": f"Failed to parse output: {e}",
193
- "running": False,
194
- "command": command,
195
- "tab_name": tab_name,
196
- "remote": self.remote_executor.remote_name,
197
- "raw_output": result.stdout
198
- }
142
+ return {"status": "error", "error": f"Failed to parse output: {e}", "running": False, "command": command, "tab_name": tab_name, "remote": self.remote_executor.remote_name, "raw_output": result.stdout}
199
143
  else:
200
- return {
201
- "status": "error",
202
- "error": f"Remote command failed: {result.stderr}",
203
- "running": False,
204
- "command": command,
205
- "tab_name": tab_name,
206
- "remote": self.remote_executor.remote_name
207
- }
208
-
144
+ return {"status": "error", "error": f"Remote command failed: {result.stderr}", "running": False, "command": command, "tab_name": tab_name, "remote": self.remote_executor.remote_name}
145
+
209
146
  except Exception as e:
210
147
  logger.error(f"Error in fresh process check for tab '{tab_name}': {e}")
211
- return {
212
- "status": "error",
213
- "error": str(e),
214
- "running": False,
215
- "command": command,
216
- "tab_name": tab_name,
217
- "remote": self.remote_executor.remote_name
218
- }
219
-
148
+ return {"status": "error", "error": str(e), "running": False, "command": command, "tab_name": tab_name, "remote": self.remote_executor.remote_name}
149
+
220
150
  def _create_fresh_check_script(self, command: str) -> str:
221
151
  """Create enhanced process checking script with freshness validation."""
222
152
  escaped_command = command.replace("'", "\\'").replace('"', '\\"')
223
-
224
- return f'''
153
+
154
+ return f"""
225
155
  import psutil
226
156
  import json
227
157
  import os
@@ -229,40 +159,40 @@ import time
229
159
 
230
160
  def force_fresh_check():
231
161
  time.sleep(0.1)
232
-
162
+
233
163
  matching_processes = []
234
164
  full_command = '{escaped_command}'
235
165
  cmd_parts = [part for part in full_command.split() if len(part) > 2]
236
166
  current_pid = os.getpid()
237
167
  primary_cmd = cmd_parts[0] if cmd_parts else ''
238
-
168
+
239
169
  check_time = time.time()
240
-
170
+
241
171
  for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'status', 'create_time']):
242
172
  try:
243
173
  if proc.info['pid'] == current_pid:
244
174
  continue
245
-
175
+
246
176
  if proc.info['cmdline'] and len(proc.info['cmdline']) > 0:
247
177
  cmdline_str = ' '.join(proc.info['cmdline'])
248
-
178
+
249
179
  if any(indicator in cmdline_str for indicator in [
250
180
  'check_process()', 'psutil.process_iter', 'force_fresh_check',
251
181
  'import psutil', 'def check_process'
252
182
  ]):
253
183
  continue
254
-
184
+
255
185
  if proc.info['create_time'] and proc.info['create_time'] > check_time - 5:
256
186
  continue
257
-
187
+
258
188
  matches_primary = primary_cmd in cmdline_str and primary_cmd != 'python'
259
189
  matches_parts = sum(1 for part in cmd_parts[1:] if part in cmdline_str)
260
-
190
+
261
191
  if matches_primary and matches_parts >= 2:
262
192
  script_indicators = ['-c', 'import ', 'def ', 'psutil']
263
- is_direct_command = not any(script_indicator in cmdline_str.lower()
193
+ is_direct_command = not any(script_indicator in cmdline_str.lower()
264
194
  for script_indicator in script_indicators)
265
-
195
+
266
196
  if is_direct_command or (full_command in cmdline_str and 'python -c' not in cmdline_str):
267
197
  matching_processes.append({{
268
198
  "pid": proc.info['pid'],
@@ -275,7 +205,7 @@ def force_fresh_check():
275
205
  }})
276
206
  except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
277
207
  continue
278
-
208
+
279
209
  return {{
280
210
  "processes": matching_processes,
281
211
  "check_timestamp": check_time,
@@ -286,24 +216,24 @@ def force_fresh_check():
286
216
  if __name__ == "__main__":
287
217
  result = force_fresh_check()
288
218
  print(json.dumps(result))
289
- '''
290
-
219
+ """
220
+
291
221
  def verify_process_alive(self, pid: int) -> bool:
292
222
  """Verify if a process with given PID is actually alive."""
293
223
  try:
294
224
  verify_cmd = f"kill -0 {pid} 2>/dev/null && echo 'alive' || echo 'dead'"
295
225
  result = self.remote_executor.run_command(verify_cmd, timeout=5)
296
-
226
+
297
227
  if result.returncode == 0:
298
- return result.stdout.strip() == 'alive'
228
+ return result.stdout.strip() == "alive"
299
229
  return False
300
230
  except Exception:
301
231
  return False
302
-
232
+
303
233
  def get_verified_process_status(self, tab_name: str, tab_config: Dict[str, Tuple[str, str]]) -> Dict[str, Any]:
304
234
  """Get process status with additional verification that processes are actually alive."""
305
235
  status = self.force_fresh_process_check(tab_name, tab_config)
306
-
236
+
307
237
  if status.get("running") and status.get("processes"):
308
238
  verified_processes = []
309
239
  for proc in status["processes"]:
@@ -314,20 +244,20 @@ if __name__ == "__main__":
314
244
  else:
315
245
  proc["verified_alive"] = False
316
246
  logger.warning(f"Process PID {pid} found in process list but not actually alive")
317
-
247
+
318
248
  status["processes"] = verified_processes
319
249
  status["running"] = bool(verified_processes)
320
250
  status["status"] = "running" if verified_processes else "not_running"
321
251
  status["verification_method"] = "kill_signal_check"
322
-
252
+
323
253
  return status
324
-
254
+
325
255
  def check_all_commands_status(self, tab_config: Dict[str, Tuple[str, str]]) -> Dict[str, Dict[str, Any]]:
326
256
  """Check status of all commands in the tab configuration."""
327
257
  if not tab_config:
328
258
  logger.warning("No tab configuration provided.")
329
259
  return {}
330
-
260
+
331
261
  status_report = {}
332
262
  for tab_name in tab_config:
333
263
  status_report[tab_name] = self.check_command_status(tab_name, tab_config)
@@ -2,6 +2,7 @@
2
2
  """
3
3
  Remote command execution utilities for SSH operations.
4
4
  """
5
+
5
6
  import subprocess
6
7
  import logging
7
8
  from typing import Dict, Any
@@ -11,20 +12,15 @@ logger = logging.getLogger(__name__)
11
12
 
12
13
  class RemoteExecutor:
13
14
  """Handles SSH command execution on remote machines."""
14
-
15
+
15
16
  def __init__(self, remote_name: str):
16
17
  self.remote_name = remote_name
17
-
18
+
18
19
  def run_command(self, command: str, timeout: int = 30) -> subprocess.CompletedProcess[str]:
19
20
  """Execute a command on the remote machine via SSH."""
20
21
  ssh_cmd = ["ssh", self.remote_name, command]
21
22
  try:
22
- result = subprocess.run(
23
- ssh_cmd,
24
- capture_output=True,
25
- text=True,
26
- timeout=timeout
27
- )
23
+ result = subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=timeout)
28
24
  return result
29
25
  except subprocess.TimeoutExpired:
30
26
  logger.error(f"SSH command timed out after {timeout}s: {command}")
@@ -32,7 +28,7 @@ class RemoteExecutor:
32
28
  except Exception as e:
33
29
  logger.error(f"SSH command failed: {e}")
34
30
  raise
35
-
31
+
36
32
  def copy_file_to_remote(self, local_file: str, remote_path: str) -> Dict[str, Any]:
37
33
  """Copy a file to the remote machine using SCP."""
38
34
  scp_cmd = ["scp", local_file, f"{self.remote_name}:{remote_path}"]
@@ -47,7 +43,7 @@ class RemoteExecutor:
47
43
  except Exception as e:
48
44
  logger.error(f"SCP operation failed: {e}")
49
45
  return {"success": False, "error": str(e)}
50
-
46
+
51
47
  def create_remote_directory(self, remote_dir: str) -> bool:
52
48
  """Create a directory on the remote machine."""
53
49
  try:
@@ -56,7 +52,7 @@ class RemoteExecutor:
56
52
  except Exception as e:
57
53
  logger.error(f"Failed to create remote directory {remote_dir}: {e}")
58
54
  return False
59
-
55
+
60
56
  def attach_to_session_interactive(self, session_name: str) -> None:
61
57
  """Attach to a Zellij session interactively via SSH."""
62
58
  try:
@@ -2,6 +2,7 @@
2
2
  """
3
3
  Zellij session management utilities for remote operations.
4
4
  """
5
+
5
6
  import logging
6
7
  from typing import Dict, Any, Optional
7
8
  from pathlib import Path
@@ -16,62 +17,46 @@ console = Console()
16
17
 
17
18
  class SessionManager:
18
19
  """Handles Zellij session operations on remote machines."""
19
-
20
+
20
21
  def __init__(self, remote_executor: RemoteExecutor, session_name: str, tmp_layout_dir: Path):
21
22
  self.remote_executor = remote_executor
22
23
  self.session_name = session_name
23
24
  self.tmp_layout_dir = tmp_layout_dir
24
-
25
+
25
26
  def copy_layout_to_remote(self, local_layout_file: Path, random_suffix: str) -> str:
26
27
  """Copy the layout file to the remote machine and return the remote path."""
27
28
  remote_layout_dir = f"~/{self.tmp_layout_dir.relative_to(Path.home())}"
28
29
  remote_layout_file = f"{remote_layout_dir}/zellij_layout_{self.session_name}_{random_suffix}.kdl"
29
-
30
+
30
31
  # Create remote directory
31
32
  if not self.remote_executor.create_remote_directory(remote_layout_dir):
32
33
  raise RuntimeError(f"Failed to create remote directory: {remote_layout_dir}")
33
-
34
+
34
35
  # Copy layout file to remote machine
35
36
  copy_result = self.remote_executor.copy_file_to_remote(str(local_layout_file), remote_layout_file)
36
37
  if not copy_result["success"]:
37
38
  raise RuntimeError(f"Failed to copy layout file to remote: {copy_result['error']}")
38
-
39
+
39
40
  # Enhanced Rich logging
40
41
  console.print(f"[bold green]📁 Zellij layout file copied to remote:[/bold green] [yellow]{self.remote_executor.remote_name}[/yellow][cyan]:{remote_layout_file}[/cyan]")
41
42
  return remote_layout_file
42
-
43
+
43
44
  def check_zellij_session_status(self) -> Dict[str, Any]:
44
45
  """Check if the Zellij session exists and is running."""
45
46
  try:
46
- result = self.remote_executor.run_command('zellij list-sessions', timeout=10)
47
-
47
+ result = self.remote_executor.run_command("zellij list-sessions", timeout=10)
48
+
48
49
  if result.returncode == 0:
49
- sessions = result.stdout.strip().split('\n') if result.stdout.strip() else []
50
+ sessions = result.stdout.strip().split("\n") if result.stdout.strip() else []
50
51
  session_running = any(self.session_name in session for session in sessions)
51
-
52
- return {
53
- "zellij_running": True,
54
- "session_exists": session_running,
55
- "session_name": self.session_name,
56
- "all_sessions": sessions,
57
- "remote": self.remote_executor.remote_name
58
- }
52
+
53
+ return {"zellij_running": True, "session_exists": session_running, "session_name": self.session_name, "all_sessions": sessions, "remote": self.remote_executor.remote_name}
59
54
  else:
60
- return {
61
- "zellij_running": False,
62
- "error": result.stderr,
63
- "session_name": self.session_name,
64
- "remote": self.remote_executor.remote_name
65
- }
66
-
55
+ return {"zellij_running": False, "error": result.stderr, "session_name": self.session_name, "remote": self.remote_executor.remote_name}
56
+
67
57
  except Exception as e:
68
- return {
69
- "zellij_running": False,
70
- "error": str(e),
71
- "session_name": self.session_name,
72
- "remote": self.remote_executor.remote_name
73
- }
74
-
58
+ return {"zellij_running": False, "error": str(e), "session_name": self.session_name, "remote": self.remote_executor.remote_name}
59
+
75
60
  def start_zellij_session(self, layout_file_path: Optional[str] = None) -> Dict[str, Any]:
76
61
  """Start a Zellij session on the remote machine with the generated layout."""
77
62
  try:
@@ -80,40 +65,27 @@ class SessionManager:
80
65
  remote_layout_file = f"~/{self.tmp_layout_dir.relative_to(Path.home())}/{layout_filename}"
81
66
  else:
82
67
  raise ValueError("No layout file path provided.")
83
-
68
+
84
69
  # Enhanced Rich logging for session start
85
- console.print(f"[bold cyan]🚀 Starting Zellij session[/bold cyan] [yellow]'{self.session_name}'[/yellow] [dim]on remote[/dim] [bold yellow]'{self.remote_executor.remote_name}'[/bold yellow] [dim]with layout:[/dim] [blue]{remote_layout_file}[/blue]")
86
-
70
+ console.print(
71
+ f"[bold cyan]🚀 Starting Zellij session[/bold cyan] [yellow]'{self.session_name}'[/yellow] [dim]on remote[/dim] [bold yellow]'{self.remote_executor.remote_name}'[/bold yellow] [dim]with layout:[/dim] [blue]{remote_layout_file}[/blue]"
72
+ )
73
+
87
74
  # Start Zellij session with layout
88
75
  start_cmd = f"zellij --layout {remote_layout_file} a -b {self.session_name}"
89
76
  console.print(f"[dim]Executing:[/dim] [green]{start_cmd}[/green]")
90
77
  result = self.remote_executor.run_command(start_cmd, timeout=30)
91
-
78
+
92
79
  if result.returncode == 0:
93
80
  console.print(f"[bold green]✅ Zellij session[/bold green] [yellow]'{self.session_name}'[/yellow] [green]started successfully on[/green] [bold yellow]{self.remote_executor.remote_name}[/bold yellow]")
94
- return {
95
- "success": True,
96
- "session_name": self.session_name,
97
- "remote": self.remote_executor.remote_name,
98
- "message": "Session started successfully"
99
- }
81
+ return {"success": True, "session_name": self.session_name, "remote": self.remote_executor.remote_name, "message": "Session started successfully"}
100
82
  else:
101
- return {
102
- "success": False,
103
- "error": result.stderr,
104
- "session_name": self.session_name,
105
- "remote": self.remote_executor.remote_name
106
- }
107
-
83
+ return {"success": False, "error": result.stderr, "session_name": self.session_name, "remote": self.remote_executor.remote_name}
84
+
108
85
  except Exception as e:
109
86
  logger.error(f"Failed to start Zellij session on {self.remote_executor.remote_name}: {e}")
110
- return {
111
- "success": False,
112
- "error": str(e),
113
- "session_name": self.session_name,
114
- "remote": self.remote_executor.remote_name
115
- }
116
-
87
+ return {"success": False, "error": str(e), "session_name": self.session_name, "remote": self.remote_executor.remote_name}
88
+
117
89
  def attach_to_session(self) -> None:
118
90
  """Attach to the Zellij session on the remote machine via SSH."""
119
91
  self.remote_executor.attach_to_session_interactive(self.session_name)
@@ -2,6 +2,7 @@
2
2
  """
3
3
  Status reporting utilities for Zellij remote layouts.
4
4
  """
5
+
5
6
  import logging
6
7
  from typing import Dict, Any, Tuple
7
8
  from .process_monitor import ProcessMonitor
@@ -12,19 +13,19 @@ logger = logging.getLogger(__name__)
12
13
 
13
14
  class StatusReporter:
14
15
  """Handles comprehensive status reporting for Zellij remote sessions."""
15
-
16
+
16
17
  def __init__(self, process_monitor: ProcessMonitor, session_manager: SessionManager):
17
18
  self.process_monitor = process_monitor
18
19
  self.session_manager = session_manager
19
-
20
+
20
21
  def get_comprehensive_status(self, tab_config: Dict[str, Tuple[str, str]]) -> Dict[str, Any]:
21
22
  """Get comprehensive status including Zellij session and all commands."""
22
23
  zellij_status = self.session_manager.check_zellij_session_status()
23
24
  commands_status = self.process_monitor.check_all_commands_status(tab_config)
24
-
25
+
25
26
  running_count = sum(1 for status in commands_status.values() if status.get("running", False))
26
27
  total_count = len(commands_status)
27
-
28
+
28
29
  return {
29
30
  "zellij_session": zellij_status,
30
31
  "commands": commands_status,
@@ -33,20 +34,20 @@ class StatusReporter:
33
34
  "running_commands": running_count,
34
35
  "stopped_commands": total_count - running_count,
35
36
  "session_healthy": zellij_status.get("session_exists", False),
36
- "remote": self.session_manager.remote_executor.remote_name
37
- }
37
+ "remote": self.session_manager.remote_executor.remote_name,
38
+ },
38
39
  }
39
-
40
+
40
41
  def print_status_report(self, tab_config: Dict[str, Tuple[str, str]]) -> None:
41
42
  """Print a formatted status report to console."""
42
43
  status = self.get_comprehensive_status(tab_config)
43
44
  remote_name = self.session_manager.remote_executor.remote_name
44
45
  session_name = self.session_manager.session_name
45
-
46
+
46
47
  print("=" * 60)
47
48
  print(f"🔍 ZELLIJ REMOTE LAYOUT STATUS REPORT ({remote_name})")
48
49
  print("=" * 60)
49
-
50
+
50
51
  # Zellij session status
51
52
  zellij = status["zellij_session"]
52
53
  if zellij.get("zellij_running", False):
@@ -56,13 +57,13 @@ class StatusReporter:
56
57
  print(f"⚠️ Zellij is running on {remote_name} but session '{session_name}' not found")
57
58
  else:
58
59
  print(f"❌ Zellij session issue on {remote_name}: {zellij.get('error', 'Unknown error')}")
59
-
60
+
60
61
  print()
61
-
62
+
62
63
  # Commands status
63
64
  print("📋 COMMAND STATUS:")
64
65
  print("-" * 40)
65
-
66
+
66
67
  for tab_name, cmd_status in status["commands"].items():
67
68
  if cmd_status.get("running", False):
68
69
  print(f"✅ {tab_name}: Running on {remote_name}")
@@ -73,7 +74,7 @@ class StatusReporter:
73
74
  print(f"❌ {tab_name}: Not running on {remote_name}")
74
75
  print(f" Command: {cmd_status.get('command', 'Unknown')}")
75
76
  print()
76
-
77
+
77
78
  # Summary
78
79
  summary = status["summary"]
79
80
  print("📊 SUMMARY:")
@@ -1,4 +1,3 @@
1
-
2
1
  # """Trogon
3
2
  # """
4
3