experimaestro 2.0.0b8__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.
- experimaestro/__init__.py +12 -5
- experimaestro/cli/__init__.py +239 -126
- experimaestro/cli/filter.py +48 -23
- experimaestro/cli/jobs.py +253 -71
- experimaestro/cli/refactor.py +1 -2
- experimaestro/commandline.py +7 -4
- experimaestro/connectors/__init__.py +9 -1
- experimaestro/connectors/local.py +43 -3
- experimaestro/core/arguments.py +18 -18
- experimaestro/core/identifier.py +11 -11
- experimaestro/core/objects/config.py +96 -39
- experimaestro/core/objects/config_walk.py +3 -3
- experimaestro/core/{subparameters.py → partial.py} +16 -16
- experimaestro/core/partial_lock.py +394 -0
- experimaestro/core/types.py +12 -15
- experimaestro/dynamic.py +290 -0
- experimaestro/experiments/__init__.py +6 -2
- experimaestro/experiments/cli.py +217 -50
- experimaestro/experiments/configuration.py +24 -0
- experimaestro/generators.py +5 -5
- experimaestro/ipc.py +118 -1
- experimaestro/launcherfinder/__init__.py +2 -2
- experimaestro/launcherfinder/registry.py +6 -7
- experimaestro/launcherfinder/specs.py +2 -9
- experimaestro/launchers/slurm/__init__.py +2 -2
- experimaestro/launchers/slurm/base.py +62 -0
- experimaestro/locking.py +957 -1
- experimaestro/notifications.py +89 -201
- experimaestro/progress.py +63 -366
- experimaestro/rpyc.py +0 -2
- experimaestro/run.py +29 -2
- experimaestro/scheduler/__init__.py +8 -1
- experimaestro/scheduler/base.py +629 -53
- experimaestro/scheduler/dependencies.py +20 -16
- experimaestro/scheduler/experiment.py +732 -167
- experimaestro/scheduler/interfaces.py +316 -101
- experimaestro/scheduler/jobs.py +58 -20
- experimaestro/scheduler/remote/adaptive_sync.py +265 -0
- experimaestro/scheduler/remote/client.py +171 -117
- experimaestro/scheduler/remote/protocol.py +8 -193
- experimaestro/scheduler/remote/server.py +95 -71
- experimaestro/scheduler/services.py +53 -28
- experimaestro/scheduler/state_provider.py +663 -2430
- experimaestro/scheduler/state_status.py +1247 -0
- experimaestro/scheduler/transient.py +31 -0
- experimaestro/scheduler/workspace.py +1 -1
- experimaestro/scheduler/workspace_state_provider.py +1273 -0
- experimaestro/scriptbuilder.py +4 -4
- experimaestro/settings.py +36 -0
- experimaestro/tests/conftest.py +33 -5
- experimaestro/tests/connectors/bin/executable.py +1 -1
- experimaestro/tests/fixtures/pre_experiment/experiment_check_env.py +16 -0
- experimaestro/tests/fixtures/pre_experiment/experiment_check_mock.py +14 -0
- experimaestro/tests/fixtures/pre_experiment/experiment_simple.py +12 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_env.py +5 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_error.py +3 -0
- experimaestro/tests/fixtures/pre_experiment/pre_setup_mock.py +8 -0
- experimaestro/tests/launchers/bin/test.py +1 -0
- experimaestro/tests/launchers/test_slurm.py +9 -9
- experimaestro/tests/partial_reschedule.py +46 -0
- experimaestro/tests/restart.py +3 -3
- experimaestro/tests/restart_main.py +1 -0
- experimaestro/tests/scripts/notifyandwait.py +1 -0
- experimaestro/tests/task_partial.py +38 -0
- experimaestro/tests/task_tokens.py +2 -2
- experimaestro/tests/tasks/test_dynamic.py +6 -6
- experimaestro/tests/test_dependencies.py +3 -3
- experimaestro/tests/test_deprecated.py +15 -15
- experimaestro/tests/test_dynamic_locking.py +317 -0
- experimaestro/tests/test_environment.py +24 -14
- experimaestro/tests/test_experiment.py +171 -36
- experimaestro/tests/test_identifier.py +25 -25
- experimaestro/tests/test_identifier_stability.py +3 -5
- experimaestro/tests/test_multitoken.py +2 -4
- experimaestro/tests/{test_subparameters.py → test_partial.py} +25 -25
- experimaestro/tests/test_partial_paths.py +81 -138
- experimaestro/tests/test_pre_experiment.py +219 -0
- experimaestro/tests/test_progress.py +2 -8
- experimaestro/tests/test_remote_state.py +560 -99
- experimaestro/tests/test_stray_jobs.py +261 -0
- experimaestro/tests/test_tasks.py +1 -2
- experimaestro/tests/test_token_locking.py +52 -67
- experimaestro/tests/test_tokens.py +5 -6
- experimaestro/tests/test_transient.py +225 -0
- experimaestro/tests/test_workspace_state_provider.py +768 -0
- experimaestro/tests/token_reschedule.py +1 -3
- experimaestro/tests/utils.py +2 -7
- experimaestro/tokens.py +227 -372
- experimaestro/tools/diff.py +1 -0
- experimaestro/tools/documentation.py +4 -5
- experimaestro/tools/jobs.py +1 -2
- experimaestro/tui/app.py +438 -1966
- experimaestro/tui/app.tcss +162 -0
- experimaestro/tui/dialogs.py +172 -0
- experimaestro/tui/log_viewer.py +253 -3
- experimaestro/tui/messages.py +137 -0
- experimaestro/tui/utils.py +54 -0
- experimaestro/tui/widgets/__init__.py +23 -0
- experimaestro/tui/widgets/experiments.py +468 -0
- experimaestro/tui/widgets/global_services.py +238 -0
- experimaestro/tui/widgets/jobs.py +972 -0
- experimaestro/tui/widgets/log.py +156 -0
- experimaestro/tui/widgets/orphans.py +363 -0
- experimaestro/tui/widgets/runs.py +185 -0
- experimaestro/tui/widgets/services.py +314 -0
- experimaestro/tui/widgets/stray_jobs.py +528 -0
- experimaestro/utils/__init__.py +1 -1
- experimaestro/utils/environment.py +105 -22
- experimaestro/utils/fswatcher.py +124 -0
- experimaestro/utils/jobs.py +1 -2
- experimaestro/utils/jupyter.py +1 -2
- experimaestro/utils/logging.py +72 -0
- experimaestro/version.py +2 -2
- experimaestro/webui/__init__.py +9 -0
- experimaestro/webui/app.py +117 -0
- experimaestro/{server → webui}/data/index.css +66 -11
- experimaestro/webui/data/index.css.map +1 -0
- experimaestro/{server → webui}/data/index.js +82763 -87217
- experimaestro/webui/data/index.js.map +1 -0
- experimaestro/webui/routes/__init__.py +5 -0
- experimaestro/webui/routes/auth.py +53 -0
- experimaestro/webui/routes/proxy.py +117 -0
- experimaestro/webui/server.py +200 -0
- experimaestro/webui/state_bridge.py +152 -0
- experimaestro/webui/websocket.py +413 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/METADATA +5 -6
- experimaestro-2.0.0b17.dist-info/RECORD +219 -0
- experimaestro/cli/progress.py +0 -269
- experimaestro/scheduler/state.py +0 -75
- experimaestro/scheduler/state_db.py +0 -437
- experimaestro/scheduler/state_sync.py +0 -891
- experimaestro/server/__init__.py +0 -467
- experimaestro/server/data/index.css.map +0 -1
- experimaestro/server/data/index.js.map +0 -1
- experimaestro/tests/test_cli_jobs.py +0 -615
- experimaestro/tests/test_file_progress.py +0 -425
- experimaestro/tests/test_file_progress_integration.py +0 -477
- experimaestro/tests/test_state_db.py +0 -434
- experimaestro-2.0.0b8.dist-info/RECORD +0 -187
- /experimaestro/{server → webui}/data/1815e00441357e01619e.ttf +0 -0
- /experimaestro/{server → webui}/data/2463b90d9a316e4e5294.woff2 +0 -0
- /experimaestro/{server → webui}/data/2582b0e4bcf85eceead0.ttf +0 -0
- /experimaestro/{server → webui}/data/89999bdf5d835c012025.woff2 +0 -0
- /experimaestro/{server → webui}/data/914997e1bdfc990d0897.ttf +0 -0
- /experimaestro/{server → webui}/data/c210719e60948b211a12.woff2 +0 -0
- /experimaestro/{server → webui}/data/favicon.ico +0 -0
- /experimaestro/{server → webui}/data/index.html +0 -0
- /experimaestro/{server → webui}/data/login.html +0 -0
- /experimaestro/{server → webui}/data/manifest.json +0 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/WHEEL +0 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/entry_points.txt +0 -0
- {experimaestro-2.0.0b8.dist-info → experimaestro-2.0.0b17.dist-info}/licenses/LICENSE +0 -0
experimaestro/server/__init__.py
DELETED
|
@@ -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
|