fastled 1.2.95__py3-none-any.whl → 1.2.97__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.
- fastled/__init__.py +1 -1
- fastled/client_server.py +513 -513
- fastled/compile_server_impl.py +355 -355
- fastled/docker_manager.py +987 -987
- fastled/open_browser.py +137 -137
- fastled/print_filter.py +190 -190
- fastled/project_init.py +129 -129
- fastled/server_flask.py +3 -9
- fastled/site/build.py +449 -449
- fastled/string_diff.py +82 -82
- {fastled-1.2.95.dist-info → fastled-1.2.97.dist-info}/METADATA +471 -471
- {fastled-1.2.95.dist-info → fastled-1.2.97.dist-info}/RECORD +16 -16
- {fastled-1.2.95.dist-info → fastled-1.2.97.dist-info}/WHEEL +0 -0
- {fastled-1.2.95.dist-info → fastled-1.2.97.dist-info}/entry_points.txt +0 -0
- {fastled-1.2.95.dist-info → fastled-1.2.97.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.2.95.dist-info → fastled-1.2.97.dist-info}/top_level.txt +0 -0
fastled/compile_server_impl.py
CHANGED
@@ -1,355 +1,355 @@
|
|
1
|
-
import subprocess
|
2
|
-
import sys
|
3
|
-
import time
|
4
|
-
import traceback
|
5
|
-
import warnings
|
6
|
-
from datetime import datetime, timezone
|
7
|
-
from pathlib import Path
|
8
|
-
|
9
|
-
import httpx
|
10
|
-
|
11
|
-
from fastled.docker_manager import (
|
12
|
-
DISK_CACHE,
|
13
|
-
Container,
|
14
|
-
DockerManager,
|
15
|
-
RunningContainer,
|
16
|
-
Volume,
|
17
|
-
)
|
18
|
-
from fastled.settings import DEFAULT_CONTAINER_NAME, IMAGE_NAME, SERVER_PORT
|
19
|
-
from fastled.sketch import looks_like_fastled_repo
|
20
|
-
from fastled.types import BuildMode, CompileResult, CompileServerError, FileResponse
|
21
|
-
from fastled.util import port_is_free
|
22
|
-
|
23
|
-
SERVER_OPTIONS = [
|
24
|
-
"--allow-shutdown", # Allow the server to be shut down without a force kill.
|
25
|
-
"--no-auto-update", # Don't auto live updates from the git repo.
|
26
|
-
]
|
27
|
-
|
28
|
-
LOCAL_DOCKER_ENV = {
|
29
|
-
"ONLY_QUICK_BUILDS": "0" # When running docker always allow release and debug.
|
30
|
-
}
|
31
|
-
|
32
|
-
|
33
|
-
def _try_get_fastled_src(path: Path) -> Path | None:
|
34
|
-
fastled_src_dir: Path | None = None
|
35
|
-
if looks_like_fastled_repo(path):
|
36
|
-
print(
|
37
|
-
"Looks like a FastLED repo, using it as the source directory and mapping it into the server."
|
38
|
-
)
|
39
|
-
fastled_src_dir = path / "src"
|
40
|
-
return fastled_src_dir
|
41
|
-
return None
|
42
|
-
|
43
|
-
|
44
|
-
class CompileServerImpl:
|
45
|
-
def __init__(
|
46
|
-
self,
|
47
|
-
interactive: bool = False,
|
48
|
-
auto_updates: bool | None = None,
|
49
|
-
mapped_dir: Path | None = None,
|
50
|
-
auto_start: bool = True,
|
51
|
-
container_name: str | None = None,
|
52
|
-
remove_previous: bool = False,
|
53
|
-
) -> None:
|
54
|
-
container_name = container_name or DEFAULT_CONTAINER_NAME
|
55
|
-
if interactive and not mapped_dir:
|
56
|
-
raise ValueError(
|
57
|
-
"Interactive mode requires a mapped directory point to a sketch"
|
58
|
-
)
|
59
|
-
if not interactive and mapped_dir:
|
60
|
-
warnings.warn(
|
61
|
-
f"Mapped directory {mapped_dir} is ignored in non-interactive mode"
|
62
|
-
)
|
63
|
-
self.container_name = container_name
|
64
|
-
self.mapped_dir = mapped_dir
|
65
|
-
self.docker = DockerManager()
|
66
|
-
self.fastled_src_dir: Path | None = _try_get_fastled_src(Path(".").resolve())
|
67
|
-
self.interactive = interactive
|
68
|
-
self.running_container: RunningContainer | None = None
|
69
|
-
self.auto_updates = auto_updates
|
70
|
-
self.remove_previous = remove_previous
|
71
|
-
self._port = 0 # 0 until compile server is started
|
72
|
-
if auto_start:
|
73
|
-
self.start()
|
74
|
-
|
75
|
-
def start(self, wait_for_startup=True) -> None:
|
76
|
-
if not DockerManager.is_docker_installed():
|
77
|
-
raise CompileServerError("Docker is not installed")
|
78
|
-
if self._port != 0:
|
79
|
-
warnings.warn("Server has already been started")
|
80
|
-
self._port = self._start()
|
81
|
-
if wait_for_startup:
|
82
|
-
ok = self.wait_for_startup()
|
83
|
-
if not ok:
|
84
|
-
if self.interactive:
|
85
|
-
print("Exited from container.")
|
86
|
-
sys.exit(0)
|
87
|
-
return
|
88
|
-
raise CompileServerError("Server did not start")
|
89
|
-
if not self.interactive:
|
90
|
-
msg = f"# FastLED Compile Server started at {self.url()} #"
|
91
|
-
print("\n" + "#" * len(msg))
|
92
|
-
print(msg)
|
93
|
-
print("#" * len(msg) + "\n")
|
94
|
-
|
95
|
-
def web_compile(
|
96
|
-
self,
|
97
|
-
directory: Path | str,
|
98
|
-
build_mode: BuildMode = BuildMode.QUICK,
|
99
|
-
profile: bool = False,
|
100
|
-
) -> CompileResult:
|
101
|
-
from fastled.web_compile import web_compile # avoid circular import
|
102
|
-
|
103
|
-
if not self._port:
|
104
|
-
raise RuntimeError("Server has not been started yet")
|
105
|
-
if not self.ping():
|
106
|
-
raise RuntimeError("Server is not running")
|
107
|
-
out: CompileResult = web_compile(
|
108
|
-
directory, host=self.url(), build_mode=build_mode, profile=profile
|
109
|
-
)
|
110
|
-
return out
|
111
|
-
|
112
|
-
def project_init(
|
113
|
-
self, example: str | None = None, outputdir: Path | None = None
|
114
|
-
) -> None:
|
115
|
-
from fastled.project_init import project_init # avoid circular import
|
116
|
-
|
117
|
-
project_init(example=example, outputdir=outputdir)
|
118
|
-
|
119
|
-
@property
|
120
|
-
def running(self) -> tuple[bool, Exception | None]:
|
121
|
-
if not self._port:
|
122
|
-
return False, Exception("Docker hasn't been initialzed with a port yet")
|
123
|
-
if not DockerManager.is_docker_installed():
|
124
|
-
return False, Exception("Docker is not installed")
|
125
|
-
docker_running, err = self.docker.is_running()
|
126
|
-
if not docker_running:
|
127
|
-
IS_MAC = sys.platform == "darwin"
|
128
|
-
if IS_MAC:
|
129
|
-
if "FileNotFoundError" in str(err):
|
130
|
-
traceback.print_exc()
|
131
|
-
print("\n\nNone fatal debug print for MacOS\n")
|
132
|
-
return False, err
|
133
|
-
ok: bool = self.docker.is_container_running(self.container_name)
|
134
|
-
if ok:
|
135
|
-
return True, None
|
136
|
-
else:
|
137
|
-
return False, Exception("Docker is not running")
|
138
|
-
|
139
|
-
def using_fastled_src_dir_volume(self) -> bool:
|
140
|
-
out = self.fastled_src_dir is not None
|
141
|
-
if out:
|
142
|
-
print(f"Using FastLED source directory: {self.fastled_src_dir}")
|
143
|
-
return out
|
144
|
-
|
145
|
-
def port(self) -> int:
|
146
|
-
if self._port == 0:
|
147
|
-
warnings.warn("Server has not been started yet")
|
148
|
-
return self._port
|
149
|
-
|
150
|
-
def url(self) -> str:
|
151
|
-
if self._port == 0:
|
152
|
-
warnings.warn("Server has not been started yet")
|
153
|
-
return f"http://localhost:{self._port}"
|
154
|
-
|
155
|
-
def ping(self) -> bool:
|
156
|
-
try:
|
157
|
-
response = httpx.get(
|
158
|
-
f"http://localhost:{self._port}", follow_redirects=True
|
159
|
-
)
|
160
|
-
if response.status_code < 400:
|
161
|
-
return True
|
162
|
-
except KeyboardInterrupt:
|
163
|
-
raise
|
164
|
-
except Exception:
|
165
|
-
pass
|
166
|
-
return False
|
167
|
-
|
168
|
-
# by default this is automatically called by the constructor, unless
|
169
|
-
# auto_start is set to False.
|
170
|
-
def wait_for_startup(self, timeout: int = 100) -> bool:
|
171
|
-
"""Wait for the server to start up."""
|
172
|
-
start_time = time.time()
|
173
|
-
while time.time() - start_time < timeout:
|
174
|
-
# ping the server to see if it's up
|
175
|
-
if not self._port:
|
176
|
-
return False
|
177
|
-
# use httpx to ping the server
|
178
|
-
# if successful, return True
|
179
|
-
if self.ping():
|
180
|
-
return True
|
181
|
-
time.sleep(0.1)
|
182
|
-
if not self.docker.is_container_running(self.container_name):
|
183
|
-
return False
|
184
|
-
return False
|
185
|
-
|
186
|
-
def fetch_source_file(self, filepath: str) -> FileResponse | Exception:
|
187
|
-
"""Get the source file from the server."""
|
188
|
-
if not self._port:
|
189
|
-
raise RuntimeError("Server has not been started yet")
|
190
|
-
try:
|
191
|
-
httpx_client = httpx.Client()
|
192
|
-
url = f"http://localhost:{self._port}/sourcefiles/{filepath}"
|
193
|
-
response = httpx_client.get(url, follow_redirects=True)
|
194
|
-
if response.status_code == 200:
|
195
|
-
content = response.text
|
196
|
-
mimetype: str = response.headers.get("Content-Type", "text/plain")
|
197
|
-
return FileResponse(
|
198
|
-
content=content,
|
199
|
-
mimetype=mimetype,
|
200
|
-
filename=filepath,
|
201
|
-
)
|
202
|
-
else:
|
203
|
-
return CompileServerError(
|
204
|
-
f"Error fetching file {filepath}: {response.status_code}"
|
205
|
-
)
|
206
|
-
except httpx.RequestError as e:
|
207
|
-
return CompileServerError(f"Error fetching file {filepath}: {e}")
|
208
|
-
|
209
|
-
def _start(self) -> int:
|
210
|
-
print("Compiling server starting")
|
211
|
-
|
212
|
-
# Ensure Docker is running
|
213
|
-
if not self.docker.is_running():
|
214
|
-
if not self.docker.start():
|
215
|
-
print("Docker could not be started. Exiting.")
|
216
|
-
raise RuntimeError("Docker could not be started. Exiting.")
|
217
|
-
now = datetime.now(timezone.utc)
|
218
|
-
now_str = now.strftime("%Y-%m-%d")
|
219
|
-
|
220
|
-
upgrade = False
|
221
|
-
if self.auto_updates is None:
|
222
|
-
prev_date_str = DISK_CACHE.get("last-update")
|
223
|
-
if prev_date_str != now_str:
|
224
|
-
print("One day has passed, checking docker for updates")
|
225
|
-
upgrade = True
|
226
|
-
else:
|
227
|
-
upgrade = self.auto_updates
|
228
|
-
updated = self.docker.validate_or_download_image(
|
229
|
-
image_name=IMAGE_NAME, tag="latest", upgrade=upgrade
|
230
|
-
)
|
231
|
-
DISK_CACHE.put("last-update", now_str)
|
232
|
-
INTERNAL_DOCKER_PORT = 80
|
233
|
-
|
234
|
-
print("Docker image now validated")
|
235
|
-
port = SERVER_PORT
|
236
|
-
if self.interactive:
|
237
|
-
server_command = ["/bin/bash"]
|
238
|
-
else:
|
239
|
-
server_command = ["python", "/js/run.py", "server"] + SERVER_OPTIONS
|
240
|
-
if self.interactive:
|
241
|
-
print("Disabling port forwarding in interactive mode")
|
242
|
-
ports = {}
|
243
|
-
else:
|
244
|
-
ports = {INTERNAL_DOCKER_PORT: port}
|
245
|
-
volumes = []
|
246
|
-
if self.fastled_src_dir:
|
247
|
-
print(
|
248
|
-
f"Mounting FastLED source directory {self.fastled_src_dir} into container /host/fastled/src"
|
249
|
-
)
|
250
|
-
volumes.append(
|
251
|
-
Volume(
|
252
|
-
host_path=str(self.fastled_src_dir),
|
253
|
-
container_path="/host/fastled/src",
|
254
|
-
mode="ro",
|
255
|
-
)
|
256
|
-
)
|
257
|
-
if self.interactive:
|
258
|
-
# add the mapped directory to the container
|
259
|
-
print(f"Mounting {self.mapped_dir} into container /mapped")
|
260
|
-
assert self.mapped_dir is not None
|
261
|
-
dir_name = self.mapped_dir.name
|
262
|
-
if not volumes:
|
263
|
-
volumes = []
|
264
|
-
volumes.append(
|
265
|
-
Volume(
|
266
|
-
host_path=str(self.mapped_dir),
|
267
|
-
container_path=f"/mapped/{dir_name}",
|
268
|
-
mode="rw",
|
269
|
-
)
|
270
|
-
)
|
271
|
-
if self.fastled_src_dir is not None:
|
272
|
-
# to allow for interactive compilation
|
273
|
-
# interactive_sources = list(INTERACTIVE_SOURCES)
|
274
|
-
interactive_sources: list[tuple[Path, str]] = []
|
275
|
-
init_runtime_py = (
|
276
|
-
Path(self.fastled_src_dir)
|
277
|
-
/ ".."
|
278
|
-
/ ".."
|
279
|
-
/ "fastled-wasm"
|
280
|
-
/ "compiler"
|
281
|
-
/ "init_runtime.py"
|
282
|
-
)
|
283
|
-
if init_runtime_py.exists():
|
284
|
-
# fastled-wasm is in a sister directory, mapping this in to the container.
|
285
|
-
mapping = (
|
286
|
-
init_runtime_py,
|
287
|
-
"/js/init_runtime.py",
|
288
|
-
)
|
289
|
-
interactive_sources.append(mapping)
|
290
|
-
|
291
|
-
src_host: Path
|
292
|
-
dst_container: str
|
293
|
-
for src_host, dst_container in interactive_sources:
|
294
|
-
src_path = Path(src_host).absolute()
|
295
|
-
if src_path.exists():
|
296
|
-
print(f"Mounting {src_host} into container")
|
297
|
-
volumes.append(
|
298
|
-
Volume(
|
299
|
-
host_path=str(src_path),
|
300
|
-
container_path=dst_container,
|
301
|
-
mode="rw",
|
302
|
-
)
|
303
|
-
)
|
304
|
-
else:
|
305
|
-
print(f"Could not find {src_path}")
|
306
|
-
|
307
|
-
cmd_str = subprocess.list2cmdline(server_command)
|
308
|
-
if not self.interactive:
|
309
|
-
container: Container = self.docker.run_container_detached(
|
310
|
-
image_name=IMAGE_NAME,
|
311
|
-
tag="latest",
|
312
|
-
container_name=self.container_name,
|
313
|
-
command=cmd_str,
|
314
|
-
ports=ports,
|
315
|
-
volumes=volumes,
|
316
|
-
remove_previous=self.interactive or self.remove_previous or updated,
|
317
|
-
environment=LOCAL_DOCKER_ENV,
|
318
|
-
)
|
319
|
-
self.running_container = self.docker.attach_and_run(container)
|
320
|
-
assert self.running_container is not None, "Container should be running"
|
321
|
-
print("Compile server starting")
|
322
|
-
return port
|
323
|
-
else:
|
324
|
-
client_port_mapped = INTERNAL_DOCKER_PORT in ports
|
325
|
-
free_port = port_is_free(INTERNAL_DOCKER_PORT)
|
326
|
-
if client_port_mapped and free_port:
|
327
|
-
warnings.warn(
|
328
|
-
f"Can't expose port {INTERNAL_DOCKER_PORT}, disabling port forwarding in interactive mode"
|
329
|
-
)
|
330
|
-
ports = {}
|
331
|
-
self.docker.run_container_interactive(
|
332
|
-
image_name=IMAGE_NAME,
|
333
|
-
tag="latest",
|
334
|
-
container_name=self.container_name,
|
335
|
-
command=cmd_str,
|
336
|
-
ports=ports,
|
337
|
-
volumes=volumes,
|
338
|
-
environment=LOCAL_DOCKER_ENV,
|
339
|
-
)
|
340
|
-
|
341
|
-
print("Exiting interactive mode")
|
342
|
-
return port
|
343
|
-
|
344
|
-
def process_running(self) -> bool:
|
345
|
-
return self.docker.is_container_running(self.container_name)
|
346
|
-
|
347
|
-
def stop(self) -> None:
|
348
|
-
if self.docker.is_suspended:
|
349
|
-
return
|
350
|
-
if self.running_container:
|
351
|
-
self.running_container.detach()
|
352
|
-
self.running_container = None
|
353
|
-
self.docker.suspend_container(self.container_name)
|
354
|
-
self._port = 0
|
355
|
-
print("Compile server stopped")
|
1
|
+
import subprocess
|
2
|
+
import sys
|
3
|
+
import time
|
4
|
+
import traceback
|
5
|
+
import warnings
|
6
|
+
from datetime import datetime, timezone
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
import httpx
|
10
|
+
|
11
|
+
from fastled.docker_manager import (
|
12
|
+
DISK_CACHE,
|
13
|
+
Container,
|
14
|
+
DockerManager,
|
15
|
+
RunningContainer,
|
16
|
+
Volume,
|
17
|
+
)
|
18
|
+
from fastled.settings import DEFAULT_CONTAINER_NAME, IMAGE_NAME, SERVER_PORT
|
19
|
+
from fastled.sketch import looks_like_fastled_repo
|
20
|
+
from fastled.types import BuildMode, CompileResult, CompileServerError, FileResponse
|
21
|
+
from fastled.util import port_is_free
|
22
|
+
|
23
|
+
SERVER_OPTIONS = [
|
24
|
+
"--allow-shutdown", # Allow the server to be shut down without a force kill.
|
25
|
+
"--no-auto-update", # Don't auto live updates from the git repo.
|
26
|
+
]
|
27
|
+
|
28
|
+
LOCAL_DOCKER_ENV = {
|
29
|
+
"ONLY_QUICK_BUILDS": "0" # When running docker always allow release and debug.
|
30
|
+
}
|
31
|
+
|
32
|
+
|
33
|
+
def _try_get_fastled_src(path: Path) -> Path | None:
|
34
|
+
fastled_src_dir: Path | None = None
|
35
|
+
if looks_like_fastled_repo(path):
|
36
|
+
print(
|
37
|
+
"Looks like a FastLED repo, using it as the source directory and mapping it into the server."
|
38
|
+
)
|
39
|
+
fastled_src_dir = path / "src"
|
40
|
+
return fastled_src_dir
|
41
|
+
return None
|
42
|
+
|
43
|
+
|
44
|
+
class CompileServerImpl:
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
interactive: bool = False,
|
48
|
+
auto_updates: bool | None = None,
|
49
|
+
mapped_dir: Path | None = None,
|
50
|
+
auto_start: bool = True,
|
51
|
+
container_name: str | None = None,
|
52
|
+
remove_previous: bool = False,
|
53
|
+
) -> None:
|
54
|
+
container_name = container_name or DEFAULT_CONTAINER_NAME
|
55
|
+
if interactive and not mapped_dir:
|
56
|
+
raise ValueError(
|
57
|
+
"Interactive mode requires a mapped directory point to a sketch"
|
58
|
+
)
|
59
|
+
if not interactive and mapped_dir:
|
60
|
+
warnings.warn(
|
61
|
+
f"Mapped directory {mapped_dir} is ignored in non-interactive mode"
|
62
|
+
)
|
63
|
+
self.container_name = container_name
|
64
|
+
self.mapped_dir = mapped_dir
|
65
|
+
self.docker = DockerManager()
|
66
|
+
self.fastled_src_dir: Path | None = _try_get_fastled_src(Path(".").resolve())
|
67
|
+
self.interactive = interactive
|
68
|
+
self.running_container: RunningContainer | None = None
|
69
|
+
self.auto_updates = auto_updates
|
70
|
+
self.remove_previous = remove_previous
|
71
|
+
self._port = 0 # 0 until compile server is started
|
72
|
+
if auto_start:
|
73
|
+
self.start()
|
74
|
+
|
75
|
+
def start(self, wait_for_startup=True) -> None:
|
76
|
+
if not DockerManager.is_docker_installed():
|
77
|
+
raise CompileServerError("Docker is not installed")
|
78
|
+
if self._port != 0:
|
79
|
+
warnings.warn("Server has already been started")
|
80
|
+
self._port = self._start()
|
81
|
+
if wait_for_startup:
|
82
|
+
ok = self.wait_for_startup()
|
83
|
+
if not ok:
|
84
|
+
if self.interactive:
|
85
|
+
print("Exited from container.")
|
86
|
+
sys.exit(0)
|
87
|
+
return
|
88
|
+
raise CompileServerError("Server did not start")
|
89
|
+
if not self.interactive:
|
90
|
+
msg = f"# FastLED Compile Server started at {self.url()} #"
|
91
|
+
print("\n" + "#" * len(msg))
|
92
|
+
print(msg)
|
93
|
+
print("#" * len(msg) + "\n")
|
94
|
+
|
95
|
+
def web_compile(
|
96
|
+
self,
|
97
|
+
directory: Path | str,
|
98
|
+
build_mode: BuildMode = BuildMode.QUICK,
|
99
|
+
profile: bool = False,
|
100
|
+
) -> CompileResult:
|
101
|
+
from fastled.web_compile import web_compile # avoid circular import
|
102
|
+
|
103
|
+
if not self._port:
|
104
|
+
raise RuntimeError("Server has not been started yet")
|
105
|
+
if not self.ping():
|
106
|
+
raise RuntimeError("Server is not running")
|
107
|
+
out: CompileResult = web_compile(
|
108
|
+
directory, host=self.url(), build_mode=build_mode, profile=profile
|
109
|
+
)
|
110
|
+
return out
|
111
|
+
|
112
|
+
def project_init(
|
113
|
+
self, example: str | None = None, outputdir: Path | None = None
|
114
|
+
) -> None:
|
115
|
+
from fastled.project_init import project_init # avoid circular import
|
116
|
+
|
117
|
+
project_init(example=example, outputdir=outputdir)
|
118
|
+
|
119
|
+
@property
|
120
|
+
def running(self) -> tuple[bool, Exception | None]:
|
121
|
+
if not self._port:
|
122
|
+
return False, Exception("Docker hasn't been initialzed with a port yet")
|
123
|
+
if not DockerManager.is_docker_installed():
|
124
|
+
return False, Exception("Docker is not installed")
|
125
|
+
docker_running, err = self.docker.is_running()
|
126
|
+
if not docker_running:
|
127
|
+
IS_MAC = sys.platform == "darwin"
|
128
|
+
if IS_MAC:
|
129
|
+
if "FileNotFoundError" in str(err):
|
130
|
+
traceback.print_exc()
|
131
|
+
print("\n\nNone fatal debug print for MacOS\n")
|
132
|
+
return False, err
|
133
|
+
ok: bool = self.docker.is_container_running(self.container_name)
|
134
|
+
if ok:
|
135
|
+
return True, None
|
136
|
+
else:
|
137
|
+
return False, Exception("Docker is not running")
|
138
|
+
|
139
|
+
def using_fastled_src_dir_volume(self) -> bool:
|
140
|
+
out = self.fastled_src_dir is not None
|
141
|
+
if out:
|
142
|
+
print(f"Using FastLED source directory: {self.fastled_src_dir}")
|
143
|
+
return out
|
144
|
+
|
145
|
+
def port(self) -> int:
|
146
|
+
if self._port == 0:
|
147
|
+
warnings.warn("Server has not been started yet")
|
148
|
+
return self._port
|
149
|
+
|
150
|
+
def url(self) -> str:
|
151
|
+
if self._port == 0:
|
152
|
+
warnings.warn("Server has not been started yet")
|
153
|
+
return f"http://localhost:{self._port}"
|
154
|
+
|
155
|
+
def ping(self) -> bool:
|
156
|
+
try:
|
157
|
+
response = httpx.get(
|
158
|
+
f"http://localhost:{self._port}", follow_redirects=True
|
159
|
+
)
|
160
|
+
if response.status_code < 400:
|
161
|
+
return True
|
162
|
+
except KeyboardInterrupt:
|
163
|
+
raise
|
164
|
+
except Exception:
|
165
|
+
pass
|
166
|
+
return False
|
167
|
+
|
168
|
+
# by default this is automatically called by the constructor, unless
|
169
|
+
# auto_start is set to False.
|
170
|
+
def wait_for_startup(self, timeout: int = 100) -> bool:
|
171
|
+
"""Wait for the server to start up."""
|
172
|
+
start_time = time.time()
|
173
|
+
while time.time() - start_time < timeout:
|
174
|
+
# ping the server to see if it's up
|
175
|
+
if not self._port:
|
176
|
+
return False
|
177
|
+
# use httpx to ping the server
|
178
|
+
# if successful, return True
|
179
|
+
if self.ping():
|
180
|
+
return True
|
181
|
+
time.sleep(0.1)
|
182
|
+
if not self.docker.is_container_running(self.container_name):
|
183
|
+
return False
|
184
|
+
return False
|
185
|
+
|
186
|
+
def fetch_source_file(self, filepath: str) -> FileResponse | Exception:
|
187
|
+
"""Get the source file from the server."""
|
188
|
+
if not self._port:
|
189
|
+
raise RuntimeError("Server has not been started yet")
|
190
|
+
try:
|
191
|
+
httpx_client = httpx.Client()
|
192
|
+
url = f"http://localhost:{self._port}/sourcefiles/{filepath}"
|
193
|
+
response = httpx_client.get(url, follow_redirects=True)
|
194
|
+
if response.status_code == 200:
|
195
|
+
content = response.text
|
196
|
+
mimetype: str = response.headers.get("Content-Type", "text/plain")
|
197
|
+
return FileResponse(
|
198
|
+
content=content,
|
199
|
+
mimetype=mimetype,
|
200
|
+
filename=filepath,
|
201
|
+
)
|
202
|
+
else:
|
203
|
+
return CompileServerError(
|
204
|
+
f"Error fetching file {filepath}: {response.status_code}"
|
205
|
+
)
|
206
|
+
except httpx.RequestError as e:
|
207
|
+
return CompileServerError(f"Error fetching file {filepath}: {e}")
|
208
|
+
|
209
|
+
def _start(self) -> int:
|
210
|
+
print("Compiling server starting")
|
211
|
+
|
212
|
+
# Ensure Docker is running
|
213
|
+
if not self.docker.is_running():
|
214
|
+
if not self.docker.start():
|
215
|
+
print("Docker could not be started. Exiting.")
|
216
|
+
raise RuntimeError("Docker could not be started. Exiting.")
|
217
|
+
now = datetime.now(timezone.utc)
|
218
|
+
now_str = now.strftime("%Y-%m-%d")
|
219
|
+
|
220
|
+
upgrade = False
|
221
|
+
if self.auto_updates is None:
|
222
|
+
prev_date_str = DISK_CACHE.get("last-update")
|
223
|
+
if prev_date_str != now_str:
|
224
|
+
print("One day has passed, checking docker for updates")
|
225
|
+
upgrade = True
|
226
|
+
else:
|
227
|
+
upgrade = self.auto_updates
|
228
|
+
updated = self.docker.validate_or_download_image(
|
229
|
+
image_name=IMAGE_NAME, tag="latest", upgrade=upgrade
|
230
|
+
)
|
231
|
+
DISK_CACHE.put("last-update", now_str)
|
232
|
+
INTERNAL_DOCKER_PORT = 80
|
233
|
+
|
234
|
+
print("Docker image now validated")
|
235
|
+
port = SERVER_PORT
|
236
|
+
if self.interactive:
|
237
|
+
server_command = ["/bin/bash"]
|
238
|
+
else:
|
239
|
+
server_command = ["python", "/js/run.py", "server"] + SERVER_OPTIONS
|
240
|
+
if self.interactive:
|
241
|
+
print("Disabling port forwarding in interactive mode")
|
242
|
+
ports = {}
|
243
|
+
else:
|
244
|
+
ports = {INTERNAL_DOCKER_PORT: port}
|
245
|
+
volumes = []
|
246
|
+
if self.fastled_src_dir:
|
247
|
+
print(
|
248
|
+
f"Mounting FastLED source directory {self.fastled_src_dir} into container /host/fastled/src"
|
249
|
+
)
|
250
|
+
volumes.append(
|
251
|
+
Volume(
|
252
|
+
host_path=str(self.fastled_src_dir),
|
253
|
+
container_path="/host/fastled/src",
|
254
|
+
mode="ro",
|
255
|
+
)
|
256
|
+
)
|
257
|
+
if self.interactive:
|
258
|
+
# add the mapped directory to the container
|
259
|
+
print(f"Mounting {self.mapped_dir} into container /mapped")
|
260
|
+
assert self.mapped_dir is not None
|
261
|
+
dir_name = self.mapped_dir.name
|
262
|
+
if not volumes:
|
263
|
+
volumes = []
|
264
|
+
volumes.append(
|
265
|
+
Volume(
|
266
|
+
host_path=str(self.mapped_dir),
|
267
|
+
container_path=f"/mapped/{dir_name}",
|
268
|
+
mode="rw",
|
269
|
+
)
|
270
|
+
)
|
271
|
+
if self.fastled_src_dir is not None:
|
272
|
+
# to allow for interactive compilation
|
273
|
+
# interactive_sources = list(INTERACTIVE_SOURCES)
|
274
|
+
interactive_sources: list[tuple[Path, str]] = []
|
275
|
+
init_runtime_py = (
|
276
|
+
Path(self.fastled_src_dir)
|
277
|
+
/ ".."
|
278
|
+
/ ".."
|
279
|
+
/ "fastled-wasm"
|
280
|
+
/ "compiler"
|
281
|
+
/ "init_runtime.py"
|
282
|
+
)
|
283
|
+
if init_runtime_py.exists():
|
284
|
+
# fastled-wasm is in a sister directory, mapping this in to the container.
|
285
|
+
mapping = (
|
286
|
+
init_runtime_py,
|
287
|
+
"/js/init_runtime.py",
|
288
|
+
)
|
289
|
+
interactive_sources.append(mapping)
|
290
|
+
|
291
|
+
src_host: Path
|
292
|
+
dst_container: str
|
293
|
+
for src_host, dst_container in interactive_sources:
|
294
|
+
src_path = Path(src_host).absolute()
|
295
|
+
if src_path.exists():
|
296
|
+
print(f"Mounting {src_host} into container")
|
297
|
+
volumes.append(
|
298
|
+
Volume(
|
299
|
+
host_path=str(src_path),
|
300
|
+
container_path=dst_container,
|
301
|
+
mode="rw",
|
302
|
+
)
|
303
|
+
)
|
304
|
+
else:
|
305
|
+
print(f"Could not find {src_path}")
|
306
|
+
|
307
|
+
cmd_str = subprocess.list2cmdline(server_command)
|
308
|
+
if not self.interactive:
|
309
|
+
container: Container = self.docker.run_container_detached(
|
310
|
+
image_name=IMAGE_NAME,
|
311
|
+
tag="latest",
|
312
|
+
container_name=self.container_name,
|
313
|
+
command=cmd_str,
|
314
|
+
ports=ports,
|
315
|
+
volumes=volumes,
|
316
|
+
remove_previous=self.interactive or self.remove_previous or updated,
|
317
|
+
environment=LOCAL_DOCKER_ENV,
|
318
|
+
)
|
319
|
+
self.running_container = self.docker.attach_and_run(container)
|
320
|
+
assert self.running_container is not None, "Container should be running"
|
321
|
+
print("Compile server starting")
|
322
|
+
return port
|
323
|
+
else:
|
324
|
+
client_port_mapped = INTERNAL_DOCKER_PORT in ports
|
325
|
+
free_port = port_is_free(INTERNAL_DOCKER_PORT)
|
326
|
+
if client_port_mapped and free_port:
|
327
|
+
warnings.warn(
|
328
|
+
f"Can't expose port {INTERNAL_DOCKER_PORT}, disabling port forwarding in interactive mode"
|
329
|
+
)
|
330
|
+
ports = {}
|
331
|
+
self.docker.run_container_interactive(
|
332
|
+
image_name=IMAGE_NAME,
|
333
|
+
tag="latest",
|
334
|
+
container_name=self.container_name,
|
335
|
+
command=cmd_str,
|
336
|
+
ports=ports,
|
337
|
+
volumes=volumes,
|
338
|
+
environment=LOCAL_DOCKER_ENV,
|
339
|
+
)
|
340
|
+
|
341
|
+
print("Exiting interactive mode")
|
342
|
+
return port
|
343
|
+
|
344
|
+
def process_running(self) -> bool:
|
345
|
+
return self.docker.is_container_running(self.container_name)
|
346
|
+
|
347
|
+
def stop(self) -> None:
|
348
|
+
if self.docker.is_suspended:
|
349
|
+
return
|
350
|
+
if self.running_container:
|
351
|
+
self.running_container.detach()
|
352
|
+
self.running_container = None
|
353
|
+
self.docker.suspend_container(self.container_name)
|
354
|
+
self._port = 0
|
355
|
+
print("Compile server stopped")
|