fastled 1.1.15__py2.py3-none-any.whl → 1.1.17__py2.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/app.py +26 -15
- fastled/compile_server.py +58 -121
- fastled/docker_manager.py +394 -140
- fastled/web_compile.py +3 -8
- {fastled-1.1.15.dist-info → fastled-1.1.17.dist-info}/METADATA +6 -2
- {fastled-1.1.15.dist-info → fastled-1.1.17.dist-info}/RECORD +11 -11
- {fastled-1.1.15.dist-info → fastled-1.1.17.dist-info}/LICENSE +0 -0
- {fastled-1.1.15.dist-info → fastled-1.1.17.dist-info}/WHEEL +0 -0
- {fastled-1.1.15.dist-info → fastled-1.1.17.dist-info}/entry_points.txt +0 -0
- {fastled-1.1.15.dist-info → fastled-1.1.17.dist-info}/top_level.txt +0 -0
fastled/__init__.py
CHANGED
fastled/app.py
CHANGED
@@ -44,9 +44,6 @@ class CompiledResult:
|
|
44
44
|
hash_value: str | None
|
45
45
|
|
46
46
|
|
47
|
-
DOCKER = DockerManager(container_name=CONTAINER_NAME)
|
48
|
-
|
49
|
-
|
50
47
|
def parse_args() -> argparse.Namespace:
|
51
48
|
"""Parse command-line arguments."""
|
52
49
|
parser = argparse.ArgumentParser(description=f"FastLED WASM Compiler {__version__}")
|
@@ -107,16 +104,32 @@ def parse_args() -> argparse.Namespace:
|
|
107
104
|
action="store_true",
|
108
105
|
help="Run the server in the current directory, volume mapping fastled if we are in the repo",
|
109
106
|
)
|
110
|
-
|
111
|
-
build_mode.add_argument(
|
107
|
+
parser.add_argument(
|
112
108
|
"--force-compile",
|
113
109
|
action="store_true",
|
114
110
|
help="Skips the test to see if the current directory is a valid FastLED sketch directory",
|
115
111
|
)
|
112
|
+
parser.add_argument(
|
113
|
+
"--no-auto-updates",
|
114
|
+
action="store_true",
|
115
|
+
help="Disable automatic updates of the wasm compiler image when using docker.",
|
116
|
+
)
|
117
|
+
parser.add_argument(
|
118
|
+
"--update",
|
119
|
+
action="store_true",
|
120
|
+
help="Update the wasm compiler (if necessary) before running",
|
121
|
+
)
|
116
122
|
|
117
123
|
cwd_is_fastled = looks_like_fastled_repo(Path(os.getcwd()))
|
118
124
|
|
119
125
|
args = parser.parse_args()
|
126
|
+
if args.update:
|
127
|
+
args.auto_update = True
|
128
|
+
elif args.no_auto_updates:
|
129
|
+
args.auto_update = False
|
130
|
+
else:
|
131
|
+
args.auto_update = None
|
132
|
+
|
120
133
|
if not cwd_is_fastled and not args.localhost and not args.web and not args.server:
|
121
134
|
print(f"Using web compiler at {DEFAULT_URL}")
|
122
135
|
args.web = DEFAULT_URL
|
@@ -202,8 +215,8 @@ def run_web_compiler(
|
|
202
215
|
|
203
216
|
|
204
217
|
def _try_start_server_or_get_url(args: argparse.Namespace) -> str | CompileServer:
|
218
|
+
auto_update = args.auto_update
|
205
219
|
is_local_host = "localhost" in args.web or "127.0.0.1" in args.web or args.localhost
|
206
|
-
|
207
220
|
# test to see if there is already a local host server
|
208
221
|
local_host_needs_server = False
|
209
222
|
if is_local_host:
|
@@ -228,7 +241,7 @@ def _try_start_server_or_get_url(args: argparse.Namespace) -> str | CompileServe
|
|
228
241
|
else:
|
229
242
|
try:
|
230
243
|
print("No local server found, starting one...")
|
231
|
-
compile_server = CompileServer()
|
244
|
+
compile_server = CompileServer(auto_updates=auto_update)
|
232
245
|
print("Waiting for the local compiler to start...")
|
233
246
|
if not compile_server.wait_for_startup():
|
234
247
|
print("Failed to start local compiler.")
|
@@ -252,7 +265,7 @@ def run_client(args: argparse.Namespace) -> int:
|
|
252
265
|
return 1
|
253
266
|
|
254
267
|
# If not explicitly using web compiler, check Docker installation
|
255
|
-
if not args.web and not
|
268
|
+
if not args.web and not DockerManager.is_docker_installed():
|
256
269
|
print(
|
257
270
|
"\nDocker is not installed on this system - switching to web compiler instead."
|
258
271
|
)
|
@@ -304,9 +317,7 @@ def run_client(args: argparse.Namespace) -> int:
|
|
304
317
|
if open_web_browser:
|
305
318
|
browser_proc = open_browser_thread(Path(args.directory) / "fastled_js")
|
306
319
|
else:
|
307
|
-
print(
|
308
|
-
"\nCompilation successful. Run without --just-compile to open in browser and watch for changes."
|
309
|
-
)
|
320
|
+
print("\nCompilation successful.")
|
310
321
|
if compile_server:
|
311
322
|
print("Shutting down compile server...")
|
312
323
|
compile_server.stop()
|
@@ -402,7 +413,8 @@ def run_client(args: argparse.Namespace) -> int:
|
|
402
413
|
|
403
414
|
def run_server(args: argparse.Namespace) -> int:
|
404
415
|
interactive = args.interactive
|
405
|
-
|
416
|
+
auto_update = args.auto_update
|
417
|
+
compile_server = CompileServer(interactive=interactive, auto_updates=auto_update)
|
406
418
|
if not interactive:
|
407
419
|
print(f"Server started at {compile_server.url()}")
|
408
420
|
compile_server.wait_for_startup()
|
@@ -432,9 +444,8 @@ def main() -> int:
|
|
432
444
|
|
433
445
|
if __name__ == "__main__":
|
434
446
|
try:
|
435
|
-
|
436
|
-
sys.argv.append("
|
437
|
-
sys.argv.append("--localhost")
|
447
|
+
os.chdir("../fastled")
|
448
|
+
sys.argv.append("--server")
|
438
449
|
sys.exit(main())
|
439
450
|
except KeyboardInterrupt:
|
440
451
|
print("\nExiting from main...")
|
fastled/compile_server.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
import socket
|
2
1
|
import subprocess
|
3
2
|
import time
|
3
|
+
from datetime import datetime, timezone
|
4
4
|
from pathlib import Path
|
5
5
|
|
6
6
|
import httpx
|
7
7
|
|
8
|
-
from fastled.docker_manager import DockerManager
|
8
|
+
from fastled.docker_manager import DISK_CACHE, DockerManager, RunningContainer
|
9
9
|
from fastled.sketch import looks_like_fastled_repo
|
10
10
|
|
11
|
+
_IMAGE_NAME = "niteris/fastled-wasm"
|
11
12
|
_DEFAULT_CONTAINER_NAME = "fastled-wasm-compiler"
|
12
13
|
|
13
14
|
SERVER_PORT = 9021
|
@@ -15,24 +16,12 @@ SERVER_PORT = 9021
|
|
15
16
|
SERVER_OPTIONS = ["--allow-shutdown", "--no-auto-update"]
|
16
17
|
|
17
18
|
|
18
|
-
def find_available_port(start_port: int = SERVER_PORT) -> int:
|
19
|
-
"""Find an available port starting from the given port."""
|
20
|
-
port = start_port
|
21
|
-
end_port = start_port + 1000
|
22
|
-
while True:
|
23
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
24
|
-
if s.connect_ex(("localhost", port)) != 0:
|
25
|
-
return port
|
26
|
-
port += 1
|
27
|
-
if port >= end_port:
|
28
|
-
raise RuntimeError("No available ports found")
|
29
|
-
|
30
|
-
|
31
19
|
class CompileServer:
|
32
20
|
def __init__(
|
33
21
|
self,
|
34
22
|
container_name=_DEFAULT_CONTAINER_NAME,
|
35
23
|
interactive: bool = False,
|
24
|
+
auto_updates: bool | None = None,
|
36
25
|
) -> None:
|
37
26
|
|
38
27
|
cwd = Path(".").resolve()
|
@@ -44,11 +33,11 @@ class CompileServer:
|
|
44
33
|
fastled_src_dir = cwd / "src"
|
45
34
|
|
46
35
|
self.container_name = container_name
|
47
|
-
self.docker = DockerManager(
|
48
|
-
self.running = False
|
49
|
-
self.running_process: subprocess.Popen | None = None
|
36
|
+
self.docker = DockerManager()
|
50
37
|
self.fastled_src_dir: Path | None = fastled_src_dir
|
51
38
|
self.interactive = interactive
|
39
|
+
self.running_container: RunningContainer | None = None
|
40
|
+
self.auto_updates = auto_updates
|
52
41
|
self._port = self._start()
|
53
42
|
# fancy print
|
54
43
|
if not interactive:
|
@@ -57,6 +46,16 @@ class CompileServer:
|
|
57
46
|
print(msg)
|
58
47
|
print("#" * len(msg) + "\n")
|
59
48
|
|
49
|
+
@property
|
50
|
+
def running(self) -> bool:
|
51
|
+
if not self._port:
|
52
|
+
return False
|
53
|
+
if not DockerManager.is_docker_installed():
|
54
|
+
return False
|
55
|
+
if not DockerManager.is_running():
|
56
|
+
return False
|
57
|
+
return self.docker.is_container_running(self.container_name)
|
58
|
+
|
60
59
|
def using_fastled_src_dir_volume(self) -> bool:
|
61
60
|
return self.fastled_src_dir is not None
|
62
61
|
|
@@ -86,56 +85,43 @@ class CompileServer:
|
|
86
85
|
except Exception:
|
87
86
|
pass
|
88
87
|
time.sleep(0.1)
|
89
|
-
if not self.
|
88
|
+
if not self.docker.is_container_running(self.container_name):
|
90
89
|
return False
|
91
90
|
return False
|
92
91
|
|
93
92
|
def _start(self) -> int:
|
94
93
|
print("Compiling server starting")
|
95
|
-
self.running = True
|
96
|
-
# Ensure Docker is running
|
97
|
-
with self.docker.get_lock():
|
98
|
-
if not self.docker.is_running():
|
99
|
-
if not self.docker.start():
|
100
|
-
print("Docker could not be started. Exiting.")
|
101
|
-
raise RuntimeError("Docker could not be started. Exiting.")
|
102
94
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
print("Ensuring Docker image exists at latest version")
|
125
|
-
if not self.docker.ensure_image_exists():
|
126
|
-
print("Failed to ensure Docker image exists.")
|
127
|
-
raise RuntimeError("Failed to ensure Docker image exists")
|
95
|
+
# Ensure Docker is running
|
96
|
+
if not self.docker.is_running():
|
97
|
+
if not self.docker.start():
|
98
|
+
print("Docker could not be started. Exiting.")
|
99
|
+
raise RuntimeError("Docker could not be started. Exiting.")
|
100
|
+
now = datetime.now(timezone.utc)
|
101
|
+
now_str = now.strftime("%Y-%m-%d %H %Z")
|
102
|
+
|
103
|
+
upgrade = False
|
104
|
+
if self.auto_updates is None:
|
105
|
+
prev_date_str = DISK_CACHE.get("last-update")
|
106
|
+
if prev_date_str != now_str:
|
107
|
+
print("One hour has passed, checking docker for updates")
|
108
|
+
upgrade = True
|
109
|
+
else:
|
110
|
+
upgrade = self.auto_updates
|
111
|
+
self.docker.validate_or_download_image(
|
112
|
+
image_name=_IMAGE_NAME, tag="main", upgrade=upgrade
|
113
|
+
)
|
114
|
+
DISK_CACHE.put("last-update", now_str)
|
128
115
|
|
129
116
|
print("Docker image now validated")
|
130
|
-
port =
|
131
|
-
print(f"Found an available port: {port}")
|
117
|
+
port = SERVER_PORT
|
132
118
|
if self.interactive:
|
133
119
|
server_command = ["/bin/bash"]
|
134
120
|
else:
|
135
121
|
server_command = ["python", "/js/run.py", "server"] + SERVER_OPTIONS
|
136
122
|
server_cmd_str = subprocess.list2cmdline(server_command)
|
137
123
|
print(f"Started Docker container with command: {server_cmd_str}")
|
138
|
-
ports = {
|
124
|
+
ports = {80: port}
|
139
125
|
volumes = None
|
140
126
|
if self.fastled_src_dir:
|
141
127
|
print(
|
@@ -144,78 +130,29 @@ class CompileServer:
|
|
144
130
|
volumes = {
|
145
131
|
str(self.fastled_src_dir): {"bind": "/host/fastled/src", "mode": "ro"}
|
146
132
|
}
|
147
|
-
|
148
|
-
|
133
|
+
|
134
|
+
cmd_str = subprocess.list2cmdline(server_command)
|
135
|
+
|
136
|
+
self.docker.run_container(
|
137
|
+
image_name=_IMAGE_NAME,
|
138
|
+
tag="main",
|
139
|
+
container_name=self.container_name,
|
140
|
+
command=cmd_str,
|
141
|
+
ports=ports,
|
142
|
+
volumes=volumes,
|
149
143
|
)
|
144
|
+
self.running_container = self.docker.attach_and_run(self.container_name)
|
145
|
+
assert self.running_container is not None, "Container should be running"
|
146
|
+
|
150
147
|
print("Compile server starting")
|
151
|
-
time.sleep(3)
|
152
|
-
if self.running_process.poll() is not None:
|
153
|
-
print("Server failed to start")
|
154
|
-
self.running = False
|
155
|
-
raise RuntimeError("Server failed to start")
|
156
148
|
return port
|
157
149
|
|
158
150
|
def proceess_running(self) -> bool:
|
159
|
-
|
160
|
-
return False
|
161
|
-
return self.running_process.poll() is None
|
151
|
+
return self.docker.is_container_running(self.container_name)
|
162
152
|
|
163
153
|
def stop(self) -> None:
|
164
|
-
print(f"Stopping server on port {self._port}")
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
# # except Exception:
|
169
|
-
# except Exception as e:
|
170
|
-
# print(f"Failed to send shutdown signal: {e}")
|
171
|
-
# pass
|
172
|
-
try:
|
173
|
-
# Stop the Docker container
|
174
|
-
cp: subprocess.CompletedProcess
|
175
|
-
cp = subprocess.run(
|
176
|
-
["docker", "stop", self.container_name],
|
177
|
-
capture_output=True,
|
178
|
-
text=True,
|
179
|
-
check=True,
|
180
|
-
)
|
181
|
-
if cp.returncode != 0:
|
182
|
-
print(f"Failed to stop Docker container: {cp.stderr}")
|
183
|
-
|
184
|
-
cp = subprocess.run(
|
185
|
-
["docker", "rm", self.container_name],
|
186
|
-
capture_output=True,
|
187
|
-
text=True,
|
188
|
-
check=True,
|
189
|
-
)
|
190
|
-
if cp.returncode != 0:
|
191
|
-
print(f"Failed to remove Docker container: {cp.stderr}")
|
192
|
-
|
193
|
-
# Close the stdout pipe
|
194
|
-
if self.running_process and self.running_process.stdout:
|
195
|
-
self.running_process.stdout.close()
|
196
|
-
|
197
|
-
# Wait for the process to fully terminate with a timeout
|
198
|
-
self.running_process.wait(timeout=10)
|
199
|
-
if self.running_process.returncode is None:
|
200
|
-
# kill
|
201
|
-
self.running_process.kill()
|
202
|
-
if self.running_process.returncode is not None:
|
203
|
-
print(
|
204
|
-
f"Server stopped with return code {self.running_process.returncode}"
|
205
|
-
)
|
206
|
-
|
207
|
-
except subprocess.TimeoutExpired:
|
208
|
-
# Force kill if it doesn't stop gracefully
|
209
|
-
if self.running_process:
|
210
|
-
self.running_process.kill()
|
211
|
-
self.running_process.wait()
|
212
|
-
except KeyboardInterrupt:
|
213
|
-
if self.running_process:
|
214
|
-
self.running_process.kill()
|
215
|
-
self.running_process.wait()
|
216
|
-
except Exception as e:
|
217
|
-
print(f"Error stopping Docker container: {e}")
|
218
|
-
finally:
|
219
|
-
self.running_process = None
|
220
|
-
self.running = False
|
154
|
+
# print(f"Stopping server on port {self._port}")
|
155
|
+
if self.running_container:
|
156
|
+
self.running_container.stop()
|
157
|
+
self.docker.suspend_container(self.container_name)
|
221
158
|
print("Compile server stopped")
|
fastled/docker_manager.py
CHANGED
@@ -1,17 +1,36 @@
|
|
1
|
-
"""
|
1
|
+
"""
|
2
|
+
New abstraction for Docker management with improved Ctrl+C handling.
|
3
|
+
"""
|
2
4
|
|
5
|
+
import _thread
|
3
6
|
import subprocess
|
4
7
|
import sys
|
8
|
+
import threading
|
5
9
|
import time
|
10
|
+
import traceback
|
11
|
+
import warnings
|
12
|
+
from datetime import datetime, timezone
|
6
13
|
from pathlib import Path
|
7
14
|
|
8
|
-
import docker
|
15
|
+
import docker
|
16
|
+
from appdirs import user_data_dir
|
17
|
+
from disklru import DiskLRUCache
|
18
|
+
from docker.client import DockerClient
|
19
|
+
from docker.models.containers import Container
|
20
|
+
from docker.models.images import Image
|
9
21
|
from filelock import FileLock
|
10
22
|
|
11
|
-
|
23
|
+
CONFIG_DIR = Path(user_data_dir("fastled", "fastled"))
|
24
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
25
|
+
DB_FILE = CONFIG_DIR / "db.db"
|
26
|
+
DISK_CACHE = DiskLRUCache(str(DB_FILE), 10)
|
12
27
|
|
13
|
-
|
14
|
-
|
28
|
+
|
29
|
+
# Docker uses datetimes in UTC but without the timezone info. If we pass in a tz
|
30
|
+
# then it will throw an exception.
|
31
|
+
def _utc_now_no_tz() -> datetime:
|
32
|
+
now = datetime.now(timezone.utc)
|
33
|
+
return now.replace(tzinfo=None)
|
15
34
|
|
16
35
|
|
17
36
|
def _win32_docker_location() -> str | None:
|
@@ -26,11 +45,52 @@ def _win32_docker_location() -> str | None:
|
|
26
45
|
return None
|
27
46
|
|
28
47
|
|
29
|
-
|
30
|
-
"""
|
48
|
+
def get_lock(image_name: str) -> FileLock:
|
49
|
+
"""Get the file lock for this DockerManager instance."""
|
50
|
+
lock_file = CONFIG_DIR / f"{image_name}.lock"
|
51
|
+
return FileLock(str(lock_file))
|
52
|
+
|
53
|
+
|
54
|
+
class RunningContainer:
|
55
|
+
def __init__(self, container, first_run=False):
|
56
|
+
self.container = container
|
57
|
+
self.first_run = first_run
|
58
|
+
self.running = True
|
59
|
+
self.thread = threading.Thread(target=self._log_monitor)
|
60
|
+
self.thread.daemon = True
|
61
|
+
self.thread.start()
|
62
|
+
|
63
|
+
def _log_monitor(self):
|
64
|
+
from_date = _utc_now_no_tz() if not self.first_run else None
|
65
|
+
to_date = _utc_now_no_tz()
|
66
|
+
|
67
|
+
while self.running:
|
68
|
+
try:
|
69
|
+
for log in self.container.logs(
|
70
|
+
follow=False, since=from_date, until=to_date, stream=True
|
71
|
+
):
|
72
|
+
print(log.decode("utf-8"), end="")
|
73
|
+
time.sleep(0.1)
|
74
|
+
from_date = to_date
|
75
|
+
to_date = _utc_now_no_tz()
|
76
|
+
except KeyboardInterrupt:
|
77
|
+
print("Monitoring logs interrupted by user.")
|
78
|
+
_thread.interrupt_main()
|
79
|
+
break
|
80
|
+
except Exception as e:
|
81
|
+
print(f"Error monitoring logs: {e}")
|
82
|
+
break
|
83
|
+
|
84
|
+
def stop(self) -> None:
|
85
|
+
"""Stop monitoring the container logs"""
|
86
|
+
self.running = False
|
87
|
+
self.thread.join()
|
88
|
+
|
31
89
|
|
32
|
-
|
33
|
-
|
90
|
+
class DockerManager:
|
91
|
+
def __init__(self) -> None:
|
92
|
+
self.client: DockerClient = docker.from_env()
|
93
|
+
self.first_run = False
|
34
94
|
|
35
95
|
@staticmethod
|
36
96
|
def is_docker_installed() -> bool:
|
@@ -46,9 +106,13 @@ class DockerManager:
|
|
46
106
|
print("Docker is not installed.")
|
47
107
|
return False
|
48
108
|
|
49
|
-
|
109
|
+
@staticmethod
|
110
|
+
def is_running() -> bool:
|
50
111
|
"""Check if Docker is running by pinging the Docker daemon."""
|
112
|
+
if not DockerManager.is_docker_installed():
|
113
|
+
return False
|
51
114
|
try:
|
115
|
+
# self.client.ping()
|
52
116
|
client = docker.from_env()
|
53
117
|
client.ping()
|
54
118
|
print("Docker is running.")
|
@@ -104,158 +168,348 @@ class DockerManager:
|
|
104
168
|
print(f"Error starting Docker: {str(e)}")
|
105
169
|
return False
|
106
170
|
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
if "linux" in result.stdout.lower():
|
116
|
-
return True
|
171
|
+
def validate_or_download_image(
|
172
|
+
self, image_name: str, tag: str = "latest", upgrade: bool = False
|
173
|
+
) -> None:
|
174
|
+
"""
|
175
|
+
Validate if the image exists, and if not, download it.
|
176
|
+
If upgrade is True, will pull the latest version even if image exists locally.
|
177
|
+
"""
|
178
|
+
print(f"Validating image {image_name}:{tag}...")
|
117
179
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
)
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
180
|
+
with get_lock(f"{image_name}-{tag}"):
|
181
|
+
try:
|
182
|
+
local_image = self.client.images.get(f"{image_name}:{tag}")
|
183
|
+
print(f"Image {image_name}:{tag} is already available.")
|
184
|
+
|
185
|
+
if upgrade:
|
186
|
+
remote_image = self.client.images.get_registry_data(
|
187
|
+
f"{image_name}:{tag}"
|
188
|
+
)
|
189
|
+
remote_image_hash = remote_image.id
|
190
|
+
remote_image_hash_from_local_image: str | None = None
|
191
|
+
try:
|
192
|
+
remote_image_hash_from_local_image = DISK_CACHE.get(
|
193
|
+
local_image.id
|
194
|
+
)
|
195
|
+
except KeyboardInterrupt:
|
196
|
+
raise
|
197
|
+
except Exception:
|
198
|
+
remote_image_hash_from_local_image = None
|
199
|
+
import traceback
|
200
|
+
import warnings
|
201
|
+
|
202
|
+
stack = traceback.format_exc()
|
203
|
+
warnings.warn(
|
204
|
+
f"Error getting remote image hash from local image: {stack}"
|
205
|
+
)
|
206
|
+
if remote_image_hash_from_local_image == remote_image_hash:
|
207
|
+
print(f"Local image {image_name}:{tag} is up to date.")
|
208
|
+
return
|
209
|
+
|
210
|
+
# Quick check for latest version
|
211
|
+
|
212
|
+
print(f"Pulling newer version of {image_name}:{tag}...")
|
213
|
+
_ = self.client.images.pull(image_name, tag=tag)
|
214
|
+
print(f"Updated to newer version of {image_name}:{tag}")
|
215
|
+
local_image_hash = self.client.images.get(f"{image_name}:{tag}").id
|
216
|
+
DISK_CACHE.put(local_image_hash, remote_image_hash)
|
217
|
+
|
218
|
+
except docker.errors.ImageNotFound:
|
219
|
+
print(f"Image {image_name}:{tag} not found. Downloading...")
|
220
|
+
self.client.images.pull(image_name, tag=tag)
|
221
|
+
try:
|
222
|
+
local_image = self.client.images.get(f"{image_name}:{tag}")
|
223
|
+
local_image_hash = local_image.id
|
224
|
+
DISK_CACHE.put(local_image_hash, remote_image_hash)
|
225
|
+
print(f"Image {image_name}:{tag} downloaded successfully.")
|
226
|
+
except docker.errors.ImageNotFound:
|
227
|
+
import warnings
|
228
|
+
|
229
|
+
warnings.warn(f"Image {image_name}:{tag} not found after download.")
|
230
|
+
|
231
|
+
def tag_image(self, image_name: str, old_tag: str, new_tag: str) -> None:
|
232
|
+
"""
|
233
|
+
Tag an image with a new tag.
|
234
|
+
"""
|
235
|
+
image: Image = self.client.images.get(f"{image_name}:{old_tag}")
|
236
|
+
image.tag(image_name, new_tag)
|
237
|
+
print(f"Image {image_name}:{old_tag} tagged as {new_tag}.")
|
134
238
|
|
135
|
-
def
|
136
|
-
|
137
|
-
|
239
|
+
def _container_configs_match(
|
240
|
+
self,
|
241
|
+
container: Container,
|
242
|
+
command: str | None,
|
243
|
+
volumes: dict | None,
|
244
|
+
ports: dict | None,
|
245
|
+
) -> bool:
|
246
|
+
"""Compare if existing container has matching configuration"""
|
247
|
+
try:
|
248
|
+
# Check if container is using the same image
|
249
|
+
container_image_id = container.image.id
|
250
|
+
container_image_tags = container.image.tags
|
138
251
|
|
139
|
-
|
140
|
-
|
252
|
+
# Simplified image comparison - just compare the IDs directly
|
253
|
+
if not container_image_tags:
|
254
|
+
print(f"Container using untagged image with ID: {container_image_id}")
|
255
|
+
else:
|
256
|
+
current_image = self.client.images.get(container_image_tags[0])
|
257
|
+
if container_image_id != current_image.id:
|
258
|
+
print(
|
259
|
+
f"Container using different image version. Container: {container_image_id}, Current: {current_image.id}"
|
260
|
+
)
|
261
|
+
return False
|
141
262
|
|
142
|
-
|
143
|
-
if
|
263
|
+
# Check command if specified
|
264
|
+
if command and container.attrs["Config"]["Cmd"] != command.split():
|
265
|
+
print(
|
266
|
+
f"Command mismatch: {container.attrs['Config']['Cmd']} != {command}"
|
267
|
+
)
|
144
268
|
return False
|
145
269
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
text=True,
|
157
|
-
check=False,
|
158
|
-
)
|
159
|
-
if result.returncode != 0:
|
160
|
-
print("Failed to check for newer image.")
|
161
|
-
return False
|
162
|
-
print("Tagging image")
|
163
|
-
|
164
|
-
tag_result = subprocess.run(
|
165
|
-
[
|
166
|
-
"docker",
|
167
|
-
"tag",
|
168
|
-
remote_image,
|
169
|
-
image_name,
|
170
|
-
],
|
171
|
-
capture_output=True,
|
172
|
-
text=True,
|
173
|
-
check=False,
|
174
|
-
)
|
175
|
-
if tag_result.returncode != 0:
|
176
|
-
print(f"Failed to tag image: {tag_result.stderr}")
|
177
|
-
return False
|
178
|
-
return True
|
179
|
-
except subprocess.CalledProcessError as e:
|
180
|
-
print(f"Failed to ensure image exists: {e}")
|
181
|
-
return False
|
270
|
+
# Check volumes if specified
|
271
|
+
if volumes:
|
272
|
+
container_mounts = (
|
273
|
+
{
|
274
|
+
m["Source"]: {"bind": m["Destination"], "mode": m["Mode"]}
|
275
|
+
for m in container.attrs["Mounts"]
|
276
|
+
}
|
277
|
+
if container.attrs.get("Mounts")
|
278
|
+
else {}
|
279
|
+
)
|
182
280
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
281
|
+
for host_dir, mount in volumes.items():
|
282
|
+
if host_dir not in container_mounts:
|
283
|
+
print(f"Volume {host_dir} not found in container mounts.")
|
284
|
+
return False
|
285
|
+
if container_mounts[host_dir] != mount:
|
286
|
+
print(
|
287
|
+
f"Volume {host_dir} has different mount options: {container_mounts[host_dir]} != {mount}"
|
288
|
+
)
|
289
|
+
return False
|
290
|
+
|
291
|
+
# Check ports if specified
|
292
|
+
if ports:
|
293
|
+
container_ports = (
|
294
|
+
container.attrs["Config"]["ExposedPorts"]
|
295
|
+
if container.attrs["Config"].get("ExposedPorts")
|
296
|
+
else {}
|
297
|
+
)
|
298
|
+
container_port_bindings = (
|
299
|
+
container.attrs["HostConfig"]["PortBindings"]
|
300
|
+
if container.attrs["HostConfig"].get("PortBindings")
|
301
|
+
else {}
|
302
|
+
)
|
194
303
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
304
|
+
for container_port, host_port in ports.items():
|
305
|
+
port_key = f"{container_port}/tcp"
|
306
|
+
if port_key not in container_ports:
|
307
|
+
print(f"Container port {port_key} not found.")
|
308
|
+
return False
|
309
|
+
if not container_port_bindings.get(port_key, [{"HostPort": None}])[
|
310
|
+
0
|
311
|
+
]["HostPort"] == str(host_port):
|
312
|
+
print(f"Port {host_port} is not bound to {port_key}.")
|
313
|
+
return False
|
314
|
+
except KeyboardInterrupt:
|
315
|
+
raise
|
316
|
+
except docker.errors.NotFound:
|
317
|
+
print("Container not found.")
|
204
318
|
return False
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
319
|
+
except Exception as e:
|
320
|
+
stack = traceback.format_exc()
|
321
|
+
warnings.warn(f"Error checking container config: {e}\n{stack}")
|
322
|
+
return False
|
323
|
+
return True
|
209
324
|
|
210
325
|
def run_container(
|
211
326
|
self,
|
212
|
-
|
327
|
+
image_name: str,
|
328
|
+
tag: str,
|
329
|
+
container_name: str,
|
330
|
+
command: str | None = None,
|
213
331
|
volumes: dict[str, dict[str, str]] | None = None,
|
214
332
|
ports: dict[int, int] | None = None,
|
215
|
-
|
216
|
-
|
217
|
-
|
333
|
+
) -> Container:
|
334
|
+
"""
|
335
|
+
Run a container from an image. If it already exists with matching config, start it.
|
336
|
+
If it exists with different config, remove and recreate it.
|
218
337
|
|
219
338
|
Args:
|
220
|
-
cmd: Command to run in the container
|
221
339
|
volumes: Dict mapping host paths to dicts with 'bind' and 'mode' keys
|
340
|
+
Example: {'/host/path': {'bind': '/container/path', 'mode': 'rw'}}
|
222
341
|
ports: Dict mapping host ports to container ports
|
342
|
+
Example: {8080: 80} maps host port 8080 to container port 80
|
223
343
|
"""
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
docker_command.append("-it")
|
233
|
-
# Attach volumes if specified
|
234
|
-
docker_command += [
|
235
|
-
"--name",
|
236
|
-
self.container_name,
|
237
|
-
]
|
238
|
-
if ports:
|
239
|
-
for host_port, container_port in ports.items():
|
240
|
-
docker_command.extend(["-p", f"{host_port}:{container_port}"])
|
241
|
-
if volumes:
|
242
|
-
for host_path, mount_spec in volumes.items():
|
243
|
-
docker_command.extend(
|
244
|
-
["-v", f"{host_path}:{mount_spec['bind']}:{mount_spec['mode']}"]
|
344
|
+
image_name = f"{image_name}:{tag}"
|
345
|
+
try:
|
346
|
+
container: Container = self.client.containers.get(container_name)
|
347
|
+
|
348
|
+
# Check if configuration matches
|
349
|
+
if not self._container_configs_match(container, command, volumes, ports):
|
350
|
+
print(
|
351
|
+
f"Container {container_name} exists but with different configuration. Removing and recreating..."
|
245
352
|
)
|
353
|
+
container.remove(force=True)
|
354
|
+
raise docker.errors.NotFound("Container removed due to config mismatch")
|
355
|
+
print(f"Container {container_name} found with matching configuration.")
|
356
|
+
|
357
|
+
# Existing container with matching config - handle various states
|
358
|
+
if container.status == "running":
|
359
|
+
print(f"Container {container_name} is already running.")
|
360
|
+
elif container.status == "exited":
|
361
|
+
print(f"Starting existing container {container_name}.")
|
362
|
+
container.start()
|
363
|
+
elif container.status == "restarting":
|
364
|
+
print(f"Waiting for container {container_name} to restart...")
|
365
|
+
timeout = 10
|
366
|
+
container.wait(timeout=10)
|
367
|
+
if container.status == "running":
|
368
|
+
print(f"Container {container_name} has restarted.")
|
369
|
+
else:
|
370
|
+
print(
|
371
|
+
f"Container {container_name} did not restart within {timeout} seconds."
|
372
|
+
)
|
373
|
+
container.stop()
|
374
|
+
print(f"Container {container_name} has been stopped.")
|
375
|
+
container.start()
|
376
|
+
elif container.status == "paused":
|
377
|
+
print(f"Resuming existing container {container_name}.")
|
378
|
+
container.unpause()
|
379
|
+
else:
|
380
|
+
print(f"Unknown container status: {container.status}")
|
381
|
+
print(f"Starting existing container {container_name}.")
|
382
|
+
self.first_run = True
|
383
|
+
container.start()
|
384
|
+
except docker.errors.NotFound:
|
385
|
+
print(f"Creating and starting {container_name}")
|
386
|
+
container = self.client.containers.run(
|
387
|
+
image_name,
|
388
|
+
command,
|
389
|
+
name=container_name,
|
390
|
+
detach=True,
|
391
|
+
tty=True,
|
392
|
+
volumes=volumes,
|
393
|
+
ports=ports,
|
394
|
+
)
|
395
|
+
return container
|
246
396
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
397
|
+
def attach_and_run(self, container: Container | str) -> RunningContainer:
|
398
|
+
"""
|
399
|
+
Attach to a running container and monitor its logs in a background thread.
|
400
|
+
Returns a RunningContainer object that can be used to stop monitoring.
|
401
|
+
"""
|
402
|
+
if isinstance(container, str):
|
403
|
+
container = self.get_container(container)
|
404
|
+
|
405
|
+
print(f"Attaching to container {container.name}...")
|
253
406
|
|
254
|
-
|
407
|
+
first_run = self.first_run
|
408
|
+
self.first_run = False
|
409
|
+
|
410
|
+
return RunningContainer(container, first_run)
|
411
|
+
|
412
|
+
def suspend_container(self, container: Container | str) -> None:
|
413
|
+
"""
|
414
|
+
Suspend (pause) the container.
|
415
|
+
"""
|
416
|
+
if isinstance(container, str):
|
417
|
+
container_name = container
|
418
|
+
container = self.get_container(container)
|
419
|
+
if not container:
|
420
|
+
print(f"Could not put container {container_name} to sleep.")
|
421
|
+
return
|
422
|
+
try:
|
423
|
+
container.pause()
|
424
|
+
print(f"Container {container.name} has been suspended.")
|
425
|
+
except KeyboardInterrupt:
|
426
|
+
print(f"Container {container.name} interrupted by keyboard interrupt.")
|
427
|
+
except Exception as e:
|
428
|
+
print(f"Failed to suspend container {container.name}: {e}")
|
429
|
+
|
430
|
+
def resume_container(self, container: Container | str) -> None:
|
431
|
+
"""
|
432
|
+
Resume (unpause) the container.
|
433
|
+
"""
|
434
|
+
if isinstance(container, str):
|
435
|
+
container = self.get_container(container)
|
436
|
+
try:
|
437
|
+
container.unpause()
|
438
|
+
print(f"Container {container.name} has been resumed.")
|
439
|
+
except Exception as e:
|
440
|
+
print(f"Failed to resume container {container.name}: {e}")
|
255
441
|
|
256
|
-
|
257
|
-
|
258
|
-
|
442
|
+
def get_container(self, container_name: str) -> Container:
|
443
|
+
"""
|
444
|
+
Get a container by name.
|
445
|
+
"""
|
446
|
+
try:
|
447
|
+
return self.client.containers.get(container_name)
|
448
|
+
except docker.errors.NotFound:
|
449
|
+
print(f"Container {container_name} not found.")
|
450
|
+
raise
|
451
|
+
|
452
|
+
def is_container_running(self, container_name: str) -> bool:
|
453
|
+
"""
|
454
|
+
Check if a container is running.
|
455
|
+
"""
|
456
|
+
try:
|
457
|
+
container = self.client.containers.get(container_name)
|
458
|
+
return container.status == "running"
|
459
|
+
except docker.errors.NotFound:
|
460
|
+
print(f"Container {container_name} not found.")
|
461
|
+
return False
|
462
|
+
|
463
|
+
|
464
|
+
def main():
|
465
|
+
# Register SIGINT handler
|
466
|
+
# signal.signal(signal.SIGINT, handle_sigint)
|
467
|
+
|
468
|
+
docker_manager = DockerManager()
|
469
|
+
|
470
|
+
# Parameters
|
471
|
+
image_name = "python"
|
472
|
+
tag = "3.10-slim"
|
473
|
+
# new_tag = "my-python"
|
474
|
+
container_name = "my-python-container"
|
475
|
+
command = "python -m http.server"
|
476
|
+
|
477
|
+
try:
|
478
|
+
# Step 1: Validate or download the image
|
479
|
+
docker_manager.validate_or_download_image(image_name, tag, upgrade=True)
|
480
|
+
|
481
|
+
# Step 2: Tag the image
|
482
|
+
# docker_manager.tag_image(image_name, tag, new_tag)
|
483
|
+
|
484
|
+
# Step 3: Run the container
|
485
|
+
container = docker_manager.run_container(
|
486
|
+
image_name, tag, container_name, command
|
259
487
|
)
|
260
488
|
|
261
|
-
|
489
|
+
# Step 4: Attach and monitor the container logs
|
490
|
+
running_container = docker_manager.attach_and_run(container)
|
491
|
+
|
492
|
+
# Wait for keyboard interrupt
|
493
|
+
while True:
|
494
|
+
time.sleep(0.1)
|
495
|
+
|
496
|
+
except KeyboardInterrupt:
|
497
|
+
print("\nStopping container...")
|
498
|
+
running_container.stop()
|
499
|
+
container = docker_manager.get_container(container_name)
|
500
|
+
docker_manager.suspend_container(container)
|
501
|
+
|
502
|
+
try:
|
503
|
+
# Suspend and resume the container
|
504
|
+
container = docker_manager.get_container(container_name)
|
505
|
+
docker_manager.suspend_container(container)
|
506
|
+
|
507
|
+
input("Press Enter to resume the container...")
|
508
|
+
|
509
|
+
docker_manager.resume_container(container)
|
510
|
+
except Exception as e:
|
511
|
+
print(f"An error occurred: {e}")
|
512
|
+
|
513
|
+
|
514
|
+
if __name__ == "__main__":
|
515
|
+
main()
|
fastled/web_compile.py
CHANGED
@@ -5,7 +5,7 @@ import os
|
|
5
5
|
import shutil
|
6
6
|
import tempfile
|
7
7
|
import zipfile
|
8
|
-
from concurrent.futures import Future,
|
8
|
+
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
|
9
9
|
from dataclasses import dataclass
|
10
10
|
from pathlib import Path
|
11
11
|
|
@@ -21,7 +21,7 @@ ENDPOINT_COMPILED_WASM = "compile/wasm"
|
|
21
21
|
_TIMEOUT = 60 * 4 # 2 mins timeout
|
22
22
|
_AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
|
23
23
|
ENABLE_EMBEDDED_DATA = True
|
24
|
-
_EXECUTOR =
|
24
|
+
_EXECUTOR = ThreadPoolExecutor(max_workers=8)
|
25
25
|
|
26
26
|
|
27
27
|
@dataclass
|
@@ -66,16 +66,11 @@ def _test_connection(host: str, use_ipv4: bool) -> ConnectionResult:
|
|
66
66
|
)
|
67
67
|
result = ConnectionResult(host, test_response.status_code == 200, use_ipv4)
|
68
68
|
except KeyboardInterrupt:
|
69
|
-
# main thraed interrupt
|
70
|
-
|
71
69
|
_thread.interrupt_main()
|
72
70
|
|
73
71
|
except TimeoutError:
|
74
72
|
result = ConnectionResult(host, False, use_ipv4)
|
75
|
-
except Exception
|
76
|
-
import warnings
|
77
|
-
|
78
|
-
warnings.warn(f"Connection failed: {e}")
|
73
|
+
except Exception:
|
79
74
|
result = ConnectionResult(host, False, use_ipv4)
|
80
75
|
return result
|
81
76
|
|
@@ -1,13 +1,13 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastled
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.17
|
4
4
|
Summary: FastLED Wasm Compiler
|
5
5
|
Home-page: https://github.com/zackees/fastled-wasm
|
6
6
|
Maintainer: Zachary Vorhies
|
7
7
|
License: BSD 3-Clause License
|
8
8
|
Keywords: template-python-cmd
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
10
|
-
Requires-Python: >=3.
|
10
|
+
Requires-Python: >=3.9
|
11
11
|
Description-Content-Type: text/markdown
|
12
12
|
License-File: LICENSE
|
13
13
|
Requires-Dist: docker
|
@@ -16,6 +16,8 @@ Requires-Dist: watchdog
|
|
16
16
|
Requires-Dist: livereload
|
17
17
|
Requires-Dist: download
|
18
18
|
Requires-Dist: filelock
|
19
|
+
Requires-Dist: disklru>=2.0.1
|
20
|
+
Requires-Dist: appdirs
|
19
21
|
|
20
22
|
# FastLED Wasm compiler
|
21
23
|
|
@@ -159,6 +161,8 @@ A: A big chunk of space is being used by unnecessary javascript `emscripten` is
|
|
159
161
|
|
160
162
|
# Revisions
|
161
163
|
|
164
|
+
* 1.1.17 - Added `--update` and `--no-auto-update` to control whether the compiler in docker mode will try to update.
|
165
|
+
* 1.1.16 - Rewrote docker logic to use container suspension and resumption. Much much faster.
|
162
166
|
* 1.1.15 - Fixed logic for considering ipv6 addresses. Auto selection of ipv6 is now restored.
|
163
167
|
* 1.1.14 - Fixes for regression in using --server and --localhost as two instances, this is now under test.
|
164
168
|
* 1.1.13 - Disable the use of ipv6. It produces random timeouts on the onrender server we are using for the web compiler.
|
@@ -1,20 +1,20 @@
|
|
1
|
-
fastled/__init__.py,sha256=
|
2
|
-
fastled/app.py,sha256=
|
1
|
+
fastled/__init__.py,sha256=eYxLUpPhHXARLnRFwdJgoCDIx4kVkzNiZ_--PzqMApE,64
|
2
|
+
fastled/app.py,sha256=xgMl-9s3dy2TurnSusVdIAr3ZbSi5oyEAofHsa7hZho,15695
|
3
3
|
fastled/build_mode.py,sha256=joMwsV4K1y_LijT4gEAcjx69RZBoe_KmFmHZdPYbL_4,631
|
4
4
|
fastled/cli.py,sha256=CNR_pQR0sNVPNuv8e_nmm-0PI8sU-eUBUgnWgWkzW9c,237
|
5
|
-
fastled/compile_server.py,sha256=
|
6
|
-
fastled/docker_manager.py,sha256=
|
5
|
+
fastled/compile_server.py,sha256=aBdpILSRrDsCJ5e9g5uwIqt9bcqE_8FrSddCV2ygtrI,5401
|
6
|
+
fastled/docker_manager.py,sha256=dj6s1mT-ecURqYJH-JpZZWuFHr-dGkQIOuFm845Nz40,20042
|
7
7
|
fastled/filewatcher.py,sha256=fJNMQRDCpihSL4nQeYPqbD4m1Jzjcz_-YRAo-wlPW6k,6518
|
8
8
|
fastled/keyboard.py,sha256=rqndglWYzRy6oiqHgsmx1peLd0Yrpci01zGENlCzh_s,2576
|
9
9
|
fastled/open_browser.py,sha256=RRHcsZ5Vzsw1AuZUEYuSfjKmf_9j3NGMDUR-FndHmqs,1483
|
10
10
|
fastled/paths.py,sha256=VsPmgu0lNSCFOoEC0BsTYzDygXqy15AHUfN-tTuzDZA,99
|
11
11
|
fastled/sketch.py,sha256=KhhPFqlFVlBk8YrzFy7-ioe7zEzecgrVLhyFbLpBp7k,1845
|
12
12
|
fastled/util.py,sha256=t4M3NFMhnCzfYbLvIyJi0RdFssZqbTN_vVIaej1WV-U,265
|
13
|
-
fastled/web_compile.py,sha256=
|
13
|
+
fastled/web_compile.py,sha256=KuvKGdX6SSUUqC7YgX4T9SMSP5wdcPUhpg9-K9zPoTI,10378
|
14
14
|
fastled/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
|
15
|
-
fastled-1.1.
|
16
|
-
fastled-1.1.
|
17
|
-
fastled-1.1.
|
18
|
-
fastled-1.1.
|
19
|
-
fastled-1.1.
|
20
|
-
fastled-1.1.
|
15
|
+
fastled-1.1.17.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
16
|
+
fastled-1.1.17.dist-info/METADATA,sha256=IX_gRsLWHvl9Fk7sbMWTmVoLqRGuDcAW_88tbfLAKuA,13496
|
17
|
+
fastled-1.1.17.dist-info/WHEEL,sha256=0VNUDWQJzfRahYI3neAhz2UVbRCtztpN5dPHAGvmGXc,109
|
18
|
+
fastled-1.1.17.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
|
19
|
+
fastled-1.1.17.dist-info/top_level.txt,sha256=xfG6Z_ol9V5YmBROkZq2QTRwjbS2ouCUxaTJsOwfkOo,14
|
20
|
+
fastled-1.1.17.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|