experimaestro 2.0.0b4__py3-none-any.whl → 2.0.0b17__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 experimaestro might be problematic. Click here for more details.

Files changed (154) hide show
  1. experimaestro/__init__.py +12 -5
  2. experimaestro/cli/__init__.py +393 -134
  3. experimaestro/cli/filter.py +48 -23
  4. experimaestro/cli/jobs.py +253 -71
  5. experimaestro/cli/refactor.py +1 -2
  6. experimaestro/commandline.py +7 -4
  7. experimaestro/connectors/__init__.py +9 -1
  8. experimaestro/connectors/local.py +43 -3
  9. experimaestro/core/arguments.py +18 -18
  10. experimaestro/core/identifier.py +11 -11
  11. experimaestro/core/objects/config.py +96 -39
  12. experimaestro/core/objects/config_walk.py +3 -3
  13. experimaestro/core/{subparameters.py → partial.py} +16 -16
  14. experimaestro/core/partial_lock.py +394 -0
  15. experimaestro/core/types.py +12 -15
  16. experimaestro/dynamic.py +290 -0
  17. experimaestro/experiments/__init__.py +6 -2
  18. experimaestro/experiments/cli.py +223 -52
  19. experimaestro/experiments/configuration.py +24 -0
  20. experimaestro/generators.py +5 -5
  21. experimaestro/ipc.py +118 -1
  22. experimaestro/launcherfinder/__init__.py +2 -2
  23. experimaestro/launcherfinder/registry.py +6 -7
  24. experimaestro/launcherfinder/specs.py +2 -9
  25. experimaestro/launchers/slurm/__init__.py +2 -2
  26. experimaestro/launchers/slurm/base.py +62 -0
  27. experimaestro/locking.py +957 -1
  28. experimaestro/notifications.py +89 -201
  29. experimaestro/progress.py +63 -366
  30. experimaestro/rpyc.py +0 -2
  31. experimaestro/run.py +29 -2
  32. experimaestro/scheduler/__init__.py +8 -1
  33. experimaestro/scheduler/base.py +650 -53
  34. experimaestro/scheduler/dependencies.py +20 -16
  35. experimaestro/scheduler/experiment.py +764 -169
  36. experimaestro/scheduler/interfaces.py +338 -96
  37. experimaestro/scheduler/jobs.py +58 -20
  38. experimaestro/scheduler/remote/__init__.py +31 -0
  39. experimaestro/scheduler/remote/adaptive_sync.py +265 -0
  40. experimaestro/scheduler/remote/client.py +928 -0
  41. experimaestro/scheduler/remote/protocol.py +282 -0
  42. experimaestro/scheduler/remote/server.py +447 -0
  43. experimaestro/scheduler/remote/sync.py +144 -0
  44. experimaestro/scheduler/services.py +186 -35
  45. experimaestro/scheduler/state_provider.py +811 -2157
  46. experimaestro/scheduler/state_status.py +1247 -0
  47. experimaestro/scheduler/transient.py +31 -0
  48. experimaestro/scheduler/workspace.py +1 -1
  49. experimaestro/scheduler/workspace_state_provider.py +1273 -0
  50. experimaestro/scriptbuilder.py +4 -4
  51. experimaestro/settings.py +36 -0
  52. experimaestro/tests/conftest.py +33 -5
  53. experimaestro/tests/connectors/bin/executable.py +1 -1
  54. experimaestro/tests/fixtures/pre_experiment/experiment_check_env.py +16 -0
  55. experimaestro/tests/fixtures/pre_experiment/experiment_check_mock.py +14 -0
  56. experimaestro/tests/fixtures/pre_experiment/experiment_simple.py +12 -0
  57. experimaestro/tests/fixtures/pre_experiment/pre_setup_env.py +5 -0
  58. experimaestro/tests/fixtures/pre_experiment/pre_setup_error.py +3 -0
  59. experimaestro/tests/fixtures/pre_experiment/pre_setup_mock.py +8 -0
  60. experimaestro/tests/launchers/bin/test.py +1 -0
  61. experimaestro/tests/launchers/test_slurm.py +9 -9
  62. experimaestro/tests/partial_reschedule.py +46 -0
  63. experimaestro/tests/restart.py +3 -3
  64. experimaestro/tests/restart_main.py +1 -0
  65. experimaestro/tests/scripts/notifyandwait.py +1 -0
  66. experimaestro/tests/task_partial.py +38 -0
  67. experimaestro/tests/task_tokens.py +2 -2
  68. experimaestro/tests/tasks/test_dynamic.py +6 -6
  69. experimaestro/tests/test_dependencies.py +3 -3
  70. experimaestro/tests/test_deprecated.py +15 -15
  71. experimaestro/tests/test_dynamic_locking.py +317 -0
  72. experimaestro/tests/test_environment.py +24 -14
  73. experimaestro/tests/test_experiment.py +171 -36
  74. experimaestro/tests/test_identifier.py +25 -25
  75. experimaestro/tests/test_identifier_stability.py +3 -5
  76. experimaestro/tests/test_multitoken.py +2 -4
  77. experimaestro/tests/{test_subparameters.py → test_partial.py} +25 -25
  78. experimaestro/tests/test_partial_paths.py +81 -138
  79. experimaestro/tests/test_pre_experiment.py +219 -0
  80. experimaestro/tests/test_progress.py +2 -8
  81. experimaestro/tests/test_remote_state.py +1132 -0
  82. experimaestro/tests/test_stray_jobs.py +261 -0
  83. experimaestro/tests/test_tasks.py +1 -2
  84. experimaestro/tests/test_token_locking.py +52 -67
  85. experimaestro/tests/test_tokens.py +5 -6
  86. experimaestro/tests/test_transient.py +225 -0
  87. experimaestro/tests/test_workspace_state_provider.py +768 -0
  88. experimaestro/tests/token_reschedule.py +1 -3
  89. experimaestro/tests/utils.py +2 -7
  90. experimaestro/tokens.py +227 -372
  91. experimaestro/tools/diff.py +1 -0
  92. experimaestro/tools/documentation.py +4 -5
  93. experimaestro/tools/jobs.py +1 -2
  94. experimaestro/tui/app.py +459 -1895
  95. experimaestro/tui/app.tcss +162 -0
  96. experimaestro/tui/dialogs.py +172 -0
  97. experimaestro/tui/log_viewer.py +253 -3
  98. experimaestro/tui/messages.py +137 -0
  99. experimaestro/tui/utils.py +54 -0
  100. experimaestro/tui/widgets/__init__.py +23 -0
  101. experimaestro/tui/widgets/experiments.py +468 -0
  102. experimaestro/tui/widgets/global_services.py +238 -0
  103. experimaestro/tui/widgets/jobs.py +972 -0
  104. experimaestro/tui/widgets/log.py +156 -0
  105. experimaestro/tui/widgets/orphans.py +363 -0
  106. experimaestro/tui/widgets/runs.py +185 -0
  107. experimaestro/tui/widgets/services.py +314 -0
  108. experimaestro/tui/widgets/stray_jobs.py +528 -0
  109. experimaestro/utils/__init__.py +1 -1
  110. experimaestro/utils/environment.py +105 -22
  111. experimaestro/utils/fswatcher.py +124 -0
  112. experimaestro/utils/jobs.py +1 -2
  113. experimaestro/utils/jupyter.py +1 -2
  114. experimaestro/utils/logging.py +72 -0
  115. experimaestro/version.py +2 -2
  116. experimaestro/webui/__init__.py +9 -0
  117. experimaestro/webui/app.py +117 -0
  118. experimaestro/{server → webui}/data/index.css +66 -11
  119. experimaestro/webui/data/index.css.map +1 -0
  120. experimaestro/{server → webui}/data/index.js +82763 -87217
  121. experimaestro/webui/data/index.js.map +1 -0
  122. experimaestro/webui/routes/__init__.py +5 -0
  123. experimaestro/webui/routes/auth.py +53 -0
  124. experimaestro/webui/routes/proxy.py +117 -0
  125. experimaestro/webui/server.py +200 -0
  126. experimaestro/webui/state_bridge.py +152 -0
  127. experimaestro/webui/websocket.py +413 -0
  128. {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/METADATA +8 -9
  129. experimaestro-2.0.0b17.dist-info/RECORD +219 -0
  130. experimaestro/cli/progress.py +0 -269
  131. experimaestro/scheduler/state.py +0 -75
  132. experimaestro/scheduler/state_db.py +0 -388
  133. experimaestro/scheduler/state_sync.py +0 -834
  134. experimaestro/server/__init__.py +0 -467
  135. experimaestro/server/data/index.css.map +0 -1
  136. experimaestro/server/data/index.js.map +0 -1
  137. experimaestro/tests/test_cli_jobs.py +0 -615
  138. experimaestro/tests/test_file_progress.py +0 -425
  139. experimaestro/tests/test_file_progress_integration.py +0 -477
  140. experimaestro/tests/test_state_db.py +0 -434
  141. experimaestro-2.0.0b4.dist-info/RECORD +0 -181
  142. /experimaestro/{server → webui}/data/1815e00441357e01619e.ttf +0 -0
  143. /experimaestro/{server → webui}/data/2463b90d9a316e4e5294.woff2 +0 -0
  144. /experimaestro/{server → webui}/data/2582b0e4bcf85eceead0.ttf +0 -0
  145. /experimaestro/{server → webui}/data/89999bdf5d835c012025.woff2 +0 -0
  146. /experimaestro/{server → webui}/data/914997e1bdfc990d0897.ttf +0 -0
  147. /experimaestro/{server → webui}/data/c210719e60948b211a12.woff2 +0 -0
  148. /experimaestro/{server → webui}/data/favicon.ico +0 -0
  149. /experimaestro/{server → webui}/data/index.html +0 -0
  150. /experimaestro/{server → webui}/data/login.html +0 -0
  151. /experimaestro/{server → webui}/data/manifest.json +0 -0
  152. {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/WHEEL +0 -0
  153. {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/entry_points.txt +0 -0
  154. {experimaestro-2.0.0b4.dist-info → experimaestro-2.0.0b17.dist-info}/licenses/LICENSE +0 -0
@@ -1,467 +0,0 @@
1
- from datetime import datetime
2
- import logging
3
- import asyncio
4
- import platform
5
- import socket
6
- import uuid
7
- from experimaestro.scheduler.base import Job
8
- import sys
9
- import http
10
- import threading
11
- from typing import Optional, Tuple, ClassVar
12
-
13
- from importlib.resources import files
14
- from experimaestro.scheduler import Scheduler, Listener as BaseListener
15
- from experimaestro.scheduler.services import Service, ServiceListener
16
- from experimaestro.settings import ServerSettings
17
- from flask import Flask, Request, Response
18
- from flask import request, redirect
19
- from flask_socketio import SocketIO, emit, ConnectionRefusedError
20
- import requests
21
-
22
-
23
- def formattime(v: Optional[float]):
24
- if not v:
25
- return ""
26
-
27
- return datetime.fromtimestamp(v).isoformat()
28
-
29
-
30
- def progress_state(job: Job):
31
- return [
32
- {"level": o.level, "progress": o.progress, "desc": o.desc} for o in job.progress
33
- ]
34
-
35
-
36
- def job_details(job):
37
- return {
38
- "jobId": job.identifier,
39
- "taskId": job.name,
40
- "locator": str(job.jobpath),
41
- "status": job.state.name.lower(),
42
- "start": formattime(job.starttime),
43
- "end": formattime(job.endtime),
44
- "submitted": formattime(job.submittime),
45
- "tags": list(job.tags.items()),
46
- "progress": progress_state(job),
47
- }
48
-
49
-
50
- def job_create(job: Job):
51
- # Get experiment IDs from job.experiments list
52
- experiment_ids = [xp.workdir.name for xp in job.experiments]
53
-
54
- return {
55
- "jobId": job.identifier,
56
- "taskId": job.name,
57
- "locator": str(job.jobpath),
58
- "status": job.state.name.lower(),
59
- "tags": list(job.tags.items()),
60
- "progress": progress_state(job),
61
- "experimentIds": experiment_ids, # Add experiment IDs
62
- }
63
-
64
-
65
- class Listener(BaseListener, ServiceListener):
66
- def __init__(self, socketio, state_provider):
67
- self.socketio = socketio
68
- self.state_provider = state_provider
69
-
70
- # Try to get the scheduler (if one is running for active experiments)
71
- # Otherwise we're in monitoring mode and don't need scheduler events
72
- try:
73
- from experimaestro.scheduler import Scheduler
74
-
75
- # Check if a scheduler instance exists (would be created if experiments are running)
76
- if Scheduler._instance is not None:
77
- self.scheduler = Scheduler._instance
78
- self.scheduler.addlistener(self)
79
- self.services = {}
80
- # Initialize services from all registered experiments
81
- for xp in self.scheduler.experiments.values():
82
- for service in xp.services.values():
83
- self.service_add(service)
84
- else:
85
- # No scheduler running - monitoring mode
86
- self.scheduler = None
87
- self.services = {}
88
- except Exception:
89
- # Scheduler not available - monitoring mode
90
- self.scheduler = None
91
- self.services = {}
92
-
93
- def job_submitted(self, job):
94
- self.socketio.emit("job.add", job_create(job))
95
-
96
- def job_state(self, job):
97
- experiment_ids = [xp.workdir.name for xp in job.experiments]
98
- self.socketio.emit(
99
- "job.update",
100
- {
101
- "jobId": job.identifier,
102
- "status": job.state.name.lower(),
103
- "progress": progress_state(job),
104
- "experimentIds": experiment_ids,
105
- },
106
- )
107
-
108
- def service_add(self, service: Service):
109
- service.add_listener(self)
110
- self.services[service.id] = service
111
- self.socketio.emit(
112
- "service.add",
113
- {
114
- "id": service.id,
115
- "description": service.description(),
116
- "state": service.state.name,
117
- },
118
- )
119
-
120
- def service_state_changed(self, service: Service):
121
- self.socketio.emit("service.update", {"state": service.state.name})
122
-
123
-
124
- MIMETYPES = {
125
- "html": "text/html",
126
- "map": "text/plain",
127
- "txt": "text/plain",
128
- "ico": "image/x-icon",
129
- "png": "image/png",
130
- "css": "text/css",
131
- "js": "application/javascript",
132
- "json": "application/json",
133
- "eot": "font/vnd.ms-fontobject",
134
- "woff": "font/woff",
135
- "woff2": "font/woff2",
136
- "ttf": "font/ttf",
137
- }
138
-
139
-
140
- def proxy_response(base_url: str, request: Request, path: str):
141
- # Whitelist a few headers to pass on
142
- request_headers = {}
143
- for key, value in request.headers.items():
144
- request_headers[key] = value
145
-
146
- if request.query_string:
147
- path = f"""{path}?{request.query_string.decode("utf-8")}"""
148
-
149
- data = None
150
- if request.method == "POST":
151
- data = request.get_data()
152
-
153
- response = requests.request(
154
- request.method,
155
- f"{base_url}{path}",
156
- data=data,
157
- stream=True,
158
- headers=request_headers,
159
- )
160
- headers = {}
161
- for key, value in response.headers.items():
162
- headers[key] = value
163
-
164
- flask_response = Response(
165
- response=response.raw.read(),
166
- status=response.status_code,
167
- headers=headers,
168
- content_type=response.headers["content-type"],
169
- )
170
- return flask_response
171
-
172
-
173
- # flake8: noqa: C901
174
- def start_app(server: "Server"):
175
- logging.debug("Starting Flask server...")
176
- app = Flask("experimaestro")
177
-
178
- logging.debug("Starting Flask server (SocketIO)...")
179
- socketio = SocketIO(app, path="/api", async_mode="gevent")
180
- listener = Listener(socketio, server.state_provider)
181
-
182
- logging.debug("Starting Flask server (setting up socketio)...")
183
-
184
- @socketio.on("connect")
185
- def handle_connect():
186
- if server.token != request.cookies.get("experimaestro_token", None):
187
- raise ConnectionRefusedError("invalid token")
188
-
189
- @socketio.on("refresh")
190
- def handle_refresh(experiment_id=None):
191
- """Refresh jobs for an experiment (or all experiments if None)"""
192
- if experiment_id:
193
- # Refresh specific experiment
194
- jobs = listener.state_provider.get_jobs(experiment_id)
195
- for job_data in jobs:
196
- emit("job.add", job_data)
197
- else:
198
- # Refresh all experiments
199
- if listener.scheduler:
200
- # Active experiments: get jobs from scheduler
201
- for job in listener.scheduler.jobs.values():
202
- emit("job.add", job_create(job))
203
- else:
204
- # Monitoring mode: get jobs from WorkspaceStateProvider
205
- for exp in listener.state_provider.get_experiments():
206
- exp_id = exp["experiment_id"]
207
- jobs = listener.state_provider.get_jobs(exp_id)
208
- for job_data in jobs:
209
- emit("job.add", job_data)
210
-
211
- @socketio.on("experiments")
212
- def handle_experiments():
213
- """List all experiments"""
214
- experiments = listener.state_provider.get_experiments()
215
- for exp in experiments:
216
- emit("experiment.add", exp)
217
-
218
- @socketio.on("job.details")
219
- def handle_details(data):
220
- """Get job details - expects {experimentId, jobId} or just jobId (backward compat)"""
221
- # Backward compatibility: if data is a string, treat it as jobId
222
- if isinstance(data, str):
223
- jobid = data
224
- if listener.scheduler:
225
- emit("job.update", job_details(listener.scheduler.jobs[jobid]))
226
- else:
227
- experiment_id = data.get("experimentId")
228
- job_id = data.get("jobId")
229
- job_data = listener.state_provider.get_job(experiment_id, job_id)
230
- if job_data:
231
- emit("job.update", job_data)
232
-
233
- @socketio.on("services")
234
- def handle_services_list():
235
- for service in listener.services.values():
236
- emit(
237
- "service.add",
238
- {
239
- "id": service.id,
240
- "description": service.description(),
241
- "state": service.state.name,
242
- },
243
- )
244
-
245
- @socketio.on("job.kill")
246
- def handle_job_kill(data):
247
- """Kill a job - expects {experimentId, jobId} or just jobId (backward compat)"""
248
- # Backward compatibility: if data is a string, treat it as jobId
249
- if isinstance(data, str):
250
- jobid = data
251
- if listener.scheduler:
252
- job = listener.scheduler.jobs[jobid]
253
- future = asyncio.run_coroutine_threadsafe(
254
- job.aio_process(), listener.scheduler.loop
255
- )
256
- process = future.result()
257
- if process is not None:
258
- process.kill()
259
- else:
260
- experiment_id = data.get("experimentId")
261
- job_id = data.get("jobId")
262
- try:
263
- listener.state_provider.kill_job(experiment_id, job_id)
264
- except NotImplementedError:
265
- logging.warning("kill_job not supported for this state provider")
266
-
267
- logging.debug("Starting Flask server (setting up routes)...")
268
-
269
- @app.route("/services/<path:path>", methods=["GET", "POST"])
270
- def route_service(path):
271
- service, *path = path.split("/", 1)
272
- if not path:
273
- return redirect(f"/services/{service}/", http.HTTPStatus.PERMANENT_REDIRECT)
274
-
275
- # Get service from all registered experiments
276
- scheduler = Scheduler.instance()
277
- service_obj = None
278
- for xp in scheduler.experiments.values():
279
- service_obj = xp.services.get(service, None)
280
- if service_obj:
281
- break
282
-
283
- service = service_obj
284
- if service is None:
285
- return Response(f"Service {service} not found", http.HTTPStatus.NOT_FOUND)
286
-
287
- base_url = service.get_url()
288
- return proxy_response(base_url, request, path[0] if path else "/")
289
-
290
- @app.route("/notifications/<jobid>/progress")
291
- def notifications_progress(jobid):
292
- level = int(request.args.get("level", 0))
293
- progress = float(request.args.get("progress", 0.0))
294
-
295
- try:
296
- scheduler = Scheduler.instance()
297
- scheduler.jobs[jobid].set_progress(
298
- level,
299
- progress,
300
- request.args.get("desc", None),
301
- )
302
- except KeyError:
303
- # Just ignore
304
- pass
305
- return Response("", http.HTTPStatus.OK)
306
-
307
- @app.route("/")
308
- def route_root():
309
- if server.token == request.cookies.get("experimaestro_token", None):
310
- return redirect("/index.html", 302)
311
- return redirect("/login.html", 302)
312
-
313
- @app.route("/auth")
314
- def route_auth():
315
- if token := request.args.get("xpm-token", None):
316
- if server.token == token:
317
- resp = redirect("/index.html", 302)
318
- resp.set_cookie("experimaestro_token", token)
319
- return resp
320
- return redirect("/login.html", 302)
321
-
322
- @app.route("/stop")
323
- def route_stop():
324
- if (server.token == request.args.get("xpm-token", None)) or (
325
- server.token == request.cookies.get("experimaestro_token", None)
326
- ):
327
- socketio.stop()
328
- return Response(status=http.HTTPStatus.ACCEPTED)
329
- return Response(status=http.HTTPStatus.UNAUTHORIZED)
330
-
331
- @app.route("/<path:path>")
332
- def static_route(path):
333
- if token := request.form.get("experimaestro_token", None):
334
- if server.token == token:
335
- request.cookies["experimaestro_token"] = token
336
-
337
- if path == "index.html":
338
- if server.token != request.cookies.get("experimaestro_token", None):
339
- return redirect("/login.html", code=302)
340
-
341
- datapath = "data/%s" % path
342
- logging.debug("Looking for %s", datapath)
343
-
344
- try:
345
- package_files = files("experimaestro.server")
346
- resource_file = package_files / datapath
347
- if resource_file.is_file():
348
- mimetype = MIMETYPES[datapath.rsplit(".", 1)[1]]
349
- content = resource_file.read_bytes()
350
- return Response(content, mimetype=mimetype)
351
- except (FileNotFoundError, KeyError):
352
- pass
353
-
354
- return Response("Page not found", status=404)
355
-
356
- # Start the app
357
- if server.port is None or server.port == 0:
358
- logging.info("Searching for an available port")
359
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
360
- sock.bind(("", 0))
361
- server.port = sock.getsockname()[1]
362
- sock.close()
363
-
364
- logging.info(
365
- "Web server started on http://%s:%d/auth?xpm-token=%s",
366
- server.host,
367
- server.port,
368
- server.token,
369
- )
370
-
371
- server.instance = socketio
372
- with server.cv_running:
373
- server.running = True
374
- server.cv_running.notify()
375
- socketio.run(
376
- app,
377
- host=server.host,
378
- port=server.port,
379
- debug=False,
380
- use_reloader=False,
381
- )
382
- logging.info("Web server stopped")
383
-
384
-
385
- class Server:
386
- _instance: ClassVar[Optional["Server"]] = None
387
- _lock: ClassVar[threading.Lock] = threading.Lock()
388
-
389
- @staticmethod
390
- def instance(settings: ServerSettings = None, state_provider=None) -> "Server":
391
- """Get or create the global server instance
392
-
393
- Args:
394
- settings: Server settings (optional)
395
- state_provider: WorkspaceStateProvider instance (required)
396
- """
397
- if Server._instance is None:
398
- with Server._lock:
399
- if Server._instance is None:
400
- if settings is None:
401
- from experimaestro.settings import get_settings
402
-
403
- settings = get_settings().server
404
-
405
- # State provider is required - it should be passed explicitly
406
- if state_provider is None:
407
- raise ValueError(
408
- "state_provider parameter is required. "
409
- "Get it via WorkspaceStateProvider.get_instance(workspace.path)"
410
- )
411
-
412
- Server._instance = Server(settings, state_provider)
413
- return Server._instance
414
-
415
- def __init__(self, settings: ServerSettings, state_provider):
416
- if settings.autohost == "fqdn":
417
- settings.host = socket.getfqdn()
418
- logging.info("Auto host name (fqdn): %s", settings.host)
419
- elif settings.autohost == "name":
420
- settings.host = platform.node()
421
- logging.info("Auto host name (name): %s", settings.host)
422
-
423
- if settings.host is None or settings.host == "127.0.0.1":
424
- self.bindinghost = "127.0.0.1"
425
- else:
426
- self.bindinghost = "0.0.0.0"
427
-
428
- self.host = settings.host or "127.0.0.1"
429
- self.port = settings.port
430
- self.token = settings.token or uuid.uuid4().hex
431
- self.state_provider = state_provider
432
- self.instance = None
433
- self.running = False
434
- self.cv_running = threading.Condition()
435
-
436
- def getNotificationSpec(self) -> Tuple[str, str]:
437
- """Returns a tuple (server ID, server URL)"""
438
- return (
439
- f"""{self.host}_{self.port}.url""",
440
- f"""http://{self.host}:{self.port}/notifications""",
441
- )
442
-
443
- def stop(self):
444
- if self.instance:
445
- try:
446
- requests.get(
447
- f"http://{self.host}:{self.port}/stop?xpm-token={self.token}"
448
- )
449
- except requests.exceptions.ConnectionError:
450
- # This is expected
451
- pass
452
-
453
- def start(self):
454
- """Start the websocket server in a daemon thread"""
455
- logging.info("Starting the web server")
456
-
457
- # Avoids clutering
458
- logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING)
459
-
460
- self.thread = threading.Thread(target=start_app, args=(self,), daemon=True)
461
- self.thread.start()
462
-
463
- # Wait until we really started
464
- while True:
465
- with self.cv_running:
466
- if self.running:
467
- break