webpilot-engine 2.0.0__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 (47) hide show
  1. adapters/__init__.py +14 -0
  2. adapters/base.py +34 -0
  3. adapters/browser_adapter.py +300 -0
  4. adapters/dom_scripts.py +518 -0
  5. adapters/envelope_builder.py +108 -0
  6. adapters/live_session_manager.py +326 -0
  7. adapters/upload_adapter.py +77 -0
  8. builder.py +83 -0
  9. cli.py +341 -0
  10. config/__init__.py +23 -0
  11. config/settings.py +339 -0
  12. constants/__init__.py +165 -0
  13. constants/contracts.py +95 -0
  14. constants/selectors.py +27 -0
  15. constants/timeouts.py +25 -0
  16. core/__init__.py +45 -0
  17. core/exceptions.py +33 -0
  18. core/models.py +273 -0
  19. engine.py +199 -0
  20. exceptions.py +23 -0
  21. mcp_server.py +347 -0
  22. models.py +29 -0
  23. orchestration/__init__.py +19 -0
  24. orchestration/context.py +71 -0
  25. orchestration/flow_orchestrator.py +358 -0
  26. orchestration/session_coordinator.py +88 -0
  27. services/__init__.py +17 -0
  28. services/auth_navigation_service.py +129 -0
  29. services/field_interaction_service.py +174 -0
  30. services/form_filler_service.py +106 -0
  31. services/inspection_formatter_service.py +68 -0
  32. services/inspection_service.py +40 -0
  33. services/reactive_interaction_service.py +174 -0
  34. supervisor/__init__.py +13 -0
  35. supervisor/client.py +171 -0
  36. supervisor/contracts.py +62 -0
  37. supervisor/master_daemon.py +303 -0
  38. supervisor/shell.py +194 -0
  39. supervisor/worker_process.py +400 -0
  40. validators/__init__.py +9 -0
  41. validators/form_validator.py +28 -0
  42. validators/payload_validator.py +25 -0
  43. webpilot_engine-2.0.0.dist-info/METADATA +299 -0
  44. webpilot_engine-2.0.0.dist-info/RECORD +47 -0
  45. webpilot_engine-2.0.0.dist-info/WHEEL +5 -0
  46. webpilot_engine-2.0.0.dist-info/entry_points.txt +6 -0
  47. webpilot_engine-2.0.0.dist-info/top_level.txt +14 -0
supervisor/client.py ADDED
@@ -0,0 +1,171 @@
1
+ """Lightweight HTTP Client for communicating with the Master Supervisor (Layer 1).
2
+
3
+ Includes Zero-Touch Auto-Spawn capability: automatically boots the background
4
+ supervisor host detached via WMI if not already running.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ import time
15
+ import urllib.error
16
+ import urllib.request
17
+ from typing import Any, Dict, Optional
18
+
19
+ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20
+ if PROJECT_ROOT not in sys.path:
21
+ sys.path.insert(0, PROJECT_ROOT)
22
+
23
+ from supervisor.contracts import SupervisorActionRequest, SupervisorActionResponse
24
+
25
+ DEFAULT_SUPERVISOR_URL: str = "http://127.0.0.1:9333"
26
+
27
+
28
+ class SupervisorClient:
29
+ """Thin client for interacting with the Master Supervisor daemon."""
30
+
31
+ def __init__(self, base_url: str = DEFAULT_SUPERVISOR_URL):
32
+ self.base_url = base_url.rstrip("/")
33
+
34
+ def is_running(self) -> bool:
35
+ """Pings the supervisor health endpoint."""
36
+ try:
37
+ req = urllib.request.Request(f"{self.base_url}/ping", headers={"User-Agent": "WebPilotClient/2.0"})
38
+ with urllib.request.urlopen(req, timeout=0.8) as resp:
39
+ return resp.status == 200
40
+ except Exception:
41
+ return False
42
+
43
+ def ensure_running(self) -> None:
44
+ """Checks if supervisor is alive; if not, launches it detached via WMI."""
45
+ if self.is_running():
46
+ return
47
+
48
+ print("[*] Master Supervisor (Layer 1) not running. Auto-spawning background host...")
49
+ script_path = os.path.join(PROJECT_ROOT, "supervisor", "master_daemon.py")
50
+ py_exe = sys.executable
51
+
52
+ # Launch detached via WMI outside any parent terminal Job Object
53
+ if sys.platform == "win32":
54
+ if getattr(sys, "frozen", False):
55
+ py_exe = sys.executable
56
+ cwd = os.path.dirname(py_exe)
57
+ cmd_line = f'`"{py_exe}`" --run-daemon'
58
+ else:
59
+ script_path = os.path.join(PROJECT_ROOT, "supervisor", "master_daemon.py")
60
+ py_exe = sys.executable
61
+ cwd = PROJECT_ROOT
62
+ cmd_line = f'`"{py_exe}`" `"{script_path}`"'
63
+
64
+ ps_script = f"""
65
+ $cwd = '{cwd}'
66
+ $cmd = '{cmd_line}'
67
+ $proc = ([wmiclass]'Win32_Process').Create($cmd, $cwd, $null)
68
+ if ($proc.ReturnValue -eq 0) {{
69
+ Write-Output $proc.ProcessId
70
+ }} else {{
71
+ exit 1
72
+ }}
73
+ """
74
+ encoded = base64.b64encode(ps_script.encode("utf-16le")).decode("ascii")
75
+ subprocess.run(
76
+ ["powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded],
77
+ stdout=subprocess.DEVNULL,
78
+ stderr=subprocess.DEVNULL,
79
+ timeout=5,
80
+ )
81
+ else:
82
+ subprocess.Popen(
83
+ [sys.executable, script_path],
84
+ stdout=subprocess.DEVNULL,
85
+ stderr=subprocess.DEVNULL,
86
+ start_new_session=True,
87
+ cwd=PROJECT_ROOT,
88
+ )
89
+
90
+ # Wait up to 4 seconds for supervisor to become active
91
+ start = time.time()
92
+ while time.time() - start < 4.0:
93
+ if self.is_running():
94
+ print("[✓] Master Supervisor is ready and active.")
95
+ return
96
+ time.sleep(0.2)
97
+
98
+ raise RuntimeError("Failed to auto-spawn Master Supervisor on http://127.0.0.1:9333.")
99
+
100
+ def execute(self, action_req: SupervisorActionRequest) -> SupervisorActionResponse:
101
+ """Sends an action request to the supervisor and returns structured response."""
102
+ self.ensure_running()
103
+ url = f"{self.base_url}/execute"
104
+ body = json.dumps(action_req.to_dict()).encode("utf-8")
105
+ req = urllib.request.Request(
106
+ url,
107
+ data=body,
108
+ headers={"Content-Type": "application/json", "User-Agent": "WebPilotClient/2.0"},
109
+ method="POST",
110
+ )
111
+ try:
112
+ with urllib.request.urlopen(req, timeout=90) as resp:
113
+ data = json.loads(resp.read().decode("utf-8"))
114
+ return SupervisorActionResponse.from_dict(data)
115
+ except Exception as exc:
116
+ return SupervisorActionResponse(
117
+ success=False,
118
+ action=action_req.action,
119
+ message=f"Network error communicating with Master Supervisor: {exc}",
120
+ )
121
+
122
+ def restart_worker(self) -> SupervisorActionResponse:
123
+ """Triggers cascading kill of Layer 2 Worker and Chromium, then respawns fresh."""
124
+ self.ensure_running()
125
+ req = urllib.request.Request(f"{self.base_url}/restart", data=b"{}", method="POST")
126
+ try:
127
+ with urllib.request.urlopen(req, timeout=10) as resp:
128
+ data = json.loads(resp.read().decode("utf-8"))
129
+ return SupervisorActionResponse.from_dict(data)
130
+ except Exception as exc:
131
+ return SupervisorActionResponse(
132
+ success=False,
133
+ action="restart",
134
+ message=f"Failed to restart worker: {exc}",
135
+ )
136
+
137
+ def stop_service(self) -> SupervisorActionResponse:
138
+ """Completely terminates Master Supervisor and all worker tiers."""
139
+ if not self.is_running():
140
+ return SupervisorActionResponse(
141
+ success=True,
142
+ action="stop",
143
+ message="Master Supervisor is already stopped.",
144
+ )
145
+
146
+ req = urllib.request.Request(f"{self.base_url}/stop", data=b"{}", method="POST")
147
+ try:
148
+ with urllib.request.urlopen(req, timeout=10) as resp:
149
+ data = json.loads(resp.read().decode("utf-8"))
150
+ return SupervisorActionResponse.from_dict(data)
151
+ except Exception as exc:
152
+ return SupervisorActionResponse(
153
+ success=False,
154
+ action="stop",
155
+ message=f"Error stopping service: {exc}",
156
+ )
157
+
158
+ def get_status(self) -> Dict[str, Any]:
159
+ """Retrieves runtime health and PID telemetry."""
160
+ if not self.is_running():
161
+ return {"status": "stopped", "supervisor_alive": False}
162
+
163
+ try:
164
+ req = urllib.request.Request(f"{self.base_url}/status")
165
+ with urllib.request.urlopen(req, timeout=2) as resp:
166
+ data = json.loads(resp.read().decode("utf-8"))
167
+ data["status"] = "running"
168
+ data["supervisor_alive"] = True
169
+ return data
170
+ except Exception:
171
+ return {"status": "unreachable", "supervisor_alive": False}
@@ -0,0 +1,62 @@
1
+ """Data contracts for Master Supervisor, Worker Process, and CLI Client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass, field
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ @dataclass
11
+ class SupervisorActionRequest:
12
+ """Action payload sent from CLI or Shell to the Master Supervisor."""
13
+ action: str # 'apply', 'inspect', 'list_tabs', 'switch_tab', 'restart_worker', 'status', 'stop'
14
+ url: Optional[str] = None
15
+ data_path: Optional[str] = None
16
+ fill_arguments: List[str] = field(default_factory=list)
17
+ press_buttons: List[str] = field(default_factory=list)
18
+ upload_files: List[str] = field(default_factory=list)
19
+ cookies_path: Optional[str] = None
20
+ submit: bool = False
21
+ screenshot_path: Optional[str] = None
22
+ tab_index: Optional[int] = None
23
+ unpack_options: bool = False
24
+ headed: bool = False
25
+ auto_inspect: bool = True
26
+ timeout_minutes: int = 30
27
+
28
+ def to_dict(self) -> Dict[str, Any]:
29
+ return asdict(self)
30
+
31
+ @classmethod
32
+ def from_dict(cls, data: Dict[str, Any]) -> SupervisorActionRequest:
33
+ return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
34
+
35
+
36
+ @dataclass
37
+ class SupervisorActionResponse:
38
+ """Standardized response returned by Master Supervisor to CLI or Shell."""
39
+ success: bool
40
+ action: str
41
+ message: str = ""
42
+ browser_pid: Optional[int] = None
43
+ worker_pid: Optional[int] = None
44
+ active_tab_index: Optional[int] = None
45
+ active_url: Optional[str] = None
46
+ active_title: Optional[str] = None
47
+ total_tabs: int = 0
48
+ tabs: List[Dict[str, Any]] = field(default_factory=list)
49
+ telemetry: Dict[str, Any] = field(default_factory=dict)
50
+ schema: Optional[Dict[str, Any]] = None
51
+ confirmed_fields: List[Any] = field(default_factory=list)
52
+ failed_fields: List[Any] = field(default_factory=list)
53
+ validation_errors: List[str] = field(default_factory=list)
54
+ reactive_event: Optional[Dict[str, Any]] = None
55
+ output_lines: List[str] = field(default_factory=list)
56
+
57
+ def to_dict(self) -> Dict[str, Any]:
58
+ return asdict(self)
59
+
60
+ @classmethod
61
+ def from_dict(cls, data: Dict[str, Any]) -> SupervisorActionResponse:
62
+ return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
@@ -0,0 +1,303 @@
1
+ """Layer 1: Master Supervisor Daemon Host.
2
+
3
+ Runs as a lightweight, persistent background daemon (Zero-Admin, Standard User).
4
+ Supervises Layer 2 (Operational Worker) and enforces cascading teardown.
5
+ Exposes a minimal HTTP endpoint on 127.0.0.1:9333 for CLI and Shell clients.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ import threading
15
+ import time
16
+ from http.server import BaseHTTPRequestHandler, HTTPServer
17
+ from typing import Any, Dict, Optional
18
+
19
+ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20
+ if PROJECT_ROOT not in sys.path:
21
+ sys.path.insert(0, PROJECT_ROOT)
22
+
23
+ from supervisor.contracts import SupervisorActionRequest, SupervisorActionResponse
24
+
25
+ DEFAULT_SUPERVISOR_HOST: str = "127.0.0.1"
26
+ DEFAULT_SUPERVISOR_PORT: int = 9333
27
+ DEFAULT_INACTIVITY_TIMEOUT_SECONDS: int = 1800 # 30 minutes
28
+ LOG_FILE: str = os.path.join(PROJECT_ROOT, "master_daemon.log")
29
+
30
+
31
+ class LogWriter:
32
+ """Safe file-backed stream writer for headless daemon background logging."""
33
+ def __init__(self, filepath: str):
34
+ self.filepath = filepath
35
+
36
+ def write(self, s: str) -> None:
37
+ if s:
38
+ try:
39
+ with open(self.filepath, "a", encoding="utf-8") as f:
40
+ f.write(s)
41
+ except Exception:
42
+ pass
43
+
44
+ def flush(self) -> None:
45
+ pass
46
+
47
+
48
+ def log_daemon(msg: str) -> None:
49
+ """Appends timestamped event to master_daemon.log."""
50
+ try:
51
+ ts = time.strftime("%Y-%m-%d %H:%M:%S")
52
+ with open(LOG_FILE, "a", encoding="utf-8") as f:
53
+ f.write(f"[{ts}] {msg}\n")
54
+ except Exception:
55
+ pass
56
+
57
+
58
+ class MasterSupervisor:
59
+ """Layer 1 Master Supervisor: controls Worker process lifecycle and inactivity watchdog."""
60
+
61
+ def __init__(
62
+ self,
63
+ host: str = DEFAULT_SUPERVISOR_HOST,
64
+ port: int = DEFAULT_SUPERVISOR_PORT,
65
+ inactivity_timeout_seconds: int = DEFAULT_INACTIVITY_TIMEOUT_SECONDS,
66
+ ):
67
+ self.host = host
68
+ self.port = port
69
+ self.inactivity_timeout_seconds = inactivity_timeout_seconds
70
+ self.worker_process: Optional[subprocess.Popen] = None
71
+ self.last_active_time: float = time.time()
72
+ self.lock = threading.Lock()
73
+ self._is_running = True
74
+
75
+ # Launch watchdog thread
76
+ self.watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True)
77
+ self.watchdog_thread.start()
78
+
79
+ def _ensure_worker(self) -> subprocess.Popen:
80
+ """Spawns Layer 2 Worker subprocess if not currently running."""
81
+ with self.lock:
82
+ if self.worker_process is None or self.worker_process.poll() is not None:
83
+ if getattr(sys, "frozen", False):
84
+ worker_cmd = [sys.executable, "--run-worker"]
85
+ worker_cwd = os.path.dirname(sys.executable)
86
+ else:
87
+ worker_cmd = [sys.executable, "-m", "supervisor.worker_process"]
88
+ worker_cwd = PROJECT_ROOT
89
+
90
+ self.worker_process = subprocess.Popen(
91
+ worker_cmd,
92
+ stdin=subprocess.PIPE,
93
+ stdout=subprocess.PIPE,
94
+ stderr=subprocess.PIPE,
95
+ text=True,
96
+ bufsize=1,
97
+ cwd=worker_cwd,
98
+ )
99
+ return self.worker_process
100
+
101
+ def execute_action(self, req: SupervisorActionRequest) -> SupervisorActionResponse:
102
+ """Proxies action request to Layer 2 Worker and returns structured response."""
103
+ self.last_active_time = time.time()
104
+
105
+ if req.action == "restart_worker":
106
+ return self.restart_worker()
107
+
108
+ if req.action in ("stop", "shutdown"):
109
+ self.terminate_worker()
110
+ self._is_running = False
111
+ return SupervisorActionResponse(
112
+ success=True,
113
+ action="stop",
114
+ message="Master Supervisor and all worker tiers shut down.",
115
+ )
116
+
117
+ worker = self._ensure_worker()
118
+ assert worker.stdin is not None and worker.stdout is not None
119
+
120
+ req_line = json.dumps(req.to_dict()) + "\n"
121
+ try:
122
+ worker.stdin.write(req_line)
123
+ worker.stdin.flush()
124
+
125
+ data = None
126
+ while True:
127
+ line = worker.stdout.readline()
128
+ if not line:
129
+ break
130
+ line_str = line.strip()
131
+ if not line_str:
132
+ continue
133
+ try:
134
+ data = json.loads(line_str)
135
+ break
136
+ except Exception:
137
+ log_daemon(f"Skipping non-JSON worker stdout: {line_str}")
138
+ continue
139
+
140
+ if not data:
141
+ log_daemon("Worker returned no valid JSON response.")
142
+ self.terminate_worker()
143
+ return SupervisorActionResponse(
144
+ success=False,
145
+ action=req.action,
146
+ message="Operational Worker failed to return valid JSON response.",
147
+ )
148
+
149
+ return SupervisorActionResponse.from_dict(data)
150
+
151
+ except Exception as exc:
152
+ return SupervisorActionResponse(
153
+ success=False,
154
+ action=req.action,
155
+ message=f"Supervisor dispatch error: {exc}",
156
+ )
157
+
158
+ def restart_worker(self) -> SupervisorActionResponse:
159
+ """Cascading kill of Layer 2 Worker and all child Chromium processes, then respawns fresh."""
160
+ with self.lock:
161
+ old_pid = self.worker_process.pid if self.worker_process else None
162
+ self.terminate_worker()
163
+ # Spawn fresh Worker
164
+ new_worker = subprocess.Popen(
165
+ [sys.executable, "-m", "supervisor.worker_process"],
166
+ stdin=subprocess.PIPE,
167
+ stdout=subprocess.PIPE,
168
+ stderr=subprocess.PIPE,
169
+ text=True,
170
+ bufsize=1,
171
+ cwd=PROJECT_ROOT,
172
+ )
173
+ self.worker_process = new_worker
174
+ self.last_active_time = time.time()
175
+
176
+ return SupervisorActionResponse(
177
+ success=True,
178
+ action="restart_worker",
179
+ message=f"Cascading kill completed for PID {old_pid}. Fresh Worker spawned (PID: {new_worker.pid}).",
180
+ worker_pid=new_worker.pid,
181
+ )
182
+
183
+ def terminate_worker(self) -> None:
184
+ """Forcefully kills the Layer 2 Worker process tree."""
185
+ proc = self.worker_process
186
+ self.worker_process = None
187
+ if proc and proc.poll() is None:
188
+ try:
189
+ if sys.platform == "win32":
190
+ subprocess.run(
191
+ ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
192
+ stdout=subprocess.DEVNULL,
193
+ stderr=subprocess.DEVNULL,
194
+ )
195
+ else:
196
+ proc.kill()
197
+ except Exception:
198
+ pass
199
+
200
+ def get_status(self) -> Dict[str, Any]:
201
+ """Returns telemetry for Supervisor and Worker."""
202
+ worker_alive = self.worker_process is not None and self.worker_process.poll() is None
203
+ worker_pid = self.worker_process.pid if worker_alive else None
204
+ return {
205
+ "supervisor_pid": os.getpid(),
206
+ "supervisor_port": self.port,
207
+ "worker_alive": worker_alive,
208
+ "worker_pid": worker_pid,
209
+ "idle_seconds": round(time.time() - self.last_active_time, 1),
210
+ "inactivity_timeout_seconds": self.inactivity_timeout_seconds,
211
+ }
212
+
213
+ def _watchdog_loop(self) -> None:
214
+ """Periodically checks inactivity. Kills idle Worker to release 100% of browser RAM."""
215
+ while self._is_running:
216
+ time.sleep(15)
217
+ if self.worker_process and self.worker_process.poll() is None:
218
+ idle = time.time() - self.last_active_time
219
+ if idle > self.inactivity_timeout_seconds:
220
+ print(f"[*] Inactivity watchdog triggered ({idle:.0f}s idle). Terminating Worker to release RAM.")
221
+ self.terminate_worker()
222
+
223
+
224
+ def create_supervisor_handler(supervisor: MasterSupervisor):
225
+ """Factory creating an HTTP Request Handler bound to the supervisor instance."""
226
+
227
+ class SupervisorHTTPHandler(BaseHTTPRequestHandler):
228
+ def log_message(self, format, *args):
229
+ # Suppress default stdout HTTP access logs
230
+ return
231
+
232
+ def _send_json(self, status_code: int, data: Dict[str, Any]):
233
+ body = json.dumps(data, ensure_ascii=False).encode("utf-8")
234
+ self.send_response(status_code)
235
+ self.send_header("Content-Type", "application/json; charset=utf-8")
236
+ self.send_header("Content-Length", str(len(body)))
237
+ self.end_headers()
238
+ self.wfile.write(body)
239
+
240
+ def do_GET(self):
241
+ if self.path == "/ping":
242
+ self._send_json(200, {"status": "ok", "supervisor_pid": os.getpid()})
243
+ elif self.path == "/status":
244
+ status = supervisor.get_status()
245
+ self._send_json(200, status)
246
+ else:
247
+ self._send_json(404, {"error": "Not Found"})
248
+
249
+ def do_POST(self):
250
+ content_length = int(self.headers.get("Content-Length", 0))
251
+ raw_body = self.rfile.read(content_length).decode("utf-8") if content_length > 0 else "{}"
252
+
253
+ try:
254
+ data = json.loads(raw_body)
255
+ except Exception:
256
+ data = {}
257
+
258
+ if self.path == "/execute":
259
+ req = SupervisorActionRequest.from_dict(data)
260
+ res = supervisor.execute_action(req)
261
+ self._send_json(200, res.to_dict())
262
+ elif self.path == "/restart":
263
+ res = supervisor.restart_worker()
264
+ self._send_json(200, res.to_dict())
265
+ elif self.path == "/stop":
266
+ req = SupervisorActionRequest(action="stop")
267
+ res = supervisor.execute_action(req)
268
+ self._send_json(200, res.to_dict())
269
+ # Schedule daemon shutdown in background thread
270
+ threading.Thread(target=self.server.shutdown, daemon=True).start()
271
+ else:
272
+ self._send_json(404, {"error": "Not Found"})
273
+
274
+ return SupervisorHTTPHandler
275
+
276
+
277
+ def run_master_daemon(port: int = DEFAULT_SUPERVISOR_PORT):
278
+ """Starts the Master Supervisor HTTP service."""
279
+ sys.stdout = LogWriter(LOG_FILE)
280
+ sys.stderr = LogWriter(LOG_FILE)
281
+ log_daemon(f"run_master_daemon starting on port {port} (PID: {os.getpid()})")
282
+ try:
283
+ supervisor = MasterSupervisor(port=port)
284
+ handler_class = create_supervisor_handler(supervisor)
285
+ server = HTTPServer((DEFAULT_SUPERVISOR_HOST, port), handler_class)
286
+ log_daemon(f"Master Supervisor listening on http://{DEFAULT_SUPERVISOR_HOST}:{port}")
287
+ print(f"[*] Master Supervisor (Layer 1) started on http://{DEFAULT_SUPERVISOR_HOST}:{port} (PID: {os.getpid()})")
288
+ server.serve_forever()
289
+ except Exception as exc:
290
+ log_daemon(f"Master Supervisor fatal exception: {exc}")
291
+ raise
292
+ finally:
293
+ try:
294
+ supervisor.terminate_worker()
295
+ except Exception:
296
+ pass
297
+ log_daemon("Master Supervisor stopped.")
298
+ print("[*] Master Supervisor stopped.")
299
+
300
+
301
+ if __name__ == "__main__":
302
+ port = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_SUPERVISOR_PORT
303
+ run_master_daemon(port=port)