fastled 1.2.33__py3-none-any.whl → 1.4.50__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 +51 -192
- fastled/__main__.py +14 -0
- fastled/__version__.py +6 -0
- fastled/app.py +124 -27
- fastled/args.py +124 -0
- fastled/assets/localhost-key.pem +28 -0
- fastled/assets/localhost.pem +27 -0
- fastled/cli.py +10 -2
- fastled/cli_test.py +21 -0
- fastled/cli_test_interactive.py +21 -0
- fastled/client_server.py +334 -55
- fastled/compile_server.py +12 -1
- fastled/compile_server_impl.py +115 -42
- fastled/docker_manager.py +392 -69
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +100 -8
- fastled/find_good_connection.py +105 -0
- fastled/header_dump.py +63 -0
- fastled/install/__init__.py +1 -0
- fastled/install/examples_manager.py +62 -0
- fastled/install/extension_manager.py +113 -0
- fastled/install/main.py +156 -0
- fastled/install/project_detection.py +167 -0
- fastled/install/test_install.py +373 -0
- fastled/install/vscode_config.py +344 -0
- fastled/interruptible_http.py +148 -0
- fastled/keyboard.py +1 -0
- fastled/keyz.py +84 -0
- fastled/live_client.py +26 -1
- fastled/open_browser.py +133 -89
- fastled/parse_args.py +219 -15
- fastled/playwright/chrome_extension_downloader.py +207 -0
- fastled/playwright/playwright_browser.py +773 -0
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -0
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -17
- fastled/server_flask.py +487 -0
- fastled/server_start.py +21 -0
- fastled/settings.py +53 -4
- fastled/site/build.py +2 -10
- fastled/site/examples.py +10 -0
- fastled/sketch.py +129 -7
- fastled/string_diff.py +218 -9
- fastled/test/examples.py +7 -5
- fastled/types.py +22 -2
- fastled/util.py +78 -0
- fastled/version.py +41 -0
- fastled/web_compile.py +401 -218
- fastled/zip_files.py +76 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
- fastled-1.4.50.dist-info/RECORD +60 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
- fastled/open_browser2.py +0 -111
- fastled-1.2.33.dist-info/RECORD +0 -33
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/server_flask.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from multiprocessing import Process
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from livereload import Server
|
|
9
|
+
|
|
10
|
+
# Logging configuration
|
|
11
|
+
# _ENABLE_LOGGING = os.environ.get("FLASK_SERVER_LOGGING", "0") == "1"
|
|
12
|
+
_ENABLE_LOGGING = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if _ENABLE_LOGGING:
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
level=logging.INFO,
|
|
18
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
19
|
+
)
|
|
20
|
+
logger = logging.getLogger("flask_server")
|
|
21
|
+
else:
|
|
22
|
+
# Disable all logging
|
|
23
|
+
logging.getLogger("flask_server").addHandler(logging.NullHandler())
|
|
24
|
+
logging.getLogger("flask_server").propagate = False
|
|
25
|
+
logger = logging.getLogger("flask_server")
|
|
26
|
+
logger.disabled = True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_dwarf_source(path: str) -> bool:
|
|
30
|
+
"""Check if the path is a dwarf source file."""
|
|
31
|
+
logger.debug(f"_is_dwarf_source called with path: {path}")
|
|
32
|
+
if "dwarfsource" in path:
|
|
33
|
+
logger.debug(f"Path '{path}' contains 'dwarfsource'")
|
|
34
|
+
return True
|
|
35
|
+
_is_dwarf_source = False
|
|
36
|
+
for p in ["fastledsource", "sketchsource", "dwarfsource", "drawfsource"]:
|
|
37
|
+
if p in path:
|
|
38
|
+
_is_dwarf_source = True
|
|
39
|
+
logger.debug(f"Path '{p}' contains '{path}'")
|
|
40
|
+
break
|
|
41
|
+
if not _is_dwarf_source:
|
|
42
|
+
logger.debug(f"Path '{path}' is not a dwarf source file")
|
|
43
|
+
return _is_dwarf_source
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_flask_server(
|
|
47
|
+
fastled_js: Path,
|
|
48
|
+
port: int,
|
|
49
|
+
compile_server_port: int,
|
|
50
|
+
certfile: Path | None = None,
|
|
51
|
+
keyfile: Path | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Run Flask server with live reload in a subprocess
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
fastled_js: Path to the fastled_js directory
|
|
57
|
+
port: Port to run the server on
|
|
58
|
+
certfile: Path to the SSL certificate file
|
|
59
|
+
keyfile: Path to the SSL key file
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
from flask import Flask, Response, request, send_from_directory
|
|
63
|
+
from flask_cors import CORS
|
|
64
|
+
|
|
65
|
+
app = Flask(__name__)
|
|
66
|
+
|
|
67
|
+
# Enable CORS for all domains on all routes
|
|
68
|
+
CORS(
|
|
69
|
+
app,
|
|
70
|
+
resources={
|
|
71
|
+
r"/*": {
|
|
72
|
+
"origins": "*",
|
|
73
|
+
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
74
|
+
"allow_headers": [
|
|
75
|
+
"Content-Type",
|
|
76
|
+
"Authorization",
|
|
77
|
+
"X-Requested-With",
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Must be a full path or flask will fail to find the file.
|
|
84
|
+
fastled_js = fastled_js.resolve()
|
|
85
|
+
|
|
86
|
+
# logger.error(f"Server error: {e}")
|
|
87
|
+
|
|
88
|
+
@app.after_request
|
|
89
|
+
def add_security_headers(response):
|
|
90
|
+
"""Add security headers required for cross-origin isolation and audio worklets"""
|
|
91
|
+
# Required for SharedArrayBuffer and audio worklets
|
|
92
|
+
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
|
|
93
|
+
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
|
|
94
|
+
return response
|
|
95
|
+
|
|
96
|
+
@app.before_request
|
|
97
|
+
def log_request_info():
|
|
98
|
+
"""Log details of each request before processing"""
|
|
99
|
+
if _ENABLE_LOGGING:
|
|
100
|
+
logger.info("=" * 80)
|
|
101
|
+
logger.info(f"Request: {request.method} {request.url}")
|
|
102
|
+
logger.info(f"Headers: {dict(request.headers)}")
|
|
103
|
+
logger.info(f"Args: {dict(request.args)}")
|
|
104
|
+
if request.is_json:
|
|
105
|
+
logger.info(f"JSON: {request.json}")
|
|
106
|
+
if request.form:
|
|
107
|
+
logger.info(f"Form: {dict(request.form)}")
|
|
108
|
+
logger.info(f"Remote addr: {request.remote_addr}")
|
|
109
|
+
logger.info(f"User agent: {request.user_agent}")
|
|
110
|
+
|
|
111
|
+
@app.route("/")
|
|
112
|
+
def serve_index():
|
|
113
|
+
if _ENABLE_LOGGING:
|
|
114
|
+
logger.info("Serving index.html")
|
|
115
|
+
response = send_from_directory(fastled_js, "index.html")
|
|
116
|
+
if _ENABLE_LOGGING:
|
|
117
|
+
logger.info(f"Index response status: {response.status_code}")
|
|
118
|
+
return response
|
|
119
|
+
|
|
120
|
+
@app.route("/sourcefiles/<path:path>")
|
|
121
|
+
def serve_source_files(path):
|
|
122
|
+
"""Proxy requests to /sourcefiles/* to the compile server"""
|
|
123
|
+
from flask import request
|
|
124
|
+
|
|
125
|
+
start_time = time.time()
|
|
126
|
+
logger.info(f"Serving source file: {path}")
|
|
127
|
+
|
|
128
|
+
# Forward the request to the compile server
|
|
129
|
+
target_url = f"http://localhost:{compile_server_port}/sourcefiles/{path}"
|
|
130
|
+
logger.info(f"Forwarding to: {target_url}")
|
|
131
|
+
|
|
132
|
+
# Log request headers
|
|
133
|
+
request_headers = {
|
|
134
|
+
key: value for key, value in request.headers if key != "Host"
|
|
135
|
+
}
|
|
136
|
+
logger.debug(f"Request headers: {request_headers}")
|
|
137
|
+
|
|
138
|
+
# Forward the request with the same method, headers, and body
|
|
139
|
+
try:
|
|
140
|
+
with httpx.Client() as client:
|
|
141
|
+
resp = client.request(
|
|
142
|
+
method=request.method,
|
|
143
|
+
url=target_url,
|
|
144
|
+
headers=request_headers,
|
|
145
|
+
content=request.get_data(),
|
|
146
|
+
cookies=request.cookies,
|
|
147
|
+
follow_redirects=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
logger.info(f"Response status: {resp.status_code}")
|
|
151
|
+
logger.debug(f"Response headers: {dict(resp.headers)}")
|
|
152
|
+
|
|
153
|
+
# Create a Flask Response object from the httpx response
|
|
154
|
+
raw_data = resp.content
|
|
155
|
+
logger.debug(f"Response size: {len(raw_data)} bytes")
|
|
156
|
+
|
|
157
|
+
response = Response(
|
|
158
|
+
raw_data, status=resp.status_code, headers=dict(resp.headers)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
elapsed_time = time.time() - start_time
|
|
162
|
+
logger.info(f"Request completed in {elapsed_time:.3f} seconds")
|
|
163
|
+
|
|
164
|
+
return response
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"Error forwarding request: {e}", exc_info=True)
|
|
168
|
+
return Response(f"Error: {str(e)}", status=500)
|
|
169
|
+
|
|
170
|
+
def handle_dwarfsource(path: str) -> Response:
|
|
171
|
+
"""Handle requests to /sourcefiles/*"""
|
|
172
|
+
from flask import Response, request
|
|
173
|
+
|
|
174
|
+
# Request body should be
|
|
175
|
+
# {
|
|
176
|
+
# "path": "string"
|
|
177
|
+
# }
|
|
178
|
+
|
|
179
|
+
start_time = time.time()
|
|
180
|
+
logger.info("\n##################################")
|
|
181
|
+
logger.info(f"# Serving source file /sourcefiles/ {path}")
|
|
182
|
+
logger.info("##################################\n")
|
|
183
|
+
|
|
184
|
+
logger.info(f"Processing sourcefile request for {path}")
|
|
185
|
+
|
|
186
|
+
# Forward the request to the compile server
|
|
187
|
+
target_url = f"http://localhost:{compile_server_port}/dwarfsource"
|
|
188
|
+
logger.info(f"Forwarding to: {target_url}")
|
|
189
|
+
|
|
190
|
+
# Log request headers
|
|
191
|
+
request_headers = {
|
|
192
|
+
key: value for key, value in request.headers if key != "Host"
|
|
193
|
+
}
|
|
194
|
+
logger.debug(f"Request headers: {request_headers}")
|
|
195
|
+
|
|
196
|
+
body: dict[str, str] = {
|
|
197
|
+
"path": path,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
# Forward the request with the same method, headers, and body
|
|
202
|
+
with httpx.Client() as client:
|
|
203
|
+
resp = client.request(
|
|
204
|
+
method="POST",
|
|
205
|
+
url=target_url,
|
|
206
|
+
headers=request_headers,
|
|
207
|
+
json=body,
|
|
208
|
+
cookies=request.cookies,
|
|
209
|
+
follow_redirects=True,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
logger.info(f"Response status: {resp.status_code}")
|
|
213
|
+
logger.debug(f"Response headers: {dict(resp.headers)}")
|
|
214
|
+
|
|
215
|
+
# Create a Flask Response object from the httpx response
|
|
216
|
+
raw_data = resp.content
|
|
217
|
+
logger.debug(f"Response size: {len(raw_data)} bytes")
|
|
218
|
+
|
|
219
|
+
response = Response(
|
|
220
|
+
raw_data, status=resp.status_code, headers=dict(resp.headers)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
elapsed_time = time.time() - start_time
|
|
224
|
+
logger.info(f"Request completed in {elapsed_time:.3f} seconds")
|
|
225
|
+
|
|
226
|
+
return response
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f"Error handling sourcefile request: {e}", exc_info=True)
|
|
230
|
+
return Response(f"Error: {str(e)}", status=500)
|
|
231
|
+
|
|
232
|
+
def handle_local_file_fetch(path: str) -> Response:
|
|
233
|
+
start_time = time.time()
|
|
234
|
+
logger.info("\n##################################")
|
|
235
|
+
logger.info(f"# Serving generic file {path}")
|
|
236
|
+
logger.info("##################################\n")
|
|
237
|
+
|
|
238
|
+
logger.info(f"Processing local file request for {path}")
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
file_path = fastled_js / path
|
|
242
|
+
logger.info(f"Full file path: {file_path}")
|
|
243
|
+
logger.info(f"File exists: {file_path.exists()}")
|
|
244
|
+
|
|
245
|
+
# Check if file exists before trying to serve it
|
|
246
|
+
if not file_path.exists():
|
|
247
|
+
logger.warning(f"File not found: {file_path}")
|
|
248
|
+
return Response(f"File not found: {path}", status=404)
|
|
249
|
+
|
|
250
|
+
response = send_from_directory(fastled_js, path)
|
|
251
|
+
|
|
252
|
+
# Some servers don't set the Content-Type header for a bunch of files.
|
|
253
|
+
content_type = None
|
|
254
|
+
if path.endswith(".js"):
|
|
255
|
+
content_type = "application/javascript"
|
|
256
|
+
elif path.endswith(".css"):
|
|
257
|
+
content_type = "text/css"
|
|
258
|
+
elif path.endswith(".wasm"):
|
|
259
|
+
content_type = "application/wasm"
|
|
260
|
+
elif path.endswith(".json"):
|
|
261
|
+
content_type = "application/json"
|
|
262
|
+
elif path.endswith(".png"):
|
|
263
|
+
content_type = "image/png"
|
|
264
|
+
elif path.endswith(".jpg") or path.endswith(".jpeg"):
|
|
265
|
+
content_type = "image/jpeg"
|
|
266
|
+
elif path.endswith(".gif"):
|
|
267
|
+
content_type = "image/gif"
|
|
268
|
+
elif path.endswith(".svg"):
|
|
269
|
+
content_type = "image/svg+xml"
|
|
270
|
+
elif path.endswith(".ico"):
|
|
271
|
+
content_type = "image/x-icon"
|
|
272
|
+
elif path.endswith(".html"):
|
|
273
|
+
content_type = "text/html"
|
|
274
|
+
|
|
275
|
+
if content_type:
|
|
276
|
+
logger.info(f"Setting Content-Type to {content_type}")
|
|
277
|
+
response.headers["Content-Type"] = content_type
|
|
278
|
+
|
|
279
|
+
# now also add headers to force no caching
|
|
280
|
+
response.headers["Cache-Control"] = (
|
|
281
|
+
"no-cache, no-store, must-revalidate"
|
|
282
|
+
)
|
|
283
|
+
response.headers["Pragma"] = "no-cache"
|
|
284
|
+
response.headers["Expires"] = "0"
|
|
285
|
+
|
|
286
|
+
logger.info(f"Response status: {response.status_code}")
|
|
287
|
+
logger.debug(f"Response headers: {dict(response.headers)}")
|
|
288
|
+
|
|
289
|
+
elapsed_time = time.time() - start_time
|
|
290
|
+
logger.info(f"Request completed in {elapsed_time:.3f} seconds")
|
|
291
|
+
|
|
292
|
+
return response
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.error(f"Error serving local file {path}: {e}", exc_info=True)
|
|
296
|
+
# Check if this is a FileNotFoundError (which can happen with send_from_directory)
|
|
297
|
+
if isinstance(e, FileNotFoundError):
|
|
298
|
+
return Response(f"File not found: {path}", status=404)
|
|
299
|
+
return Response(f"Error serving file: {str(e)}", status=500)
|
|
300
|
+
|
|
301
|
+
@app.route("/fastapi")
|
|
302
|
+
def server_backend_redirect():
|
|
303
|
+
"""Redirect to the compile server"""
|
|
304
|
+
logger.info("Redirecting to compile server")
|
|
305
|
+
target_url = f"http://localhost:{compile_server_port}/docs"
|
|
306
|
+
logger.info(f"Redirecting to: {target_url}")
|
|
307
|
+
return Response(
|
|
308
|
+
f"Redirecting to compile server: <a href='{target_url}'>{target_url}</a>",
|
|
309
|
+
status=302,
|
|
310
|
+
headers={"Location": target_url},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
@app.route("/<path:path>")
|
|
314
|
+
def serve_files(path: str):
|
|
315
|
+
logger.info(f"Received request for path: {path}")
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
is_debug_src_code_request = _is_dwarf_source(path)
|
|
319
|
+
logger.info(f"is debug_src_code_request: {is_debug_src_code_request}")
|
|
320
|
+
if is_debug_src_code_request:
|
|
321
|
+
logger.info(f"Handling as dwarf source file request: {path}")
|
|
322
|
+
return handle_dwarfsource(path)
|
|
323
|
+
else:
|
|
324
|
+
logger.info(f"Handling as local file: {path}")
|
|
325
|
+
return handle_local_file_fetch(path)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.error(f"Error in serve_files for {path}: {e}", exc_info=True)
|
|
328
|
+
return Response(f"Server error: {str(e)}", status=500)
|
|
329
|
+
|
|
330
|
+
@app.errorhandler(Exception)
|
|
331
|
+
def handle_exception(e):
|
|
332
|
+
"""Log any uncaught exceptions"""
|
|
333
|
+
logger.error(f"Unhandled exception: {e}", exc_info=True)
|
|
334
|
+
return Response(f"Server error: {str(e)}", status=500)
|
|
335
|
+
|
|
336
|
+
logger.info("Setting up livereload server")
|
|
337
|
+
server = Server(app.wsgi_app)
|
|
338
|
+
# Watch index.html for changes
|
|
339
|
+
server.watch(str(fastled_js / "index.html"))
|
|
340
|
+
# server.watch(str(fastled_js / "index.js"))
|
|
341
|
+
# server.watch(str(fastled_js / "index.css"))
|
|
342
|
+
# Start the server
|
|
343
|
+
logger.info(f"Starting server on port {port}")
|
|
344
|
+
|
|
345
|
+
# Configure SSL if certificates are provided
|
|
346
|
+
if certfile and keyfile:
|
|
347
|
+
logger.info(
|
|
348
|
+
f"Configuring SSL with certfile: {certfile}, keyfile: {keyfile}"
|
|
349
|
+
)
|
|
350
|
+
# Monkey-patch the server to use SSL
|
|
351
|
+
# The livereload Server doesn't expose ssl_context directly,
|
|
352
|
+
# so we need to use the underlying tornado server
|
|
353
|
+
import ssl
|
|
354
|
+
|
|
355
|
+
from tornado import httpserver
|
|
356
|
+
from tornado.ioloop import IOLoop
|
|
357
|
+
from tornado.wsgi import WSGIContainer
|
|
358
|
+
|
|
359
|
+
# Create SSL context
|
|
360
|
+
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
361
|
+
ssl_ctx.load_cert_chain(str(certfile), str(keyfile))
|
|
362
|
+
|
|
363
|
+
# Wrap the WSGI app in Tornado's WSGIContainer
|
|
364
|
+
wsgi_container = WSGIContainer(server.app) # type: ignore[arg-type]
|
|
365
|
+
|
|
366
|
+
# Create HTTP server with SSL
|
|
367
|
+
http_server = httpserver.HTTPServer(wsgi_container, ssl_options=ssl_ctx)
|
|
368
|
+
http_server.listen(port)
|
|
369
|
+
logger.info(f"HTTPS server started on port {port}")
|
|
370
|
+
IOLoop.current().start()
|
|
371
|
+
else:
|
|
372
|
+
server.serve(port=port, debug=True)
|
|
373
|
+
except KeyboardInterrupt:
|
|
374
|
+
logger.info("Server stopped by keyboard interrupt")
|
|
375
|
+
import _thread
|
|
376
|
+
|
|
377
|
+
_thread.interrupt_main()
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.error(f"Failed to run Flask server: {e}", exc_info=True)
|
|
380
|
+
logger.info("Flask server thread running")
|
|
381
|
+
import _thread
|
|
382
|
+
|
|
383
|
+
_thread.interrupt_main()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def run_flask_in_thread(
|
|
387
|
+
port: int,
|
|
388
|
+
cwd: Path,
|
|
389
|
+
compile_server_port: int,
|
|
390
|
+
certfile: Path | None = None,
|
|
391
|
+
keyfile: Path | None = None,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Run the Flask server."""
|
|
394
|
+
try:
|
|
395
|
+
if _ENABLE_LOGGING:
|
|
396
|
+
logger.info(f"Starting Flask server thread on port {port}")
|
|
397
|
+
logger.info(f"Serving files from {cwd}")
|
|
398
|
+
logger.info(f"Compile server port: {compile_server_port}")
|
|
399
|
+
if certfile:
|
|
400
|
+
logger.info(f"Using SSL certificate: {certfile}")
|
|
401
|
+
if keyfile:
|
|
402
|
+
logger.info(f"Using SSL key: {keyfile}")
|
|
403
|
+
|
|
404
|
+
_run_flask_server(cwd, port, compile_server_port, certfile, keyfile)
|
|
405
|
+
except KeyboardInterrupt:
|
|
406
|
+
logger.info("Flask server thread stopped by keyboard interrupt")
|
|
407
|
+
import _thread
|
|
408
|
+
|
|
409
|
+
_thread.interrupt_main()
|
|
410
|
+
pass
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.error(f"Error in Flask server thread: {e}", exc_info=True)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def parse_args() -> argparse.Namespace:
|
|
416
|
+
"""Parse the command line arguments."""
|
|
417
|
+
parser = argparse.ArgumentParser(
|
|
418
|
+
description="Open a browser to the fastled_js directory"
|
|
419
|
+
)
|
|
420
|
+
parser.add_argument(
|
|
421
|
+
"fastled_js", type=Path, help="Path to the fastled_js directory"
|
|
422
|
+
)
|
|
423
|
+
parser.add_argument(
|
|
424
|
+
"--port",
|
|
425
|
+
"-p",
|
|
426
|
+
type=int,
|
|
427
|
+
required=True,
|
|
428
|
+
help="Port to run the server on (default: %(default)s)",
|
|
429
|
+
)
|
|
430
|
+
parser.add_argument(
|
|
431
|
+
"--certfile",
|
|
432
|
+
type=Path,
|
|
433
|
+
help="Path to the SSL certificate file for HTTPS",
|
|
434
|
+
)
|
|
435
|
+
parser.add_argument(
|
|
436
|
+
"--keyfile",
|
|
437
|
+
type=Path,
|
|
438
|
+
help="Path to the SSL key file for HTTPS",
|
|
439
|
+
)
|
|
440
|
+
return parser.parse_args()
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def run_flask_server_process(
|
|
444
|
+
port: int,
|
|
445
|
+
cwd: Path,
|
|
446
|
+
compile_server_port: int,
|
|
447
|
+
certfile: Path | None = None,
|
|
448
|
+
keyfile: Path | None = None,
|
|
449
|
+
) -> Process:
|
|
450
|
+
"""Run the Flask server in a separate process."""
|
|
451
|
+
if _ENABLE_LOGGING:
|
|
452
|
+
logger.info(f"Starting Flask server process on port {port}")
|
|
453
|
+
logger.info(f"Serving files from {cwd}")
|
|
454
|
+
logger.info(f"Compile server port: {compile_server_port}")
|
|
455
|
+
|
|
456
|
+
process = Process(
|
|
457
|
+
target=run_flask_in_thread,
|
|
458
|
+
args=(port, cwd, compile_server_port, certfile, keyfile),
|
|
459
|
+
)
|
|
460
|
+
process.start()
|
|
461
|
+
if _ENABLE_LOGGING:
|
|
462
|
+
logger.info(f"Flask server process started with PID {process.pid}")
|
|
463
|
+
return process
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def main() -> None:
|
|
467
|
+
"""Main function."""
|
|
468
|
+
if _ENABLE_LOGGING:
|
|
469
|
+
logger.info("Starting main function")
|
|
470
|
+
args = parse_args()
|
|
471
|
+
if _ENABLE_LOGGING:
|
|
472
|
+
logger.info(f"Arguments: port={args.port}, fastled_js={args.fastled_js}")
|
|
473
|
+
if args.certfile:
|
|
474
|
+
logger.info(f"Using SSL certificate: {args.certfile}")
|
|
475
|
+
if args.keyfile:
|
|
476
|
+
logger.info(f"Using SSL key: {args.keyfile}")
|
|
477
|
+
logger.warning("Note: main() is missing compile_server_port parameter")
|
|
478
|
+
|
|
479
|
+
# Note: This call is missing the compile_server_port parameter
|
|
480
|
+
# This is a bug in the original code
|
|
481
|
+
run_flask_in_thread(args.port, args.fastled_js, 0, args.certfile, args.keyfile)
|
|
482
|
+
if _ENABLE_LOGGING:
|
|
483
|
+
logger.info("Main function completed")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == "__main__":
|
|
487
|
+
main()
|
fastled/server_start.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from fastled.server_flask import run_flask_in_thread
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def start_server_in_thread(
|
|
7
|
+
port: int,
|
|
8
|
+
fastled_js: Path,
|
|
9
|
+
compile_server_port: int,
|
|
10
|
+
certfile: Path | None = None,
|
|
11
|
+
keyfile: Path | None = None,
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Start the server in a separate thread."""
|
|
14
|
+
|
|
15
|
+
run_flask_in_thread(
|
|
16
|
+
port=port,
|
|
17
|
+
cwd=fastled_js,
|
|
18
|
+
compile_server_port=compile_server_port,
|
|
19
|
+
certfile=certfile,
|
|
20
|
+
keyfile=keyfile,
|
|
21
|
+
)
|
fastled/settings.py
CHANGED
|
@@ -1,13 +1,62 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import platform
|
|
3
|
+
import sys
|
|
3
4
|
|
|
5
|
+
FILE_CHANGED_DEBOUNCE_SECONDS = 2.0
|
|
4
6
|
MACHINE = platform.machine().lower()
|
|
5
7
|
IS_ARM: bool = "arm" in MACHINE or "aarch64" in MACHINE
|
|
6
8
|
PLATFORM_TAG: str = "-arm64" if IS_ARM else ""
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_running_under_github_actions() -> bool:
|
|
12
|
+
"""Detect if we're running under github actions."""
|
|
13
|
+
return "GITHUB_ACTIONS" in os.environ
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_running_under_pytest() -> bool:
|
|
17
|
+
"""Detect if we're running under pytest."""
|
|
18
|
+
# Check if pytest is in the loaded modules
|
|
19
|
+
if "pytest" in sys.modules:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
# Check for pytest environment variables
|
|
23
|
+
if "PYTEST_CURRENT_TEST" in os.environ:
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_container_name() -> str:
|
|
30
|
+
"""Get the appropriate container name based on runtime context."""
|
|
31
|
+
base_name = "fastled-wasm-container"
|
|
32
|
+
|
|
33
|
+
if not _is_running_under_github_actions() and _is_running_under_pytest():
|
|
34
|
+
# Use test container name when running under pytest
|
|
35
|
+
return f"{base_name}-test{PLATFORM_TAG}"
|
|
36
|
+
else:
|
|
37
|
+
# Use regular container name
|
|
38
|
+
return f"{base_name}{PLATFORM_TAG}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_server_port() -> int:
|
|
42
|
+
"""Get the appropriate server port based on runtime context."""
|
|
43
|
+
if not _is_running_under_github_actions() and _is_running_under_pytest():
|
|
44
|
+
# Use test port when running under pytest to avoid conflicts
|
|
45
|
+
return 9022
|
|
46
|
+
else:
|
|
47
|
+
# Use regular port
|
|
48
|
+
return 9021
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
CONTAINER_NAME = _get_container_name()
|
|
8
52
|
DEFAULT_URL = str(os.environ.get("FASTLED_URL", "https://fastled.onrender.com"))
|
|
9
|
-
SERVER_PORT =
|
|
53
|
+
SERVER_PORT = _get_server_port()
|
|
54
|
+
AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
|
|
10
55
|
|
|
11
56
|
IMAGE_NAME = "niteris/fastled-wasm"
|
|
12
|
-
DEFAULT_CONTAINER_NAME =
|
|
13
|
-
# IMAGE_TAG = "
|
|
57
|
+
DEFAULT_CONTAINER_NAME = _get_container_name()
|
|
58
|
+
# IMAGE_TAG = "latest"
|
|
59
|
+
|
|
60
|
+
DOCKER_FILE = (
|
|
61
|
+
"https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/Dockerfile"
|
|
62
|
+
)
|
fastled/site/build.py
CHANGED
|
@@ -3,6 +3,8 @@ import subprocess
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from shutil import copytree, rmtree, which
|
|
5
5
|
|
|
6
|
+
from fastled.site.examples import EXAMPLES
|
|
7
|
+
|
|
6
8
|
CSS_CONTENT = """
|
|
7
9
|
/* CSS Reset & Variables */
|
|
8
10
|
*, *::before, *::after {
|
|
@@ -339,16 +341,6 @@ INDEX_TEMPLATE = """<!DOCTYPE html>
|
|
|
339
341
|
"""
|
|
340
342
|
|
|
341
343
|
|
|
342
|
-
EXAMPLES = [
|
|
343
|
-
"wasm",
|
|
344
|
-
"Chromancer",
|
|
345
|
-
"LuminescentGrand",
|
|
346
|
-
"FxSdCard",
|
|
347
|
-
"FxNoiseRing",
|
|
348
|
-
"FxWater",
|
|
349
|
-
]
|
|
350
|
-
|
|
351
|
-
|
|
352
344
|
def _exec(cmd: str) -> None:
|
|
353
345
|
subprocess.run(cmd, shell=True, check=True)
|
|
354
346
|
|