machineconfig 2.98__py3-none-any.whl → 3.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.
- machineconfig/__init__.py +16 -17
- machineconfig/cluster/sessions_managers/enhanced_command_runner.py +6 -6
- machineconfig/cluster/sessions_managers/wt_local.py +7 -7
- machineconfig/cluster/sessions_managers/wt_local_manager.py +8 -8
- machineconfig/cluster/sessions_managers/wt_remote.py +8 -8
- machineconfig/cluster/sessions_managers/wt_remote_manager.py +6 -6
- machineconfig/cluster/sessions_managers/zellij_local.py +156 -77
- machineconfig/cluster/sessions_managers/zellij_local_manager.py +18 -15
- machineconfig/cluster/sessions_managers/zellij_remote.py +14 -61
- machineconfig/cluster/sessions_managers/zellij_remote_manager.py +15 -12
- machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +9 -3
- machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +2 -2
- machineconfig/cluster/sessions_managers/zellij_utils/monitoring_types.py +40 -67
- machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +19 -17
- machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +2 -2
- machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +1 -1
- machineconfig/jobs/python/python_ve_symlink.py +1 -1
- machineconfig/profile/create.py +7 -0
- machineconfig/scripts/linux/kill_process +1 -1
- machineconfig/scripts/python/ai/mcinit.py +2 -3
- machineconfig/scripts/python/cloud_mount.py +1 -1
- machineconfig/scripts/python/cloud_repo_sync.py +1 -0
- machineconfig/scripts/python/cloud_sync.py +1 -0
- machineconfig/scripts/python/croshell.py +1 -0
- machineconfig/scripts/python/devops.py +11 -0
- machineconfig/scripts/python/devops_add_identity.py +1 -0
- machineconfig/scripts/python/devops_add_ssh_key.py +1 -0
- machineconfig/scripts/python/devops_backup_retrieve.py +1 -0
- machineconfig/scripts/python/devops_devapps_install.py +4 -6
- machineconfig/scripts/python/devops_update_repos.py +21 -20
- machineconfig/scripts/python/fire_agents.py +17 -7
- machineconfig/scripts/python/fire_agents_help_launch.py +4 -3
- machineconfig/scripts/python/fire_agents_help_search.py +1 -2
- machineconfig/scripts/python/fire_agents_load_balancer.py +8 -10
- machineconfig/scripts/python/fire_jobs.py +1 -0
- machineconfig/scripts/python/helpers/cloud_helpers.py +1 -0
- machineconfig/scripts/python/mount_nfs.py +2 -0
- machineconfig/scripts/python/mount_nw_drive.py +1 -1
- machineconfig/scripts/python/mount_ssh.py +1 -0
- machineconfig/scripts/python/repos.py +0 -2
- machineconfig/scripts/python/repos_helper_record.py +43 -66
- machineconfig/scripts/python/repos_helper_update.py +3 -2
- machineconfig/scripts/python/start_slidev.py +1 -0
- machineconfig/scripts/python/start_terminals.py +1 -1
- machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +1 -0
- machineconfig/utils/code.py +0 -1
- machineconfig/utils/notifications.py +2 -2
- machineconfig/utils/procs.py +10 -15
- machineconfig/utils/schemas/fire_agents/fire_agents_input.py +9 -4
- machineconfig/utils/schemas/layouts/layout_types.py +2 -0
- machineconfig/utils/schemas/repos/repos_types.py +0 -3
- machineconfig/utils/ssh.py +9 -11
- machineconfig/utils/utils2.py +1 -0
- machineconfig/utils/ve.py +0 -1
- {machineconfig-2.98.dist-info → machineconfig-3.1.dist-info}/METADATA +1 -1
- {machineconfig-2.98.dist-info → machineconfig-3.1.dist-info}/RECORD +59 -78
- {machineconfig-2.98.dist-info → machineconfig-3.1.dist-info}/entry_points.txt +1 -0
- machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_repo_sync.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_agents.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos_helper_record.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos_helper_update.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-313.pyc +0 -0
- machineconfig/settings/linters/.ruff_cache/.gitignore +0 -2
- machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +0 -1
- machineconfig/settings/shells/bash/.inputrc +0 -3
- {machineconfig-2.98.dist-info → machineconfig-3.1.dist-info}/WHEEL +0 -0
- {machineconfig-2.98.dist-info → machineconfig-3.1.dist-info}/top_level.txt +0 -0
machineconfig/__init__.py
CHANGED
|
@@ -6,25 +6,24 @@ import tomllib
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def _get_version() -> str:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
name: str = "machineconfig"
|
|
10
|
+
try:
|
|
11
|
+
return _pkg_version(name)
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
pass
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
root: Path = Path(__file__).resolve().parents[2]
|
|
16
|
+
pyproject: Path = root / "pyproject.toml"
|
|
17
|
+
if pyproject.is_file():
|
|
18
|
+
with pyproject.open("rb") as f:
|
|
19
|
+
data: dict[str, object] = tomllib.load(f)
|
|
20
|
+
project = data.get("project")
|
|
21
|
+
if isinstance(project, dict):
|
|
22
|
+
version = project.get("version")
|
|
23
|
+
if isinstance(version, str) and version:
|
|
24
|
+
return version
|
|
25
|
+
return "0.0.0"
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
__version__: str = _get_version()
|
|
29
29
|
__all__: list[str] = ["__version__"]
|
|
30
|
-
|
|
@@ -14,7 +14,7 @@ from rich import box
|
|
|
14
14
|
console = Console()
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def run_enhanced_command(command: str, description: Optional[str]
|
|
17
|
+
def run_enhanced_command(command: str, description: Optional[str], show_progress: bool, timeout: Optional[int]) -> Dict[str, Any]:
|
|
18
18
|
"""
|
|
19
19
|
Run a command with enhanced Rich formatting and user feedback.
|
|
20
20
|
|
|
@@ -89,16 +89,16 @@ def enhanced_zellij_session_start(session_name: str, layout_path: str) -> Dict[s
|
|
|
89
89
|
run_enhanced_command(
|
|
90
90
|
delete_cmd,
|
|
91
91
|
f"Cleaning up existing session '{session_name}'",
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
False,
|
|
93
|
+
5, # Quick timeout for cleanup
|
|
94
94
|
)
|
|
95
95
|
# Start new session (use -b for background to avoid hanging)
|
|
96
96
|
start_cmd = f"zellij --layout {layout_path} a -b {session_name}"
|
|
97
97
|
start_result = run_enhanced_command(
|
|
98
98
|
start_cmd,
|
|
99
99
|
f"Starting session '{session_name}' with layout",
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
False,
|
|
101
|
+
10, # Add timeout to prevent hanging
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
if start_result["success"]:
|
|
@@ -114,5 +114,5 @@ if __name__ == "__main__":
|
|
|
114
114
|
console.print(Panel.fit("🎨 Enhanced Command Execution Demo", style="bold cyan"))
|
|
115
115
|
|
|
116
116
|
# Test with a simple command
|
|
117
|
-
result = run_enhanced_command("echo 'Hello, Rich world!'", "Testing enhanced output")
|
|
117
|
+
result = run_enhanced_command("echo 'Hello, Rich world!'", "Testing enhanced output", True, None)
|
|
118
118
|
console.print(f"Result: {result}")
|
|
@@ -27,7 +27,7 @@ class WTLayoutGenerator:
|
|
|
27
27
|
self.script_path: Optional[str] = None # Store the full path to the PowerShell script
|
|
28
28
|
|
|
29
29
|
@staticmethod
|
|
30
|
-
def _generate_random_suffix(length: int
|
|
30
|
+
def _generate_random_suffix(length: int) -> str:
|
|
31
31
|
"""Generate a random string suffix for unique PowerShell script names."""
|
|
32
32
|
return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
|
33
33
|
|
|
@@ -53,7 +53,7 @@ class WTLayoutGenerator:
|
|
|
53
53
|
return text
|
|
54
54
|
|
|
55
55
|
@staticmethod
|
|
56
|
-
def _create_tab_command(tab_config: TabConfig, is_first_tab: bool
|
|
56
|
+
def _create_tab_command(tab_config: TabConfig, is_first_tab: bool) -> str:
|
|
57
57
|
"""Create a Windows Terminal tab command string from tab config."""
|
|
58
58
|
tab_name = tab_config["tabName"]
|
|
59
59
|
cwd = tab_config["startDir"]
|
|
@@ -95,7 +95,7 @@ class WTLayoutGenerator:
|
|
|
95
95
|
if not tab["startDir"].strip():
|
|
96
96
|
raise ValueError(f"Invalid startDir for tab '{tab['tabName']}': {tab['startDir']}")
|
|
97
97
|
|
|
98
|
-
def create_wt_layout(self, layout_config: LayoutConfig, output_dir: Optional[str]
|
|
98
|
+
def create_wt_layout(self, layout_config: LayoutConfig, output_dir: Optional[str]) -> str:
|
|
99
99
|
WTLayoutGenerator._validate_layout_config(layout_config)
|
|
100
100
|
logger.info(f"Creating Windows Terminal layout '{layout_config['layoutName']}' with {len(layout_config['layoutTabs'])} tabs")
|
|
101
101
|
|
|
@@ -107,7 +107,7 @@ class WTLayoutGenerator:
|
|
|
107
107
|
wt_command = self._generate_wt_command_string(layout_config, self.session_name)
|
|
108
108
|
|
|
109
109
|
try:
|
|
110
|
-
random_suffix = WTLayoutGenerator._generate_random_suffix()
|
|
110
|
+
random_suffix = WTLayoutGenerator._generate_random_suffix(8)
|
|
111
111
|
if output_dir:
|
|
112
112
|
output_path = Path(output_dir)
|
|
113
113
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -367,7 +367,7 @@ Get-Process | ForEach-Object {{
|
|
|
367
367
|
print("=" * 80)
|
|
368
368
|
|
|
369
369
|
|
|
370
|
-
def create_wt_layout(layout_config: LayoutConfig, output_dir: Optional[str]
|
|
370
|
+
def create_wt_layout(layout_config: LayoutConfig, output_dir: Optional[str]) -> str:
|
|
371
371
|
generator = WTLayoutGenerator()
|
|
372
372
|
return generator.create_wt_layout(layout_config, output_dir)
|
|
373
373
|
|
|
@@ -375,7 +375,7 @@ def create_wt_layout(layout_config: LayoutConfig, output_dir: Optional[str] = No
|
|
|
375
375
|
def run_wt_layout(layout_config: LayoutConfig) -> str:
|
|
376
376
|
"""Create and run a Windows Terminal layout."""
|
|
377
377
|
generator = WTLayoutGenerator()
|
|
378
|
-
script_path = generator.create_wt_layout(layout_config)
|
|
378
|
+
script_path = generator.create_wt_layout(layout_config, None)
|
|
379
379
|
|
|
380
380
|
# Execute the script
|
|
381
381
|
cmd = f'powershell -ExecutionPolicy Bypass -File "{script_path}"'
|
|
@@ -406,7 +406,7 @@ if __name__ == "__main__":
|
|
|
406
406
|
try:
|
|
407
407
|
# Create layout using the generator
|
|
408
408
|
generator = WTLayoutGenerator()
|
|
409
|
-
script_path = generator.create_wt_layout(sample_layout)
|
|
409
|
+
script_path = generator.create_wt_layout(sample_layout, None)
|
|
410
410
|
print(f"✅ Windows Terminal layout created: {script_path}")
|
|
411
411
|
|
|
412
412
|
# Show preview
|
|
@@ -35,7 +35,7 @@ class WTLocalManager:
|
|
|
35
35
|
# Create a WTLayoutGenerator for each session
|
|
36
36
|
for layout_config in session_layouts:
|
|
37
37
|
manager = WTLayoutGenerator()
|
|
38
|
-
manager.create_wt_layout(layout_config=layout_config)
|
|
38
|
+
manager.create_wt_layout(layout_config=layout_config, output_dir=None)
|
|
39
39
|
self.managers.append(manager)
|
|
40
40
|
|
|
41
41
|
logger.info(f"Initialized WTLocalManager with {len(self.managers)} sessions")
|
|
@@ -324,16 +324,16 @@ class WTLocalManager:
|
|
|
324
324
|
if not config_file.exists():
|
|
325
325
|
raise FileNotFoundError(f"Configuration file not found: {config_file}")
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
text = config_file.read_text(encoding="utf-8")
|
|
328
|
+
session_layouts = json.loads(text)
|
|
329
329
|
|
|
330
330
|
# Load metadata
|
|
331
331
|
metadata_file = session_dir / "metadata.json"
|
|
332
332
|
session_name_prefix = "LocalWTMgr" # default fallback
|
|
333
333
|
if metadata_file.exists():
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
334
|
+
text = metadata_file.read_text(encoding="utf-8")
|
|
335
|
+
metadata = json.loads(text)
|
|
336
|
+
session_name_prefix = metadata.get("session_name_prefix", "LocalWTMgr")
|
|
337
337
|
|
|
338
338
|
# Create new instance
|
|
339
339
|
instance = cls(session_layouts=session_layouts, session_name_prefix=session_name_prefix)
|
|
@@ -346,8 +346,8 @@ class WTLocalManager:
|
|
|
346
346
|
|
|
347
347
|
for manager_file in manager_files:
|
|
348
348
|
try:
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
text = manager_file.read_text(encoding="utf-8")
|
|
350
|
+
manager_data = json.loads(text)
|
|
351
351
|
|
|
352
352
|
# Recreate the manager
|
|
353
353
|
manager = WTLayoutGenerator()
|
|
@@ -34,7 +34,7 @@ class WTRemoteLayoutGenerator:
|
|
|
34
34
|
|
|
35
35
|
# Tabs are stored and used as List[TabConfig]; no legacy dict compatibility
|
|
36
36
|
|
|
37
|
-
def create_wt_layout(self, tabs: List[TabConfig], output_dir: Optional[str]
|
|
37
|
+
def create_wt_layout(self, tabs: List[TabConfig], output_dir: Optional[str]) -> str:
|
|
38
38
|
logger.info(f"Creating Windows Terminal layout with {len(tabs)} tabs for remote '{self.remote_name}'")
|
|
39
39
|
self.tabs = tabs
|
|
40
40
|
if output_dir:
|
|
@@ -49,7 +49,7 @@ class WTRemoteLayoutGenerator:
|
|
|
49
49
|
def to_dict(self) -> Dict[str, Any]:
|
|
50
50
|
return {"remote_name": self.remote_name, "session_name": self.session_name, "tabs": self.tabs, "script_path": self.script_path, "created_at": datetime.now().isoformat(), "class_name": self.__class__.__name__}
|
|
51
51
|
|
|
52
|
-
def to_json(self, file_path: Optional[str]
|
|
52
|
+
def to_json(self, file_path: Optional[str]) -> str:
|
|
53
53
|
# Generate file path if not provided
|
|
54
54
|
if file_path is None:
|
|
55
55
|
random_id = str(uuid.uuid4())[:8]
|
|
@@ -87,8 +87,8 @@ class WTRemoteLayoutGenerator:
|
|
|
87
87
|
raise FileNotFoundError(f"JSON file not found: {path_obj}")
|
|
88
88
|
|
|
89
89
|
# Load JSON data
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
text = path_obj.read_text(encoding="utf-8")
|
|
91
|
+
data = json.loads(text)
|
|
92
92
|
|
|
93
93
|
# Validate that it's the correct class
|
|
94
94
|
if data.get("class_name") != cls.__name__:
|
|
@@ -117,7 +117,7 @@ class WTRemoteLayoutGenerator:
|
|
|
117
117
|
return instance
|
|
118
118
|
|
|
119
119
|
@staticmethod
|
|
120
|
-
def list_saved_sessions(directory_path: Optional[str]
|
|
120
|
+
def list_saved_sessions(directory_path: Optional[str]) -> List[str]:
|
|
121
121
|
if directory_path is None:
|
|
122
122
|
dir_path = Path.home() / "tmp_results" / "wt_sessions" / "serialized"
|
|
123
123
|
else:
|
|
@@ -146,7 +146,7 @@ if __name__ == "__main__":
|
|
|
146
146
|
try:
|
|
147
147
|
# Create layout using the remote generator
|
|
148
148
|
generator = WTRemoteLayoutGenerator(remote_name=remote_name, session_name_prefix=session_name)
|
|
149
|
-
script_path = generator.create_wt_layout(sample_tabs)
|
|
149
|
+
script_path = generator.create_wt_layout(sample_tabs, None)
|
|
150
150
|
print(f"✅ Remote layout created successfully: {script_path}")
|
|
151
151
|
|
|
152
152
|
# Check if Windows Terminal is available on remote
|
|
@@ -160,11 +160,11 @@ if __name__ == "__main__":
|
|
|
160
160
|
|
|
161
161
|
# Demonstrate serialization
|
|
162
162
|
print("\n💾 Demonstrating serialization...")
|
|
163
|
-
saved_path = generator.to_json()
|
|
163
|
+
saved_path = generator.to_json(None)
|
|
164
164
|
print(f"✅ Session saved to: {saved_path}")
|
|
165
165
|
|
|
166
166
|
# List all saved sessions
|
|
167
|
-
saved_sessions = WTRemoteLayoutGenerator.list_saved_sessions()
|
|
167
|
+
saved_sessions = WTRemoteLayoutGenerator.list_saved_sessions(None)
|
|
168
168
|
print(f"📋 Available saved sessions: {saved_sessions}")
|
|
169
169
|
|
|
170
170
|
# Demonstrate loading (using the full path)
|
|
@@ -24,7 +24,7 @@ class WTSessionManager:
|
|
|
24
24
|
an_m = WTRemoteLayoutGenerator(remote_name=machine, session_name_prefix=self.session_name_prefix)
|
|
25
25
|
# Convert legacy dict[str, tuple[str,str]] to List[TabConfig]
|
|
26
26
|
tabs: list[TabConfig] = [{"tabName": name, "startDir": cwd, "command": cmd} for name, (cwd, cmd) in tab_config.items()]
|
|
27
|
-
an_m.create_wt_layout(tabs=tabs)
|
|
27
|
+
an_m.create_wt_layout(tabs=tabs, output_dir=None)
|
|
28
28
|
self.managers.append(an_m)
|
|
29
29
|
|
|
30
30
|
def ssh_to_all_machines(self) -> str:
|
|
@@ -124,16 +124,16 @@ class WTSessionManager:
|
|
|
124
124
|
config_file = session_dir / "machine2wt_tabs.json"
|
|
125
125
|
if not config_file.exists():
|
|
126
126
|
raise FileNotFoundError(f"Configuration file not found: {config_file}")
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
text = config_file.read_text(encoding="utf-8")
|
|
128
|
+
machine2wt_tabs = json.loads(text)
|
|
129
129
|
|
|
130
130
|
# Load metadata
|
|
131
131
|
metadata_file = session_dir / "metadata.json"
|
|
132
132
|
session_name_prefix = "WTJobMgr" # default fallback
|
|
133
133
|
if metadata_file.exists():
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
text = metadata_file.read_text(encoding="utf-8")
|
|
135
|
+
metadata = json.loads(text)
|
|
136
|
+
session_name_prefix = metadata.get("session_name_prefix", "WTJobMgr")
|
|
137
137
|
# Create new instance (this will create new managers)
|
|
138
138
|
instance = cls(machine2wt_tabs=machine2wt_tabs, session_name_prefix=session_name_prefix)
|
|
139
139
|
# Load saved managers to restore their states
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import shlex
|
|
3
3
|
import subprocess
|
|
4
|
-
from machineconfig.cluster.sessions_managers.zellij_utils.monitoring_types import
|
|
4
|
+
from machineconfig.cluster.sessions_managers.zellij_utils.monitoring_types import CommandStatus, ZellijSessionStatus, ComprehensiveStatus, ProcessInfo
|
|
5
5
|
import psutil
|
|
6
6
|
import random
|
|
7
7
|
import string
|
|
@@ -14,7 +14,6 @@ from rich.console import Console
|
|
|
14
14
|
from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig, TabConfig
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
logging.basicConfig(level=logging.INFO)
|
|
19
18
|
logger = logging.getLogger(__name__)
|
|
20
19
|
console = Console()
|
|
@@ -37,7 +36,7 @@ class ZellijLayoutGenerator:
|
|
|
37
36
|
"""
|
|
38
37
|
|
|
39
38
|
@staticmethod
|
|
40
|
-
def _generate_random_suffix(length: int
|
|
39
|
+
def _generate_random_suffix(length: int) -> str:
|
|
41
40
|
"""Generate a random string suffix for unique layout file names."""
|
|
42
41
|
return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
|
43
42
|
|
|
@@ -71,7 +70,6 @@ class ZellijLayoutGenerator:
|
|
|
71
70
|
tab_name = tab_config["tabName"]
|
|
72
71
|
cwd = tab_config["startDir"]
|
|
73
72
|
command = tab_config["command"]
|
|
74
|
-
|
|
75
73
|
cmd, args = ZellijLayoutGenerator._parse_command(command)
|
|
76
74
|
args_str = ZellijLayoutGenerator._format_args_for_kdl(args)
|
|
77
75
|
tab_cwd = cwd or "~"
|
|
@@ -95,7 +93,7 @@ class ZellijLayoutGenerator:
|
|
|
95
93
|
if not tab["startDir"].strip():
|
|
96
94
|
raise ValueError(f"Invalid startDir for tab '{tab['tabName']}': {tab['startDir']}")
|
|
97
95
|
|
|
98
|
-
def create_zellij_layout(self, layout_config: LayoutConfig, output_dir: Optional[str]
|
|
96
|
+
def create_zellij_layout(self, layout_config: LayoutConfig, output_dir: Optional[str], session_name: Optional[str]) -> str:
|
|
99
97
|
ZellijLayoutGenerator._validate_layout_config(layout_config)
|
|
100
98
|
|
|
101
99
|
# Enhanced Rich logging
|
|
@@ -117,7 +115,7 @@ class ZellijLayoutGenerator:
|
|
|
117
115
|
layout_content += "\n}\n"
|
|
118
116
|
|
|
119
117
|
try:
|
|
120
|
-
random_suffix = ZellijLayoutGenerator._generate_random_suffix()
|
|
118
|
+
random_suffix = ZellijLayoutGenerator._generate_random_suffix(8)
|
|
121
119
|
if output_dir:
|
|
122
120
|
output_path = Path(output_dir)
|
|
123
121
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -139,7 +137,7 @@ class ZellijLayoutGenerator:
|
|
|
139
137
|
raise
|
|
140
138
|
|
|
141
139
|
@staticmethod
|
|
142
|
-
def get_layout_preview(layout_config: LayoutConfig, layout_template: str | None
|
|
140
|
+
def get_layout_preview(layout_config: LayoutConfig, layout_template: str | None) -> str:
|
|
143
141
|
if layout_template is None:
|
|
144
142
|
layout_template = """layout {
|
|
145
143
|
default_tab_template {
|
|
@@ -157,7 +155,7 @@ class ZellijLayoutGenerator:
|
|
|
157
155
|
return layout_content + "\n}\n"
|
|
158
156
|
|
|
159
157
|
@staticmethod
|
|
160
|
-
def check_command_status(tab_name: str, layout_config: LayoutConfig) ->
|
|
158
|
+
def check_command_status(tab_name: str, layout_config: LayoutConfig) -> CommandStatus:
|
|
161
159
|
# Find the tab with the given name
|
|
162
160
|
tab_config = None
|
|
163
161
|
for tab in layout_config["layoutTabs"]:
|
|
@@ -171,94 +169,176 @@ class ZellijLayoutGenerator:
|
|
|
171
169
|
command = tab_config["command"]
|
|
172
170
|
cwd = tab_config["startDir"]
|
|
173
171
|
cmd, args = ZellijLayoutGenerator._parse_command(command)
|
|
174
|
-
|
|
175
172
|
try:
|
|
176
|
-
|
|
173
|
+
shells = {"bash", "sh", "zsh", "fish"}
|
|
177
174
|
matching_processes: list[ProcessInfo] = []
|
|
178
|
-
for proc in psutil.process_iter(["pid", "name", "cmdline", "status", "ppid"]):
|
|
175
|
+
for proc in psutil.process_iter(["pid", "name", "cmdline", "status", "ppid", "create_time", "memory_info"]):
|
|
179
176
|
try:
|
|
180
|
-
|
|
177
|
+
info = proc.info
|
|
178
|
+
proc_cmdline: list[str] | None = info.get("cmdline") # type: ignore[assignment]
|
|
179
|
+
if not proc_cmdline:
|
|
181
180
|
continue
|
|
182
|
-
|
|
183
|
-
# Skip processes that are already dead/zombie/stopped
|
|
184
|
-
if proc.info["status"] in ["zombie", "dead", "stopped"]:
|
|
181
|
+
if info.get("status") in ["zombie", "dead", "stopped"]:
|
|
185
182
|
continue
|
|
186
|
-
|
|
187
|
-
# Get the actual command from cmdline
|
|
188
|
-
proc_cmdline = proc.info["cmdline"]
|
|
189
|
-
proc_name = proc.info["name"]
|
|
190
|
-
|
|
191
|
-
# More precise matching logic
|
|
183
|
+
proc_name = info.get("name", "")
|
|
192
184
|
is_match = False
|
|
193
|
-
|
|
194
|
-
#
|
|
195
|
-
if proc_name == cmd:
|
|
196
|
-
# For
|
|
197
|
-
if
|
|
185
|
+
joined_cmdline = " ".join(proc_cmdline)
|
|
186
|
+
# Primary matching heuristics - more precise matching
|
|
187
|
+
if proc_name == cmd and cmd not in shells:
|
|
188
|
+
# For non-shell commands, match if args appear in cmdline
|
|
189
|
+
if not args or any(arg in joined_cmdline for arg in args):
|
|
198
190
|
is_match = True
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
191
|
+
elif proc_name == cmd and cmd in shells:
|
|
192
|
+
# For shell commands, require more precise matching to avoid false positives
|
|
193
|
+
if args:
|
|
194
|
+
# Check if all args appear as separate cmdline arguments (not just substrings)
|
|
195
|
+
args_found = 0
|
|
196
|
+
for arg in args:
|
|
197
|
+
for cmdline_arg in proc_cmdline[1:]: # Skip shell name
|
|
198
|
+
if arg == cmdline_arg or (len(arg) > 3 and arg in cmdline_arg):
|
|
199
|
+
args_found += 1
|
|
200
|
+
break
|
|
201
|
+
# Require at least as many args found as we're looking for
|
|
202
|
+
if args_found >= len(args):
|
|
203
|
+
is_match = True
|
|
204
|
+
elif cmd in proc_cmdline[0] and cmd not in shells:
|
|
205
|
+
# Non-shell command in first argument
|
|
202
206
|
is_match = True
|
|
203
207
|
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
# Additional shell wrapper filter - be more restrictive for shells
|
|
209
|
+
if is_match and proc_name in shells and args:
|
|
210
|
+
# For shell processes, ensure the match is actually meaningful
|
|
211
|
+
# Don't match generic shell sessions just because they contain common paths
|
|
212
|
+
meaningful_match = False
|
|
213
|
+
for arg in args:
|
|
214
|
+
# Only consider it meaningful if the arg is substantial (not just a common path)
|
|
215
|
+
if len(arg) > 10 and any(arg == cmdline_arg for cmdline_arg in proc_cmdline[1:]):
|
|
216
|
+
meaningful_match = True
|
|
217
|
+
break
|
|
218
|
+
# Or if it's an exact script name match
|
|
219
|
+
elif arg.endswith('.py') or arg.endswith('.sh') or arg.endswith('.rb'):
|
|
220
|
+
if any(arg in cmdline_arg for cmdline_arg in proc_cmdline[1:]):
|
|
221
|
+
meaningful_match = True
|
|
222
|
+
break
|
|
223
|
+
if not meaningful_match:
|
|
213
224
|
is_match = False
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
225
|
+
if not is_match:
|
|
226
|
+
continue
|
|
227
|
+
try:
|
|
228
|
+
proc_obj = psutil.Process(info["pid"]) # type: ignore[index]
|
|
229
|
+
if proc_obj.status() not in ["running", "sleeping"]:
|
|
230
|
+
continue
|
|
231
|
+
mem_info = None
|
|
217
232
|
try:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if proc_obj.status() not in ["running", "sleeping"]:
|
|
221
|
-
continue # Skip inactive processes
|
|
222
|
-
|
|
223
|
-
matching_processes.append({
|
|
224
|
-
"pid": proc.info["pid"],
|
|
225
|
-
"name": proc.info["name"],
|
|
226
|
-
"cmdline": proc.info["cmdline"],
|
|
227
|
-
"status": proc.info["status"]
|
|
228
|
-
})
|
|
233
|
+
mem = proc_obj.memory_info()
|
|
234
|
+
mem_info = mem.rss / (1024 * 1024)
|
|
229
235
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
230
|
-
|
|
231
|
-
|
|
236
|
+
pass
|
|
237
|
+
matching_processes.append(
|
|
238
|
+
{
|
|
239
|
+
"pid": info["pid"], # type: ignore[index]
|
|
240
|
+
"name": proc_name,
|
|
241
|
+
"cmdline": proc_cmdline,
|
|
242
|
+
"status": info.get("status", "unknown"),
|
|
243
|
+
"cmdline_str": joined_cmdline,
|
|
244
|
+
"create_time": info.get("create_time", 0.0),
|
|
245
|
+
**({"memory_mb": float(mem_info)} if mem_info is not None else {}),
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
249
|
+
continue
|
|
232
250
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
233
251
|
continue
|
|
234
252
|
|
|
235
|
-
#
|
|
236
|
-
|
|
253
|
+
# Second-pass filtering: remove idle wrapper shells that have no meaningful (non-shell) descendants
|
|
254
|
+
filtered_active: list[ProcessInfo] = []
|
|
237
255
|
for proc_info in matching_processes:
|
|
238
256
|
try:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
257
|
+
proc_obj = psutil.Process(proc_info["pid"]) # type: ignore[index]
|
|
258
|
+
if not proc_obj.is_running():
|
|
259
|
+
continue
|
|
260
|
+
status_val = proc_obj.status()
|
|
261
|
+
if status_val not in ["running", "sleeping"]:
|
|
262
|
+
continue
|
|
263
|
+
proc_name = proc_info.get("name", "")
|
|
264
|
+
if proc_name in shells:
|
|
265
|
+
descendants = proc_obj.children(recursive=True)
|
|
266
|
+
# Keep shell only if there exists a non-shell alive descendant OR descendant cmdline still includes our command token
|
|
267
|
+
meaningful = False
|
|
268
|
+
for child in descendants:
|
|
269
|
+
try:
|
|
270
|
+
if not child.is_running():
|
|
271
|
+
continue
|
|
272
|
+
child_name = child.name()
|
|
273
|
+
child_cmdline = " ".join(child.cmdline())
|
|
274
|
+
if child_name not in shells:
|
|
275
|
+
meaningful = True
|
|
276
|
+
break
|
|
277
|
+
if cmd in child_cmdline or any(arg in child_cmdline for arg in args):
|
|
278
|
+
meaningful = True
|
|
279
|
+
break
|
|
280
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
281
|
+
continue
|
|
282
|
+
if not meaningful:
|
|
283
|
+
continue # discard idle wrapper shell
|
|
284
|
+
filtered_active.append(proc_info)
|
|
243
285
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
244
|
-
# Process is gone, don't count it
|
|
245
286
|
continue
|
|
246
287
|
|
|
247
|
-
if
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
288
|
+
if filtered_active:
|
|
289
|
+
# Heuristic: if the only remaining processes are wrapper shells invoking a script that already completed, mark as not running.
|
|
290
|
+
# Case: layout launches 'bash <script.sh>' where script finishes and leaves an idle shell whose cmdline still shows the script path.
|
|
291
|
+
try:
|
|
292
|
+
if all(p.get("name") in shells for p in filtered_active):
|
|
293
|
+
script_paths = [arg for arg in args if arg.endswith(".sh")]
|
|
294
|
+
shell_only = True
|
|
295
|
+
stale_script_overall = False
|
|
296
|
+
for p in filtered_active:
|
|
297
|
+
proc_shell = psutil.Process(p["pid"]) # type: ignore[index]
|
|
298
|
+
create_time = getattr(proc_shell, "create_time", lambda: None)()
|
|
299
|
+
cmdline_joined = " ".join(p.get("cmdline", []))
|
|
300
|
+
stale_script = False
|
|
301
|
+
for spath in script_paths:
|
|
302
|
+
script_file = Path(spath)
|
|
303
|
+
if script_file.exists():
|
|
304
|
+
try:
|
|
305
|
+
# If script mtime older than process start AND no non-shell descendants -> likely finished
|
|
306
|
+
if create_time and script_file.stat().st_mtime < create_time:
|
|
307
|
+
stale_script = True
|
|
308
|
+
except OSError:
|
|
309
|
+
pass
|
|
310
|
+
if spath not in cmdline_joined:
|
|
311
|
+
stale_script = False
|
|
312
|
+
# If shell has any alive non-shell descendants, treat as running
|
|
313
|
+
descendants = proc_shell.children(recursive=True)
|
|
314
|
+
for d in descendants:
|
|
315
|
+
try:
|
|
316
|
+
if d.is_running() and d.name() not in shells:
|
|
317
|
+
shell_only = False
|
|
318
|
+
break
|
|
319
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
320
|
+
continue
|
|
321
|
+
if not shell_only:
|
|
322
|
+
break
|
|
323
|
+
if stale_script:
|
|
324
|
+
stale_script_overall = True
|
|
325
|
+
if shell_only and stale_script_overall:
|
|
326
|
+
return {"status": "not_running", "running": False, "processes": [], "command": command, "cwd": cwd, "tab_name": tab_name}
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
return {"status": "running", "running": True, "processes": filtered_active, "command": command, "cwd": cwd, "tab_name": tab_name}
|
|
330
|
+
return {"status": "not_running", "running": False, "processes": [], "command": command, "cwd": cwd, "tab_name": tab_name}
|
|
251
331
|
|
|
252
332
|
except Exception as e:
|
|
253
333
|
logger.error(f"Error checking command status for tab '{tab_name}': {e}")
|
|
254
334
|
return {"status": "error", "error": str(e), "running": False, "command": command, "cwd": cwd, "tab_name": tab_name, "processes": []}
|
|
255
335
|
|
|
256
|
-
def check_all_commands_status(self) -> dict[str,
|
|
336
|
+
def check_all_commands_status(self) -> dict[str, CommandStatus]:
|
|
257
337
|
if not self.layout_config:
|
|
258
338
|
logger.warning("No layout config tracked. Make sure to create a layout first.")
|
|
259
339
|
return {}
|
|
260
340
|
|
|
261
|
-
status_report: dict[str,
|
|
341
|
+
status_report: dict[str, CommandStatus] = {}
|
|
262
342
|
for tab in self.layout_config["layoutTabs"]:
|
|
263
343
|
tab_name = tab["tabName"]
|
|
264
344
|
status_report[tab_name] = ZellijLayoutGenerator.check_command_status(tab_name, self.layout_config)
|
|
@@ -277,22 +357,20 @@ class ZellijLayoutGenerator:
|
|
|
277
357
|
|
|
278
358
|
return {"zellij_running": True, "session_exists": session_running, "session_name": session_name, "all_sessions": sessions}
|
|
279
359
|
else:
|
|
280
|
-
return {"zellij_running": False, "session_name": session_name, "all_sessions": [], "error": result.stderr}
|
|
360
|
+
return {"zellij_running": False, "session_exists": False, "session_name": session_name, "all_sessions": [], "error": result.stderr}
|
|
281
361
|
|
|
282
362
|
except subprocess.TimeoutExpired:
|
|
283
|
-
return {"zellij_running": False, "session_name": session_name, "all_sessions": [], "error": "Timeout while checking Zellij sessions"}
|
|
363
|
+
return {"zellij_running": False, "session_exists": False, "session_name": session_name, "all_sessions": [], "error": "Timeout while checking Zellij sessions"}
|
|
284
364
|
except FileNotFoundError:
|
|
285
|
-
return {"zellij_running": False, "session_name": session_name, "all_sessions": [], "error": "Zellij not found in PATH"}
|
|
365
|
+
return {"zellij_running": False, "session_exists": False, "session_name": session_name, "all_sessions": [], "error": "Zellij not found in PATH"}
|
|
286
366
|
except Exception as e:
|
|
287
|
-
return {"zellij_running": False, "session_name": session_name, "all_sessions": [], "error": str(e)}
|
|
367
|
+
return {"zellij_running": False, "session_exists": False, "session_name": session_name, "all_sessions": [], "error": str(e)}
|
|
288
368
|
|
|
289
369
|
def get_comprehensive_status(self) -> ComprehensiveStatus:
|
|
290
370
|
zellij_status = ZellijLayoutGenerator.check_zellij_session_status(self.session_name or "default")
|
|
291
371
|
commands_status = self.check_all_commands_status()
|
|
292
|
-
|
|
293
372
|
running_count = sum(1 for status in commands_status.values() if status.get("running", False))
|
|
294
373
|
total_count = len(commands_status)
|
|
295
|
-
|
|
296
374
|
return {
|
|
297
375
|
"zellij_session": zellij_status,
|
|
298
376
|
"commands": commands_status,
|
|
@@ -369,16 +447,17 @@ class ZellijLayoutGenerator:
|
|
|
369
447
|
console.print(Panel(summary_text, title="📊 Summary", style="blue"))
|
|
370
448
|
|
|
371
449
|
|
|
372
|
-
def created_zellij_layout(layout_config: LayoutConfig, output_dir: Optional[str]
|
|
450
|
+
def created_zellij_layout(layout_config: LayoutConfig, output_dir: Optional[str]) -> str:
|
|
373
451
|
generator = ZellijLayoutGenerator()
|
|
374
|
-
return generator.create_zellij_layout(layout_config, output_dir)
|
|
452
|
+
return generator.create_zellij_layout(layout_config, output_dir, None)
|
|
375
453
|
|
|
376
454
|
|
|
377
455
|
def run_zellij_layout(layout_config: LayoutConfig):
|
|
378
|
-
layout_path = created_zellij_layout(layout_config)
|
|
456
|
+
layout_path = created_zellij_layout(layout_config, None)
|
|
379
457
|
session_name = layout_config["layoutName"]
|
|
380
458
|
try:
|
|
381
459
|
from machineconfig.cluster.sessions_managers.enhanced_command_runner import enhanced_zellij_session_start
|
|
460
|
+
|
|
382
461
|
enhanced_zellij_session_start(session_name, layout_path)
|
|
383
462
|
except ImportError:
|
|
384
463
|
# Fallback to original implementation
|
|
@@ -425,7 +504,7 @@ if __name__ == "__main__":
|
|
|
425
504
|
try:
|
|
426
505
|
# Create layout using the generator directly to access status methods
|
|
427
506
|
generator = ZellijLayoutGenerator()
|
|
428
|
-
layout_path = generator.create_zellij_layout(sample_layout,
|
|
507
|
+
layout_path = generator.create_zellij_layout(sample_layout, None, "test_session")
|
|
429
508
|
print(f"✅ Layout created successfully: {layout_path}")
|
|
430
509
|
|
|
431
510
|
# Demonstrate status checking
|