fastled 1.1.0__py2.py3-none-any.whl → 1.1.3__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/app.py +74 -9
- fastled/compile_server.py +98 -41
- fastled/docker_manager.py +8 -6
- fastled/web_compile.py +220 -173
- {fastled-1.1.0.dist-info → fastled-1.1.3.dist-info}/METADATA +4 -2
- {fastled-1.1.0.dist-info → fastled-1.1.3.dist-info}/RECORD +10 -10
- {fastled-1.1.0.dist-info → fastled-1.1.3.dist-info}/LICENSE +0 -0
- {fastled-1.1.0.dist-info → fastled-1.1.3.dist-info}/WHEEL +0 -0
- {fastled-1.1.0.dist-info → fastled-1.1.3.dist-info}/entry_points.txt +0 -0
- {fastled-1.1.0.dist-info → fastled-1.1.3.dist-info}/top_level.txt +0 -0
fastled/app.py
CHANGED
@@ -16,7 +16,7 @@ from dataclasses import dataclass
|
|
16
16
|
from pathlib import Path
|
17
17
|
|
18
18
|
from fastled.build_mode import BuildMode, get_build_mode
|
19
|
-
from fastled.compile_server import CompileServer
|
19
|
+
from fastled.compile_server import CompileServer, looks_like_fastled_repo
|
20
20
|
from fastled.docker_manager import DockerManager
|
21
21
|
from fastled.filewatcher import FileChangedNotifier
|
22
22
|
from fastled.open_browser import open_browser_thread
|
@@ -81,6 +81,12 @@ def parse_args() -> argparse.Namespace:
|
|
81
81
|
nargs="+",
|
82
82
|
help="Additional patterns to exclude from file watching (Not available with --web)",
|
83
83
|
)
|
84
|
+
parser.add_argument(
|
85
|
+
"-i",
|
86
|
+
"--interactive",
|
87
|
+
action="store_true",
|
88
|
+
help="Run in interactive mode (Not available with --web)",
|
89
|
+
)
|
84
90
|
parser.add_argument(
|
85
91
|
"--profile",
|
86
92
|
action="store_true",
|
@@ -97,6 +103,11 @@ def parse_args() -> argparse.Namespace:
|
|
97
103
|
build_mode.add_argument(
|
98
104
|
"--release", action="store_true", help="Build in release mode"
|
99
105
|
)
|
106
|
+
build_mode.add_argument(
|
107
|
+
"--server",
|
108
|
+
action="store_true",
|
109
|
+
help="Run the server in the current directory, volume mapping fastled if we are in the repo",
|
110
|
+
)
|
100
111
|
|
101
112
|
build_mode.add_argument(
|
102
113
|
"--force-compile",
|
@@ -148,9 +159,7 @@ def run_web_compiler(
|
|
148
159
|
|
149
160
|
# now check to see if the hash value is the same as the last hash value
|
150
161
|
if last_hash_value is not None and last_hash_value == web_result.hash_value:
|
151
|
-
print(
|
152
|
-
"\nNo significant source code changes detected and data was the same, skipping recompilation."
|
153
|
-
)
|
162
|
+
print("\nSkipping redeploy: No significant changes found.")
|
154
163
|
print_results()
|
155
164
|
return CompiledResult(
|
156
165
|
success=True, fastled_js=str(output_dir), hash_value=web_result.hash_value
|
@@ -200,7 +209,24 @@ def _try_start_server_or_get_url(args: argparse.Namespace) -> str | CompileServe
|
|
200
209
|
return DEFAULT_URL
|
201
210
|
|
202
211
|
|
212
|
+
def _lots_and_lots_of_files(directory: Path) -> bool:
|
213
|
+
count = 0
|
214
|
+
for root, dirs, files in os.walk(directory):
|
215
|
+
count += len(files)
|
216
|
+
if count > 100:
|
217
|
+
return True
|
218
|
+
return False
|
219
|
+
|
220
|
+
|
203
221
|
def _looks_like_sketch_directory(directory: Path) -> bool:
|
222
|
+
if looks_like_fastled_repo(directory):
|
223
|
+
print("Directory looks like the FastLED repo")
|
224
|
+
return False
|
225
|
+
|
226
|
+
if _lots_and_lots_of_files(directory):
|
227
|
+
print("Too many files in the directory, bailing out")
|
228
|
+
return False
|
229
|
+
|
204
230
|
# walk the path and if there are over 30 files, return False
|
205
231
|
# at the root of the directory there should either be an ino file or a src directory
|
206
232
|
# or some cpp files
|
@@ -217,11 +243,10 @@ def _looks_like_sketch_directory(directory: Path) -> bool:
|
|
217
243
|
return False
|
218
244
|
|
219
245
|
|
220
|
-
def
|
221
|
-
|
246
|
+
def run_client(args: argparse.Namespace) -> int:
|
247
|
+
compile_server: CompileServer | None = None
|
222
248
|
open_web_browser = not args.just_compile
|
223
249
|
profile = args.profile
|
224
|
-
|
225
250
|
if not args.force_compile and not _looks_like_sketch_directory(
|
226
251
|
Path(args.directory)
|
227
252
|
):
|
@@ -237,16 +262,16 @@ def main() -> int:
|
|
237
262
|
)
|
238
263
|
args.web = True
|
239
264
|
|
240
|
-
compile_server: CompileServer | None = None
|
241
265
|
url: str
|
242
|
-
|
243
266
|
try:
|
244
267
|
try:
|
245
268
|
url_or_server: str | CompileServer = _try_start_server_or_get_url(args)
|
246
269
|
if isinstance(url_or_server, str):
|
270
|
+
print(f"Found URL: {url_or_server}")
|
247
271
|
url = url_or_server
|
248
272
|
else:
|
249
273
|
compile_server = url_or_server
|
274
|
+
print(f"Server started at {compile_server.url()}")
|
250
275
|
url = compile_server.url()
|
251
276
|
except KeyboardInterrupt:
|
252
277
|
print("\nExiting from first try...")
|
@@ -343,9 +368,49 @@ def main() -> int:
|
|
343
368
|
browser_proc.kill()
|
344
369
|
|
345
370
|
|
371
|
+
def run_server(args: argparse.Namespace) -> int:
|
372
|
+
interactive = args.interactive
|
373
|
+
compile_server = CompileServer(
|
374
|
+
disable_auto_clean=args.no_auto_clean, interactive=interactive
|
375
|
+
)
|
376
|
+
print(f"Server started at {compile_server.url()}")
|
377
|
+
compile_server.start()
|
378
|
+
compile_server.wait_for_startup()
|
379
|
+
try:
|
380
|
+
while True:
|
381
|
+
if not compile_server.proceess_running():
|
382
|
+
print("Server process is not running. Exiting...")
|
383
|
+
return 1
|
384
|
+
time.sleep(1)
|
385
|
+
except KeyboardInterrupt:
|
386
|
+
print("\nExiting from server...")
|
387
|
+
return 1
|
388
|
+
finally:
|
389
|
+
compile_server.stop()
|
390
|
+
return 0
|
391
|
+
|
392
|
+
|
393
|
+
def main() -> int:
|
394
|
+
args = parse_args()
|
395
|
+
target_dir = Path(args.directory)
|
396
|
+
cwd_is_target_dir = target_dir == Path(os.getcwd())
|
397
|
+
force_server = cwd_is_target_dir and looks_like_fastled_repo(target_dir)
|
398
|
+
auto_server = (args.server or args.interactive or cwd_is_target_dir) and (
|
399
|
+
not args.web and not args.just_compile
|
400
|
+
)
|
401
|
+
if auto_server or force_server:
|
402
|
+
print("Running in server only mode.")
|
403
|
+
return run_server(args)
|
404
|
+
else:
|
405
|
+
print("Running in client/server mode.")
|
406
|
+
return run_client(args)
|
407
|
+
|
408
|
+
|
346
409
|
if __name__ == "__main__":
|
347
410
|
try:
|
348
411
|
sys.argv.append("examples/wasm")
|
412
|
+
sys.argv.append("-w")
|
413
|
+
sys.argv.append("localhost")
|
349
414
|
sys.exit(main())
|
350
415
|
except KeyboardInterrupt:
|
351
416
|
print("\nExiting from main...")
|
fastled/compile_server.py
CHANGED
@@ -2,6 +2,7 @@ import socket
|
|
2
2
|
import subprocess
|
3
3
|
import threading
|
4
4
|
import time
|
5
|
+
from pathlib import Path
|
5
6
|
from typing import Optional
|
6
7
|
|
7
8
|
import httpx
|
@@ -10,10 +11,10 @@ from fastled.docker_manager import DockerManager
|
|
10
11
|
|
11
12
|
_DEFAULT_CONTAINER_NAME = "fastled-wasm-compiler"
|
12
13
|
|
13
|
-
|
14
|
+
SERVER_PORT = 9021
|
14
15
|
|
15
16
|
|
16
|
-
def
|
17
|
+
def find_available_port(start_port: int = SERVER_PORT) -> int:
|
17
18
|
"""Find an available port starting from the given port."""
|
18
19
|
port = start_port
|
19
20
|
end_port = start_port + 1000
|
@@ -26,16 +27,38 @@ def _find_available_port(start_port: int = _DEFAULT_START_PORT) -> int:
|
|
26
27
|
raise RuntimeError("No available ports found")
|
27
28
|
|
28
29
|
|
30
|
+
def looks_like_fastled_repo(directory: Path) -> bool:
|
31
|
+
libprops = directory / "library.properties"
|
32
|
+
if not libprops.exists():
|
33
|
+
return False
|
34
|
+
txt = libprops.read_text()
|
35
|
+
return "FastLED" in txt
|
36
|
+
|
37
|
+
|
29
38
|
class CompileServer:
|
30
39
|
def __init__(
|
31
|
-
self,
|
40
|
+
self,
|
41
|
+
container_name=_DEFAULT_CONTAINER_NAME,
|
42
|
+
disable_auto_clean: bool = False,
|
43
|
+
interactive: bool = False,
|
32
44
|
) -> None:
|
45
|
+
|
46
|
+
cwd = Path(".").resolve()
|
47
|
+
fastled_src_dir: Path | None = None
|
48
|
+
if looks_like_fastled_repo(cwd):
|
49
|
+
print(
|
50
|
+
"Looks like a FastLED repo, using it as the source directory and mapping it into the server."
|
51
|
+
)
|
52
|
+
fastled_src_dir = cwd / "src"
|
53
|
+
|
33
54
|
self.container_name = container_name
|
34
55
|
self.disable_auto_clean = disable_auto_clean
|
35
56
|
self.docker = DockerManager(container_name=container_name)
|
36
57
|
self.running = False
|
37
58
|
self.thread: Optional[threading.Thread] = None
|
38
59
|
self.running_process: subprocess.Popen | None = None
|
60
|
+
self.fastled_src_dir: Path | None = fastled_src_dir
|
61
|
+
self.interactive = interactive
|
39
62
|
self._port = self.start()
|
40
63
|
|
41
64
|
def port(self) -> int:
|
@@ -54,7 +77,9 @@ class CompileServer:
|
|
54
77
|
# use httpx to ping the server
|
55
78
|
# if successful, return True
|
56
79
|
try:
|
57
|
-
response = httpx.get(
|
80
|
+
response = httpx.get(
|
81
|
+
f"http://localhost:{self._port}", follow_redirects=True
|
82
|
+
)
|
58
83
|
if response.status_code < 400:
|
59
84
|
return True
|
60
85
|
except KeyboardInterrupt:
|
@@ -98,7 +123,7 @@ class CompileServer:
|
|
98
123
|
except Exception as e:
|
99
124
|
print(f"Warning: Failed to remove existing container: {e}")
|
100
125
|
|
101
|
-
print("Ensuring Docker image exists")
|
126
|
+
print("Ensuring Docker image exists at latest version")
|
102
127
|
if not self.docker.ensure_image_exists():
|
103
128
|
print("Failed to ensure Docker image exists.")
|
104
129
|
raise RuntimeError("Failed to ensure Docker image exists")
|
@@ -109,14 +134,35 @@ class CompileServer:
|
|
109
134
|
# subprocess.run(["docker", "rmi", "fastled-wasm"], capture_output=True)
|
110
135
|
# print("All clean")
|
111
136
|
|
112
|
-
port =
|
137
|
+
port = find_available_port()
|
138
|
+
print(f"Found an available port: {port}")
|
113
139
|
# server_command = ["python", "/js/run.py", "server", "--allow-shutdown"]
|
114
|
-
|
140
|
+
if self.interactive:
|
141
|
+
server_command = ["/bin/bash"]
|
142
|
+
else:
|
143
|
+
server_command = ["python", "/js/run.py", "server"]
|
115
144
|
if self.disable_auto_clean:
|
116
145
|
server_command.append("--disable-auto-clean")
|
117
146
|
print(f"Started Docker container with command: {server_command}")
|
118
147
|
ports = {port: 80}
|
119
|
-
|
148
|
+
volumes = None
|
149
|
+
if self.fastled_src_dir:
|
150
|
+
print(
|
151
|
+
f"Mounting FastLED source directory {self.fastled_src_dir} into container /js/fastled/src"
|
152
|
+
)
|
153
|
+
volumes = {
|
154
|
+
str(self.fastled_src_dir): {"bind": "/js/fastled/src", "mode": "rw"}
|
155
|
+
}
|
156
|
+
# no auto-update because the source directory is mapped in.
|
157
|
+
server_command.append("--no-auto-update") # stop git repo updates.
|
158
|
+
if not self.interactive:
|
159
|
+
server_command.append(
|
160
|
+
"--no-sketch-cache"
|
161
|
+
) # Remove sketch cache which assumes src is static.
|
162
|
+
self.running_process = self.docker.run_container(
|
163
|
+
server_command, ports=ports, volumes=volumes
|
164
|
+
)
|
165
|
+
print("Compile server starting")
|
120
166
|
time.sleep(3)
|
121
167
|
if self.running_process.poll() is not None:
|
122
168
|
print("Server failed to start")
|
@@ -124,38 +170,47 @@ class CompileServer:
|
|
124
170
|
raise RuntimeError("Server failed to start")
|
125
171
|
self.thread = threading.Thread(target=self._server_loop, daemon=True)
|
126
172
|
self.thread.start()
|
127
|
-
|
173
|
+
|
128
174
|
return port
|
129
175
|
|
176
|
+
def proceess_running(self) -> bool:
|
177
|
+
if self.running_process is None:
|
178
|
+
return False
|
179
|
+
return self.running_process.poll() is None
|
180
|
+
|
130
181
|
def stop(self) -> None:
|
131
182
|
print(f"Stopping server on port {self._port}")
|
132
183
|
# attempt to send a shutdown signal to the server
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
184
|
+
try:
|
185
|
+
httpx.get(f"http://localhost:{self._port}/shutdown", timeout=2)
|
186
|
+
# except Exception:
|
187
|
+
except Exception as e:
|
188
|
+
print(f"Failed to send shutdown signal: {e}")
|
189
|
+
pass
|
190
|
+
try:
|
191
|
+
# Stop the Docker container
|
192
|
+
cp: subprocess.CompletedProcess
|
193
|
+
cp = subprocess.run(
|
194
|
+
["docker", "stop", self.container_name],
|
195
|
+
capture_output=True,
|
196
|
+
text=True,
|
197
|
+
check=True,
|
198
|
+
)
|
199
|
+
if cp.returncode != 0:
|
200
|
+
print(f"Failed to stop Docker container: {cp.stderr}")
|
201
|
+
|
202
|
+
cp = subprocess.run(
|
203
|
+
["docker", "rm", self.container_name],
|
204
|
+
capture_output=True,
|
205
|
+
text=True,
|
206
|
+
check=True,
|
207
|
+
)
|
208
|
+
if cp.returncode != 0:
|
209
|
+
print(f"Failed to remove Docker container: {cp.stderr}")
|
155
210
|
|
156
|
-
|
157
|
-
|
158
|
-
|
211
|
+
# Close the stdout pipe
|
212
|
+
if self.running_process and self.running_process.stdout:
|
213
|
+
self.running_process.stdout.close()
|
159
214
|
|
160
215
|
# Wait for the process to fully terminate with a timeout
|
161
216
|
self.running_process.wait(timeout=10)
|
@@ -167,17 +222,19 @@ class CompileServer:
|
|
167
222
|
f"Server stopped with return code {self.running_process.returncode}"
|
168
223
|
)
|
169
224
|
|
170
|
-
|
171
|
-
|
225
|
+
except subprocess.TimeoutExpired:
|
226
|
+
# Force kill if it doesn't stop gracefully
|
227
|
+
if self.running_process:
|
172
228
|
self.running_process.kill()
|
173
229
|
self.running_process.wait()
|
174
|
-
|
230
|
+
except KeyboardInterrupt:
|
231
|
+
if self.running_process:
|
175
232
|
self.running_process.kill()
|
176
233
|
self.running_process.wait()
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
234
|
+
except Exception as e:
|
235
|
+
print(f"Error stopping Docker container: {e}")
|
236
|
+
finally:
|
237
|
+
self.running_process = None
|
181
238
|
# Signal the server thread to stop
|
182
239
|
self.running = False
|
183
240
|
if self.thread:
|
fastled/docker_manager.py
CHANGED
@@ -210,15 +210,15 @@ class DockerManager:
|
|
210
210
|
def run_container(
|
211
211
|
self,
|
212
212
|
cmd: list[str],
|
213
|
-
volumes: dict[str, str] | None = None,
|
213
|
+
volumes: dict[str, dict[str, str]] | None = None,
|
214
214
|
ports: dict[int, int] | None = None,
|
215
215
|
) -> subprocess.Popen:
|
216
216
|
"""Run the Docker container with the specified volume.
|
217
217
|
|
218
218
|
Args:
|
219
|
-
|
220
|
-
|
221
|
-
|
219
|
+
cmd: Command to run in the container
|
220
|
+
volumes: Dict mapping host paths to dicts with 'bind' and 'mode' keys
|
221
|
+
ports: Dict mapping host ports to container ports
|
222
222
|
"""
|
223
223
|
volumes = volumes or {}
|
224
224
|
ports = ports or {}
|
@@ -237,8 +237,10 @@ class DockerManager:
|
|
237
237
|
for host_port, container_port in ports.items():
|
238
238
|
docker_command.extend(["-p", f"{host_port}:{container_port}"])
|
239
239
|
if volumes:
|
240
|
-
for host_path,
|
241
|
-
docker_command.extend(
|
240
|
+
for host_path, mount_spec in volumes.items():
|
241
|
+
docker_command.extend(
|
242
|
+
["-v", f"{host_path}:{mount_spec['bind']}:{mount_spec['mode']}"]
|
243
|
+
)
|
242
244
|
|
243
245
|
docker_command.extend(
|
244
246
|
[
|
fastled/web_compile.py
CHANGED
@@ -1,173 +1,220 @@
|
|
1
|
-
import shutil
|
2
|
-
import tempfile
|
3
|
-
from
|
4
|
-
from
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
return
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
1
|
+
import shutil
|
2
|
+
import tempfile
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import httpx
|
8
|
+
|
9
|
+
from fastled.build_mode import BuildMode
|
10
|
+
from fastled.compile_server import SERVER_PORT
|
11
|
+
|
12
|
+
DEFAULT_HOST = "https://fastled.onrender.com"
|
13
|
+
ENDPOINT_COMPILED_WASM = "compile/wasm"
|
14
|
+
_TIMEOUT = 60 * 4 # 2 mins timeout
|
15
|
+
_AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class TestConnectionResult:
|
20
|
+
host: str
|
21
|
+
success: bool
|
22
|
+
ipv4: bool
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class WebCompileResult:
|
27
|
+
success: bool
|
28
|
+
stdout: str
|
29
|
+
hash_value: str | None
|
30
|
+
zip_bytes: bytes
|
31
|
+
|
32
|
+
def __bool__(self) -> bool:
|
33
|
+
return self.success
|
34
|
+
|
35
|
+
|
36
|
+
def _sanitize_host(host: str) -> str:
|
37
|
+
if host.startswith("http"):
|
38
|
+
return host
|
39
|
+
is_local_host = "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
|
40
|
+
use_https = not is_local_host
|
41
|
+
if use_https:
|
42
|
+
return host if host.startswith("https://") else f"https://{host}"
|
43
|
+
return host if host.startswith("http://") else f"http://{host}"
|
44
|
+
|
45
|
+
|
46
|
+
_CONNECTION_ERROR_MAP: dict[str, TestConnectionResult] = {}
|
47
|
+
|
48
|
+
|
49
|
+
def _test_connection(host: str, use_ipv4: bool) -> TestConnectionResult:
|
50
|
+
key = f"{host}-{use_ipv4}"
|
51
|
+
maybe_result: TestConnectionResult | None = _CONNECTION_ERROR_MAP.get(key)
|
52
|
+
if maybe_result is not None:
|
53
|
+
return maybe_result
|
54
|
+
transport = httpx.HTTPTransport(local_address="0.0.0.0") if use_ipv4 else None
|
55
|
+
try:
|
56
|
+
with httpx.Client(
|
57
|
+
timeout=_TIMEOUT,
|
58
|
+
transport=transport,
|
59
|
+
) as test_client:
|
60
|
+
test_response = test_client.get(
|
61
|
+
f"{host}/healthz", timeout=_TIMEOUT, follow_redirects=True
|
62
|
+
)
|
63
|
+
result = TestConnectionResult(
|
64
|
+
host, test_response.status_code == 200, use_ipv4
|
65
|
+
)
|
66
|
+
_CONNECTION_ERROR_MAP[key] = result
|
67
|
+
except Exception:
|
68
|
+
result = TestConnectionResult(host, False, use_ipv4)
|
69
|
+
_CONNECTION_ERROR_MAP[key] = result
|
70
|
+
return result
|
71
|
+
|
72
|
+
|
73
|
+
def web_compile(
|
74
|
+
directory: Path,
|
75
|
+
host: str | None = None,
|
76
|
+
auth_token: str | None = None,
|
77
|
+
build_mode: BuildMode | None = None,
|
78
|
+
profile: bool = False,
|
79
|
+
) -> WebCompileResult:
|
80
|
+
host = _sanitize_host(host or DEFAULT_HOST)
|
81
|
+
print("Compiling on", host)
|
82
|
+
auth_token = auth_token or _AUTH_TOKEN
|
83
|
+
# zip up the files
|
84
|
+
print("Zipping files...")
|
85
|
+
|
86
|
+
# Create a temporary zip file
|
87
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
|
88
|
+
# Create temporary directory for organizing files
|
89
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
90
|
+
# Create wasm subdirectory
|
91
|
+
wasm_dir = Path(temp_dir) / "wasm"
|
92
|
+
|
93
|
+
# Copy all files from source to wasm subdirectory, excluding fastled_js
|
94
|
+
def ignore_fastled_js(dir: str, files: list[str]) -> list[str]:
|
95
|
+
if "fastled_js" in dir:
|
96
|
+
return files
|
97
|
+
if dir.startswith("."):
|
98
|
+
return files
|
99
|
+
return []
|
100
|
+
|
101
|
+
shutil.copytree(directory, wasm_dir, ignore=ignore_fastled_js)
|
102
|
+
# Create zip archive from the temp directory
|
103
|
+
shutil.make_archive(tmp_zip.name[:-4], "zip", temp_dir)
|
104
|
+
|
105
|
+
print(f"Web compiling on {host}...")
|
106
|
+
|
107
|
+
try:
|
108
|
+
with open(tmp_zip.name, "rb") as zip_file:
|
109
|
+
files = {"file": ("wasm.zip", zip_file, "application/x-zip-compressed")}
|
110
|
+
urls = [host]
|
111
|
+
domain = host.split("://")[-1]
|
112
|
+
if ":" not in domain:
|
113
|
+
urls.append(f"{host}:{SERVER_PORT}")
|
114
|
+
test_connection_result: TestConnectionResult | None = None
|
115
|
+
|
116
|
+
with ThreadPoolExecutor(max_workers=len(urls)) as executor:
|
117
|
+
futures: list = []
|
118
|
+
ip_versions = [True, False] if "localhost" not in host else [True]
|
119
|
+
for ipv4 in ip_versions:
|
120
|
+
for url in urls:
|
121
|
+
f = executor.submit(_test_connection, url, ipv4)
|
122
|
+
futures.append(f)
|
123
|
+
|
124
|
+
succeeded = False
|
125
|
+
for future in as_completed(futures):
|
126
|
+
result: TestConnectionResult = future.result()
|
127
|
+
|
128
|
+
if result.success:
|
129
|
+
print(f"Connection successful to {result.host}")
|
130
|
+
succeeded = True
|
131
|
+
# host = test_url
|
132
|
+
test_connection_result = result
|
133
|
+
break
|
134
|
+
else:
|
135
|
+
print(f"Ignoring {result.host} due to connection failure")
|
136
|
+
|
137
|
+
if not succeeded:
|
138
|
+
print("Connection failed to all endpoints")
|
139
|
+
return WebCompileResult(
|
140
|
+
success=False,
|
141
|
+
stdout="Connection failed",
|
142
|
+
hash_value=None,
|
143
|
+
zip_bytes=b"",
|
144
|
+
)
|
145
|
+
assert test_connection_result is not None
|
146
|
+
ipv4_stmt = "IPv4" if test_connection_result.ipv4 else "IPv6"
|
147
|
+
transport = (
|
148
|
+
httpx.HTTPTransport(local_address="0.0.0.0")
|
149
|
+
if test_connection_result.ipv4
|
150
|
+
else None
|
151
|
+
)
|
152
|
+
with httpx.Client(
|
153
|
+
transport=transport,
|
154
|
+
timeout=_TIMEOUT,
|
155
|
+
) as client:
|
156
|
+
headers = {
|
157
|
+
"accept": "application/json",
|
158
|
+
"authorization": auth_token,
|
159
|
+
"build": (
|
160
|
+
build_mode.value.lower()
|
161
|
+
if build_mode
|
162
|
+
else BuildMode.QUICK.value.lower()
|
163
|
+
),
|
164
|
+
"profile": "true" if profile else "false",
|
165
|
+
}
|
166
|
+
|
167
|
+
url = f"{test_connection_result.host}/{ENDPOINT_COMPILED_WASM}"
|
168
|
+
print(f"Compiling on {url} via {ipv4_stmt}")
|
169
|
+
response = client.post(
|
170
|
+
url,
|
171
|
+
follow_redirects=True,
|
172
|
+
files=files,
|
173
|
+
headers=headers,
|
174
|
+
timeout=_TIMEOUT,
|
175
|
+
)
|
176
|
+
|
177
|
+
if response.status_code != 200:
|
178
|
+
json_response = response.json()
|
179
|
+
detail = json_response.get("detail", "Could not compile")
|
180
|
+
return WebCompileResult(
|
181
|
+
success=False, stdout=detail, hash_value=None, zip_bytes=b""
|
182
|
+
)
|
183
|
+
|
184
|
+
print(f"Response status code: {response}")
|
185
|
+
# Create a temporary directory to extract the zip
|
186
|
+
with tempfile.TemporaryDirectory() as extract_dir:
|
187
|
+
extract_path = Path(extract_dir)
|
188
|
+
|
189
|
+
# Write the response content to a temporary zip file
|
190
|
+
temp_zip = extract_path / "response.zip"
|
191
|
+
temp_zip.write_bytes(response.content)
|
192
|
+
|
193
|
+
# Extract the zip
|
194
|
+
shutil.unpack_archive(temp_zip, extract_path, "zip")
|
195
|
+
|
196
|
+
# Read stdout from out.txt if it exists
|
197
|
+
stdout_file = extract_path / "out.txt"
|
198
|
+
hash_file = extract_path / "hash.txt"
|
199
|
+
stdout = stdout_file.read_text() if stdout_file.exists() else ""
|
200
|
+
hash_value = hash_file.read_text() if hash_file.exists() else None
|
201
|
+
|
202
|
+
return WebCompileResult(
|
203
|
+
success=True,
|
204
|
+
stdout=stdout,
|
205
|
+
hash_value=hash_value,
|
206
|
+
zip_bytes=response.content,
|
207
|
+
)
|
208
|
+
except KeyboardInterrupt:
|
209
|
+
print("Keyboard interrupt")
|
210
|
+
raise
|
211
|
+
except httpx.HTTPError as e:
|
212
|
+
print(f"Error: {e}")
|
213
|
+
return WebCompileResult(
|
214
|
+
success=False, stdout=str(e), hash_value=None, zip_bytes=b""
|
215
|
+
)
|
216
|
+
finally:
|
217
|
+
try:
|
218
|
+
Path(tmp_zip.name).unlink()
|
219
|
+
except PermissionError:
|
220
|
+
print("Warning: Could not delete temporary zip file")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastled
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.3
|
4
4
|
Summary: FastLED Wasm Compiler
|
5
5
|
Home-page: https://github.com/zackees/fastled-wasm
|
6
6
|
Maintainer: Zachary Vorhies
|
@@ -28,7 +28,6 @@ Compiles an Arduino/Platformio sketch into a wasm binary that can be run directl
|
|
28
28
|
[](https://github.com/zackees/fastled-wasm/actions/workflows/test_win.yml)
|
29
29
|
|
30
30
|
|
31
|
-
|
32
31
|
# About
|
33
32
|
|
34
33
|
This python app will compile your FastLED style sketches into html/js/wasm output that runs directly in the browser.
|
@@ -92,6 +91,9 @@ provide shims for most of the common api points.
|
|
92
91
|
|
93
92
|
# Revisions
|
94
93
|
|
94
|
+
* 1.1.3 - Live editing of *.h and *.cpp files is now possible. Sketch cache will be disabled in this mode.
|
95
|
+
* 1.1.2 - `--server` will now volume map fastled src directory if it detects this. This was also implemented on the docker side.
|
96
|
+
* 1.1.1 - `--interactive` is now supported to debug the container. Volume maps and better compatibilty with ipv4/v6 by concurrent connection finding.
|
95
97
|
* 1.1.0 - Use `fastled` as the command for the wasm compiler.
|
96
98
|
* 1.0.17 - Pulls updates when necessary. Removed dependency on keyring.
|
97
99
|
* 1.0.16 - `fastled-wasm` package name has been changed to `fled`
|
@@ -1,18 +1,18 @@
|
|
1
1
|
fastled/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
fastled/app.py,sha256=
|
2
|
+
fastled/app.py,sha256=duSkE31cggy363rFZRHjKIzc0ijCA_TmmPXOLAj-jEM,13560
|
3
3
|
fastled/build_mode.py,sha256=joMwsV4K1y_LijT4gEAcjx69RZBoe_KmFmHZdPYbL_4,631
|
4
4
|
fastled/check_cpp_syntax.py,sha256=YxRJm7cFPv4bdhL1v_KOkBz8RL86ihayoJYvclr69ms,1024
|
5
5
|
fastled/cli.py,sha256=CNR_pQR0sNVPNuv8e_nmm-0PI8sU-eUBUgnWgWkzW9c,237
|
6
|
-
fastled/compile_server.py,sha256=
|
7
|
-
fastled/docker_manager.py,sha256=
|
6
|
+
fastled/compile_server.py,sha256=QqxTWYmC_Fej-hTNRjfa8qjyGusLY6L858zwTciC9AM,9903
|
7
|
+
fastled/docker_manager.py,sha256=WcOKa3EpIPAjICPfTL87CUYuAmX9KYT6L_Hcqbj95eE,9028
|
8
8
|
fastled/filewatcher.py,sha256=SHKx9Dnt4EJiT-iPYakdPZBRIL1gsJGN9tY8FJW2myU,5079
|
9
9
|
fastled/open_browser.py,sha256=-VhpGmydwLCcXmrDD2esMEdJPZYcoX2Mt73eb88Nna0,1392
|
10
10
|
fastled/paths.py,sha256=VsPmgu0lNSCFOoEC0BsTYzDygXqy15AHUfN-tTuzDZA,99
|
11
|
-
fastled/web_compile.py,sha256=
|
11
|
+
fastled/web_compile.py,sha256=rCMClX8yUtZlAzrgFB-r5Pdt7l9TJWhHXe-u99Hmdvw,8257
|
12
12
|
fastled/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
|
13
|
-
fastled-1.1.
|
14
|
-
fastled-1.1.
|
15
|
-
fastled-1.1.
|
16
|
-
fastled-1.1.
|
17
|
-
fastled-1.1.
|
18
|
-
fastled-1.1.
|
13
|
+
fastled-1.1.3.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
14
|
+
fastled-1.1.3.dist-info/METADATA,sha256=C2OkZwJd57L96Gauxz_CZKMmk7bmNlv_VhvghbFdMD8,5687
|
15
|
+
fastled-1.1.3.dist-info/WHEEL,sha256=0VNUDWQJzfRahYI3neAhz2UVbRCtztpN5dPHAGvmGXc,109
|
16
|
+
fastled-1.1.3.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
|
17
|
+
fastled-1.1.3.dist-info/top_level.txt,sha256=xfG6Z_ol9V5YmBROkZq2QTRwjbS2ouCUxaTJsOwfkOo,14
|
18
|
+
fastled-1.1.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|