fastled 1.2.80__tar.gz → 1.2.82__tar.gz

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.
Files changed (135) hide show
  1. {fastled-1.2.80 → fastled-1.2.82}/PKG-INFO +1 -5
  2. {fastled-1.2.80 → fastled-1.2.82}/compiler/compile.py +1 -1
  3. {fastled-1.2.80 → fastled-1.2.82}/compiler/server.py +34 -0
  4. {fastled-1.2.80 → fastled-1.2.82}/docker-compose.yml +2 -1
  5. {fastled-1.2.80 → fastled-1.2.82}/lint +6 -0
  6. {fastled-1.2.80 → fastled-1.2.82}/pyproject.toml +0 -4
  7. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/__init__.py +9 -4
  8. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/client_server.py +78 -13
  9. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/compile_server.py +5 -1
  10. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/compile_server_impl.py +31 -33
  11. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/live_client.py +5 -0
  12. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/open_browser.py +38 -41
  13. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/server_flask.py +29 -3
  14. fastled-1.2.82/src/fastled/server_start.py +21 -0
  15. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/types.py +9 -0
  16. fastled-1.2.82/src/fastled/util.py +45 -0
  17. {fastled-1.2.80 → fastled-1.2.82}/src/fastled.egg-info/PKG-INFO +1 -5
  18. {fastled-1.2.80 → fastled-1.2.82}/src/fastled.egg-info/SOURCES.txt +2 -2
  19. {fastled-1.2.80 → fastled-1.2.82}/src/fastled.egg-info/requires.txt +0 -4
  20. {fastled-1.2.80 → fastled-1.2.82}/test +7 -0
  21. fastled-1.2.82/tests/unit/test_fastled_source_get.py +77 -0
  22. fastled-1.2.82/tests/unit/test_version.py +27 -0
  23. fastled-1.2.80/src/fastled/server_fastapi.py +0 -60
  24. fastled-1.2.80/src/fastled/server_fastapi_cli.py +0 -61
  25. fastled-1.2.80/src/fastled/server_start.py +0 -145
  26. fastled-1.2.80/src/fastled/util.py +0 -19
  27. {fastled-1.2.80 → fastled-1.2.82}/.aiderignore +0 -0
  28. {fastled-1.2.80 → fastled-1.2.82}/.dockerignore +0 -0
  29. {fastled-1.2.80 → fastled-1.2.82}/.github/workflows/build_multi_docker_image.yml +0 -0
  30. {fastled-1.2.80 → fastled-1.2.82}/.github/workflows/build_webpage.yml +0 -0
  31. {fastled-1.2.80 → fastled-1.2.82}/.github/workflows/lint.yml +0 -0
  32. {fastled-1.2.80 → fastled-1.2.82}/.github/workflows/publish_release.yml +0 -0
  33. {fastled-1.2.80 → fastled-1.2.82}/.github/workflows/template_build_docker_image.yml +0 -0
  34. {fastled-1.2.80 → fastled-1.2.82}/.github/workflows/test_build_exe.yml +0 -0
  35. {fastled-1.2.80 → fastled-1.2.82}/.github/workflows/test_macos.yml +0 -0
  36. {fastled-1.2.80 → fastled-1.2.82}/.github/workflows/test_ubuntu.yml +0 -0
  37. {fastled-1.2.80 → fastled-1.2.82}/.github/workflows/test_win.yml +0 -0
  38. {fastled-1.2.80 → fastled-1.2.82}/.gitignore +0 -0
  39. {fastled-1.2.80 → fastled-1.2.82}/.pylintrc +0 -0
  40. {fastled-1.2.80 → fastled-1.2.82}/.vscode/launch.json +0 -0
  41. {fastled-1.2.80 → fastled-1.2.82}/.vscode/settings.json +0 -0
  42. {fastled-1.2.80 → fastled-1.2.82}/.vscode/tasks.json +0 -0
  43. {fastled-1.2.80 → fastled-1.2.82}/Dockerfile +0 -0
  44. {fastled-1.2.80 → fastled-1.2.82}/LICENSE +0 -0
  45. {fastled-1.2.80 → fastled-1.2.82}/MANIFEST.in +0 -0
  46. {fastled-1.2.80 → fastled-1.2.82}/README.md +0 -0
  47. {fastled-1.2.80 → fastled-1.2.82}/RELEASE.md +0 -0
  48. {fastled-1.2.80 → fastled-1.2.82}/TODO.md +0 -0
  49. {fastled-1.2.80 → fastled-1.2.82}/build_exe.py +0 -0
  50. {fastled-1.2.80 → fastled-1.2.82}/build_site.py +0 -0
  51. {fastled-1.2.80 → fastled-1.2.82}/clean +0 -0
  52. {fastled-1.2.80 → fastled-1.2.82}/compiler/CMakeLists.txt +0 -0
  53. {fastled-1.2.80 → fastled-1.2.82}/compiler/__init__.py +0 -0
  54. {fastled-1.2.80 → fastled-1.2.82}/compiler/arduino-pre-process.sh +0 -0
  55. {fastled-1.2.80 → fastled-1.2.82}/compiler/build.sh +0 -0
  56. {fastled-1.2.80 → fastled-1.2.82}/compiler/build_archive.sh +0 -0
  57. {fastled-1.2.80 → fastled-1.2.82}/compiler/build_fast.sh +0 -0
  58. {fastled-1.2.80 → fastled-1.2.82}/compiler/code_sync.py +0 -0
  59. {fastled-1.2.80 → fastled-1.2.82}/compiler/compile_lock.py +0 -0
  60. {fastled-1.2.80 → fastled-1.2.82}/compiler/debug.sh +0 -0
  61. {fastled-1.2.80 → fastled-1.2.82}/compiler/entrypoint.sh +0 -0
  62. {fastled-1.2.80 → fastled-1.2.82}/compiler/extra/100dots.html +0 -0
  63. {fastled-1.2.80 → fastled-1.2.82}/compiler/extra/demo_threejs.html +0 -0
  64. {fastled-1.2.80 → fastled-1.2.82}/compiler/extra/micdemo.html +0 -0
  65. {fastled-1.2.80 → fastled-1.2.82}/compiler/extra/mp3upload.html +0 -0
  66. {fastled-1.2.80 → fastled-1.2.82}/compiler/extra/webgl_postprocessing_unreal_bloom.html +0 -0
  67. {fastled-1.2.80 → fastled-1.2.82}/compiler/final_prewarm.sh +0 -0
  68. {fastled-1.2.80 → fastled-1.2.82}/compiler/init_runtime.py +0 -0
  69. {fastled-1.2.80 → fastled-1.2.82}/compiler/install-arduino-cli.sh +0 -0
  70. {fastled-1.2.80 → fastled-1.2.82}/compiler/libcompile/CMakeLists.txt +0 -0
  71. {fastled-1.2.80 → fastled-1.2.82}/compiler/paths.py +0 -0
  72. {fastled-1.2.80 → fastled-1.2.82}/compiler/pre-process.sh +0 -0
  73. {fastled-1.2.80 → fastled-1.2.82}/compiler/prewarm.sh +0 -0
  74. {fastled-1.2.80 → fastled-1.2.82}/compiler/process-ino.py +0 -0
  75. {fastled-1.2.80 → fastled-1.2.82}/compiler/process_extended.py +0 -0
  76. {fastled-1.2.80 → fastled-1.2.82}/compiler/pyproject.toml +0 -0
  77. {fastled-1.2.80 → fastled-1.2.82}/compiler/run.py +0 -0
  78. {fastled-1.2.80 → fastled-1.2.82}/compiler/sketch_hasher.py +0 -0
  79. {fastled-1.2.80 → fastled-1.2.82}/compiler/wasm_compiler_flags.py +0 -0
  80. {fastled-1.2.80 → fastled-1.2.82}/install +0 -0
  81. {fastled-1.2.80 → fastled-1.2.82}/install_linux.sh +0 -0
  82. {fastled-1.2.80 → fastled-1.2.82}/requirements.testing.txt +0 -0
  83. {fastled-1.2.80 → fastled-1.2.82}/setup.cfg +0 -0
  84. {fastled-1.2.80 → fastled-1.2.82}/setup.py +0 -0
  85. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/app.py +0 -0
  86. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/assets/example.txt +0 -0
  87. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/assets/localhost-key.pem +0 -0
  88. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/assets/localhost.pem +0 -0
  89. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/cli.py +0 -0
  90. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/cli_test.py +0 -0
  91. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/cli_test_interactive.py +0 -0
  92. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/docker_manager.py +0 -0
  93. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/filewatcher.py +0 -0
  94. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/keyboard.py +0 -0
  95. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/keyz.py +0 -0
  96. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/parse_args.py +0 -0
  97. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/paths.py +0 -0
  98. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/print_filter.py +0 -0
  99. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/project_init.py +0 -0
  100. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/select_sketch_directory.py +0 -0
  101. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/settings.py +0 -0
  102. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/site/build.py +0 -0
  103. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/site/examples.py +0 -0
  104. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/sketch.py +0 -0
  105. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/spinner.py +0 -0
  106. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/string_diff.py +0 -0
  107. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/test/can_run_local_docker_tests.py +0 -0
  108. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/test/examples.py +0 -0
  109. {fastled-1.2.80 → fastled-1.2.82}/src/fastled/web_compile.py +0 -0
  110. {fastled-1.2.80 → fastled-1.2.82}/src/fastled.egg-info/dependency_links.txt +0 -0
  111. {fastled-1.2.80 → fastled-1.2.82}/src/fastled.egg-info/entry_points.txt +0 -0
  112. {fastled-1.2.80 → fastled-1.2.82}/src/fastled.egg-info/top_level.txt +0 -0
  113. {fastled-1.2.80 → fastled-1.2.82}/tests/integration/test_build_examples.py +0 -0
  114. {fastled-1.2.80 → fastled-1.2.82}/tests/integration/test_examples.py +0 -0
  115. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/html/index.html +0 -0
  116. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_api.py +0 -0
  117. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_bad_ino.py +0 -0
  118. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_cli.py +0 -0
  119. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_compile_server.py +0 -0
  120. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_docker_linux_on_windows.py +0 -0
  121. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_embedded_data.py +0 -0
  122. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_filechanger.py +0 -0
  123. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_http_server.py +0 -0
  124. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_ino/bad/bad.ino +0 -0
  125. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
  126. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
  127. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
  128. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_ino/embedded/wasm.ino +0 -0
  129. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_ino/wasm/wasm.ino +0 -0
  130. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_print_filter.py +0 -0
  131. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_project_init.py +0 -0
  132. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_server_and_client_seperatly.py +0 -0
  133. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_string_diff.py +0 -0
  134. {fastled-1.2.80 → fastled-1.2.82}/tests/unit/test_webcompile.py +0 -0
  135. {fastled-1.2.80 → fastled-1.2.82}/upload_package.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.2.80
3
+ Version: 1.2.82
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -13,15 +13,11 @@ License-File: LICENSE
13
13
  Requires-Dist: docker>=7.1.0
14
14
  Requires-Dist: httpx>=0.28.1
15
15
  Requires-Dist: watchdog>=6.0.0
16
- Requires-Dist: download>=0.3.5
17
16
  Requires-Dist: filelock>=3.16.1
18
17
  Requires-Dist: disklru>=2.0.1
19
18
  Requires-Dist: appdirs>=1.4.4
20
19
  Requires-Dist: rapidfuzz>=3.10.1
21
20
  Requires-Dist: progress>=1.6
22
- Requires-Dist: fastapi>=0.115.12
23
- Requires-Dist: uvicorn>=0.34.2
24
- Requires-Dist: pywebview>=5.4
25
21
  Requires-Dist: watchfiles>=1.0.5
26
22
  Requires-Dist: Flask>=3.0.0
27
23
  Requires-Dist: livereload
@@ -86,7 +86,7 @@ def copy_files(src_dir: Path, js_src: Path) -> None:
86
86
  print(f"Copying file: {item}")
87
87
  shutil.copy2(item, js_src / item.name)
88
88
  else:
89
- warnings.warn("No files found in the mapped directory.")
89
+ warnings.warn(f"No files found in the mapped directory: {src_dir.absolute()}")
90
90
 
91
91
 
92
92
  def _banner(msg: str) -> str:
@@ -598,6 +598,40 @@ def project_init_example(
598
598
  )
599
599
 
600
600
 
601
+ @app.get("/sourcefiles/{filepath:path}")
602
+ def source_file(filepath: str) -> FileResponse:
603
+ """Get the source file from the server."""
604
+ print(f"Endpoint accessed: /sourcefiles/{filepath}")
605
+ if ".." in filepath:
606
+ raise HTTPException(status_code=400, detail="Invalid file path.")
607
+ full_path = Path(FASTLED_SRC / filepath)
608
+ if not full_path.exists():
609
+ raise HTTPException(status_code=404, detail="File not found.")
610
+ if not full_path.is_file():
611
+ raise HTTPException(status_code=400, detail="Not a file.")
612
+ if not full_path.is_relative_to(FASTLED_SRC):
613
+ raise HTTPException(status_code=400, detail="Invalid file path.")
614
+
615
+ suffix = full_path.suffix
616
+ if suffix in [".h", ".cpp"]:
617
+ media_type = "text/plain"
618
+ elif suffix == ".html":
619
+ media_type = "text/html"
620
+ elif suffix == ".js":
621
+ media_type = "application/javascript"
622
+ elif suffix == ".css":
623
+ media_type = "text/css"
624
+ else:
625
+ media_type = "application/octet-stream"
626
+
627
+ fr = FileResponse(
628
+ path=full_path,
629
+ media_type=media_type,
630
+ filename=filepath,
631
+ )
632
+ return fr
633
+
634
+
601
635
  @app.get("/static/{file_path:path}")
602
636
  async def static_files(file_path: str) -> Response:
603
637
  """Serve static files."""
@@ -4,6 +4,7 @@ services:
4
4
  build:
5
5
  context: .
6
6
  ports:
7
- - "80:80"
7
+ - "8234:80"
8
8
  environment:
9
9
  - ENVIRONMENT=dev
10
+ image: niteris/fastled-wasm:latest
@@ -1,6 +1,12 @@
1
1
  #!/bin/bash
2
2
  set -e
3
3
 
4
+ # if venv doesn't exist, invoke ./install
5
+ if [ ! -d ".venv" ]; then
6
+ echo "No venv present, so installing..."
7
+ ./install
8
+ fi
9
+
4
10
  . ./activate
5
11
 
6
12
  echo Running ruff src compiler
@@ -14,7 +14,6 @@ dependencies = [
14
14
  "docker>=7.1.0",
15
15
  "httpx>=0.28.1",
16
16
  "watchdog>=6.0.0",
17
- "download>=0.3.5",
18
17
  ########## Begin Docker Manager Dependencies
19
18
  "filelock>=3.16.1",
20
19
  "disklru>=2.0.1",
@@ -22,9 +21,6 @@ dependencies = [
22
21
  "rapidfuzz>=3.10.1",
23
22
  "progress>=1.6",
24
23
  ########## End Docker Manager Dependencies
25
- "fastapi>=0.115.12",
26
- "uvicorn>=0.34.2",
27
- "pywebview>=5.4",
28
24
  "watchfiles>=1.0.5",
29
25
  "Flask>=3.0.0",
30
26
  "livereload",
@@ -9,12 +9,12 @@ from .compile_server import CompileServer
9
9
  from .live_client import LiveClient
10
10
  from .settings import DOCKER_FILE, IMAGE_NAME
11
11
  from .site.build import build
12
- from .types import BuildMode, CompileResult, CompileServerError
12
+ from .types import BuildMode, CompileResult, CompileServerError, FileResponse
13
13
 
14
14
  # IMPORTANT! There's a bug in github which will REJECT any version update
15
15
  # that has any other change in the repo. Please bump the version as the
16
16
  # ONLY change in a commit, or else the pypi update and the release will fail.
17
- __version__ = "1.2.80"
17
+ __version__ = "1.2.82"
18
18
 
19
19
 
20
20
  class Api:
@@ -65,6 +65,9 @@ class Api:
65
65
  keep_running=True,
66
66
  build_mode=BuildMode.QUICK,
67
67
  profile=False,
68
+ http_port: (
69
+ int | None
70
+ ) = None, # None means auto select a free port. -1 means no server.
68
71
  ) -> LiveClient:
69
72
  return LiveClient(
70
73
  sketch_directory=sketch_directory,
@@ -75,6 +78,7 @@ class Api:
75
78
  keep_running=keep_running,
76
79
  build_mode=build_mode,
77
80
  profile=profile,
81
+ http_port=http_port,
78
82
  )
79
83
 
80
84
  @staticmethod
@@ -197,12 +201,12 @@ class Test:
197
201
  compile_server_port: int | None = None,
198
202
  open_browser: bool = True,
199
203
  ) -> Process:
200
- from fastled.open_browser import open_browser_process
204
+ from fastled.open_browser import spawn_http_server
201
205
 
202
206
  compile_server_port = compile_server_port or -1
203
207
  if isinstance(directory, str):
204
208
  directory = Path(directory)
205
- proc: Process = open_browser_process(
209
+ proc: Process = spawn_http_server(
206
210
  directory,
207
211
  port=port,
208
212
  compile_server_port=compile_server_port,
@@ -218,5 +222,6 @@ __all__ = [
218
222
  "CompileResult",
219
223
  "CompileServerError",
220
224
  "BuildMode",
225
+ "FileResponse",
221
226
  "DOCKER_FILE",
222
227
  ]
@@ -2,6 +2,7 @@ import shutil
2
2
  import tempfile
3
3
  import threading
4
4
  import time
5
+ import warnings
5
6
  from multiprocessing import Process
6
7
  from pathlib import Path
7
8
 
@@ -9,7 +10,7 @@ from fastled.compile_server import CompileServer
9
10
  from fastled.docker_manager import DockerManager
10
11
  from fastled.filewatcher import DebouncedFileWatcherProcess, FileWatcherProcess
11
12
  from fastled.keyboard import SpaceBarWatcher
12
- from fastled.open_browser import open_browser_process
13
+ from fastled.open_browser import spawn_http_server
13
14
  from fastled.parse_args import Args
14
15
  from fastled.settings import DEFAULT_URL
15
16
  from fastled.sketch import looks_like_sketch_directory
@@ -165,6 +166,45 @@ def _try_start_server_or_get_url(
165
166
  return (DEFAULT_URL, None)
166
167
 
167
168
 
169
+ def _try_make_compile_server() -> CompileServer | None:
170
+ if not DockerManager.is_docker_installed():
171
+ return None
172
+ try:
173
+ print(
174
+ "\nNo host specified, but Docker is installed, attempting to start a compile server using Docker."
175
+ )
176
+ from fastled.util import find_free_port
177
+
178
+ free_port = find_free_port(start_port=9723, end_port=9743)
179
+ if free_port is None:
180
+ return None
181
+ compile_server = CompileServer(auto_updates=False)
182
+ print("Waiting for the local compiler to start...")
183
+ if not compile_server.ping():
184
+ print("Failed to start local compiler.")
185
+ raise CompileServerError("Failed to start local compiler.")
186
+ return compile_server
187
+ except KeyboardInterrupt:
188
+ import _thread
189
+
190
+ _thread.interrupt_main()
191
+ raise
192
+ except Exception as e:
193
+ warnings.warn(f"Error starting local compile server: {e}")
194
+ return None
195
+
196
+
197
+ def _is_local_host(host: str) -> bool:
198
+ return (
199
+ host.startswith("http://localhost")
200
+ or host.startswith("http://127.0.0.1")
201
+ or host.startswith("http://0.0.0.0")
202
+ or host.startswith("http://[::]")
203
+ or host.startswith("http://[::1]")
204
+ or host.startswith("http://[::ffff:127.0.0.1]")
205
+ )
206
+
207
+
168
208
  def run_client(
169
209
  directory: Path,
170
210
  host: str | CompileServer | None,
@@ -173,14 +213,24 @@ def run_client(
173
213
  build_mode: BuildMode = BuildMode.QUICK,
174
214
  profile: bool = False,
175
215
  shutdown: threading.Event | None = None,
216
+ http_port: (
217
+ int | None
218
+ ) = None, # None means auto select a free port, http_port < 0 means no server.
176
219
  ) -> int:
220
+ compile_server: CompileServer | None = None
221
+
222
+ if host is None:
223
+ # attempt to start a compile server if docker is installed.
224
+ compile_server = _try_make_compile_server()
225
+ if compile_server is None:
226
+ host = DEFAULT_URL
227
+ elif isinstance(host, CompileServer):
228
+ # if the host is a compile server, use that
229
+ compile_server = host
177
230
 
178
- compile_server: CompileServer | None = (
179
- host if isinstance(host, CompileServer) else None
180
- )
181
231
  shutdown = shutdown or threading.Event()
182
232
 
183
- def get_url() -> str:
233
+ def get_url(host=host, compile_server=compile_server) -> str:
184
234
  if compile_server is not None:
185
235
  return compile_server.url()
186
236
  if isinstance(host, str):
@@ -196,6 +246,11 @@ def run_client(
196
246
  if parsed_url.port is not None:
197
247
  port = parsed_url.port
198
248
  else:
249
+ if _is_local_host(url):
250
+ raise ValueError(
251
+ "Cannot use local host without a port. Please specify a port."
252
+ )
253
+ # Assume default port for www
199
254
  port = 80
200
255
 
201
256
  try:
@@ -221,10 +276,20 @@ def run_client(
221
276
  if not result.success:
222
277
  print("\nCompilation failed.")
223
278
 
224
- browser_proc: Process | None = None
225
- if open_web_browser:
226
- browser_proc = open_browser_process(
227
- directory / "fastled_js", compile_server_port=port
279
+ use_http_server = http_port is None or http_port >= 0
280
+ if not use_http_server and open_web_browser:
281
+ warnings.warn(
282
+ f"Warning: --http-port={http_port} specified but open_web_browser is False, ignoring --http-port."
283
+ )
284
+ use_http_server = False
285
+
286
+ http_proc: Process | None = None
287
+ if use_http_server:
288
+ http_proc = spawn_http_server(
289
+ directory / "fastled_js",
290
+ port=http_port,
291
+ compile_server_port=port,
292
+ open_browser=open_web_browser,
228
293
  )
229
294
  else:
230
295
  print("\nCompilation successful.")
@@ -234,8 +299,8 @@ def run_client(
234
299
  return 0
235
300
 
236
301
  if not keep_running or shutdown.is_set():
237
- if browser_proc:
238
- browser_proc.kill()
302
+ if http_proc:
303
+ http_proc.kill()
239
304
  return 0 if result.success else 1
240
305
  except KeyboardInterrupt:
241
306
  print("\nExiting from main")
@@ -346,8 +411,8 @@ def run_client(
346
411
  debounced_sketch_watcher.stop()
347
412
  if compile_server:
348
413
  compile_server.stop()
349
- if browser_proc:
350
- browser_proc.kill()
414
+ if http_proc:
415
+ http_proc.kill()
351
416
 
352
417
 
353
418
  def run_client_server(args: Args) -> int:
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from fastled.types import BuildMode, CompileResult, Platform
3
+ from fastled.types import BuildMode, CompileResult, FileResponse, Platform
4
4
 
5
5
 
6
6
  class CompileServer:
@@ -53,6 +53,10 @@ class CompileServer:
53
53
 
54
54
  project_init(example=example, outputdir=outputdir)
55
55
 
56
+ def fetch_source_file(self, filepath: str) -> FileResponse | Exception:
57
+ """Get the source file from the server."""
58
+ return self.impl.fetch_source_file(filepath)
59
+
56
60
  @property
57
61
  def name(self) -> str:
58
62
  return self.impl.container_name
@@ -17,7 +17,8 @@ from fastled.docker_manager import (
17
17
  )
18
18
  from fastled.settings import DEFAULT_CONTAINER_NAME, IMAGE_NAME, SERVER_PORT
19
19
  from fastled.sketch import looks_like_fastled_repo
20
- from fastled.types import BuildMode, CompileResult, CompileServerError
20
+ from fastled.types import BuildMode, CompileResult, CompileServerError, FileResponse
21
+ from fastled.util import port_is_free
21
22
 
22
23
  SERVER_OPTIONS = [
23
24
  "--allow-shutdown", # Allow the server to be shut down without a force kill.
@@ -36,32 +37,6 @@ def _try_get_fastled_src(path: Path) -> Path | None:
36
37
  return None
37
38
 
38
39
 
39
- def _port_is_free(port: int) -> bool:
40
- """Check if a port is free."""
41
- import socket
42
-
43
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
44
- try:
45
- _ = sock.bind(("localhost", port)) and sock.bind(("0.0.0.0", port))
46
- return True
47
- except OSError:
48
- return False
49
-
50
-
51
- def _find_free_port() -> int:
52
- """Find a free port on the system."""
53
-
54
- start_port = 49152
55
- tries = 10
56
-
57
- for port in range(start_port, start_port + tries):
58
- if _port_is_free(port):
59
- return port
60
- raise RuntimeError(
61
- f"No free port found in the range {start_port}-{start_port + tries - 1}"
62
- )
63
-
64
-
65
40
  class CompileServerImpl:
66
41
  def __init__(
67
42
  self,
@@ -204,6 +179,29 @@ class CompileServerImpl:
204
179
  return False
205
180
  return False
206
181
 
182
+ def fetch_source_file(self, filepath: str) -> FileResponse | Exception:
183
+ """Get the source file from the server."""
184
+ if not self._port:
185
+ raise RuntimeError("Server has not been started yet")
186
+ try:
187
+ httpx_client = httpx.Client()
188
+ url = f"http://localhost:{self._port}/sourcefiles/{filepath}"
189
+ response = httpx_client.get(url, follow_redirects=True)
190
+ if response.status_code == 200:
191
+ content = response.text
192
+ mimetype: str = response.headers.get("Content-Type", "text/plain")
193
+ return FileResponse(
194
+ content=content,
195
+ mimetype=mimetype,
196
+ filename=filepath,
197
+ )
198
+ else:
199
+ return CompileServerError(
200
+ f"Error fetching file {filepath}: {response.status_code}"
201
+ )
202
+ except httpx.RequestError as e:
203
+ return CompileServerError(f"Error fetching file {filepath}: {e}")
204
+
207
205
  def _start(self) -> int:
208
206
  print("Compiling server starting")
209
207
 
@@ -227,7 +225,7 @@ class CompileServerImpl:
227
225
  image_name=IMAGE_NAME, tag="latest", upgrade=upgrade
228
226
  )
229
227
  DISK_CACHE.put("last-update", now_str)
230
- CLIENT_PORT = 80 # TODO: Don't use port 80.
228
+ INTERNAL_DOCKER_PORT = 80
231
229
 
232
230
  print("Docker image now validated")
233
231
  port = SERVER_PORT
@@ -239,7 +237,7 @@ class CompileServerImpl:
239
237
  print("Disabling port forwarding in interactive mode")
240
238
  ports = {}
241
239
  else:
242
- ports = {CLIENT_PORT: port}
240
+ ports = {INTERNAL_DOCKER_PORT: port}
243
241
  volumes = []
244
242
  if self.fastled_src_dir:
245
243
  print(
@@ -318,11 +316,11 @@ class CompileServerImpl:
318
316
  print("Compile server starting")
319
317
  return port
320
318
  else:
321
- client_port_mapped = CLIENT_PORT in ports
322
- port_is_free = _port_is_free(CLIENT_PORT)
323
- if client_port_mapped and port_is_free:
319
+ client_port_mapped = INTERNAL_DOCKER_PORT in ports
320
+ free_port = port_is_free(INTERNAL_DOCKER_PORT)
321
+ if client_port_mapped and free_port:
324
322
  warnings.warn(
325
- f"Can't expose port {CLIENT_PORT}, disabling port forwarding in interactive mode"
323
+ f"Can't expose port {INTERNAL_DOCKER_PORT}, disabling port forwarding in interactive mode"
326
324
  )
327
325
  ports = {}
328
326
  self.docker.run_container_interactive(
@@ -16,6 +16,9 @@ class LiveClient:
16
16
  auto_start: bool = True,
17
17
  auto_updates: bool = True,
18
18
  open_web_browser: bool = True,
19
+ http_port: (
20
+ int | None
21
+ ) = None, # None means auto select a free port. -1 means no server.
19
22
  keep_running: bool = True,
20
23
  build_mode: BuildMode = BuildMode.QUICK,
21
24
  profile: bool = False,
@@ -23,6 +26,7 @@ class LiveClient:
23
26
  self.sketch_directory = sketch_directory
24
27
  self.host = host
25
28
  self.open_web_browser = open_web_browser
29
+ self.http_port = http_port
26
30
  self.keep_running = keep_running
27
31
  self.build_mode = build_mode
28
32
  self.profile = profile
@@ -47,6 +51,7 @@ class LiveClient:
47
51
  build_mode=self.build_mode,
48
52
  profile=self.profile,
49
53
  shutdown=self.shutdown,
54
+ http_port=self.http_port,
50
55
  )
51
56
  return rtn
52
57
 
@@ -1,51 +1,36 @@
1
- import subprocess
1
+ import atexit
2
+ import random
2
3
  import sys
3
4
  import time
5
+ import weakref
4
6
  from multiprocessing import Process
5
7
  from pathlib import Path
6
8
 
7
- from fastled.keyz import get_ssl_config
9
+ from fastled.server_flask import run_flask_in_thread
8
10
 
9
11
  DEFAULT_PORT = 8089 # different than live version.
10
12
  PYTHON_EXE = sys.executable
11
13
 
14
+ # Use a weak reference set to track processes without preventing garbage collection
15
+ _WEAK_CLEANUP_SET = weakref.WeakSet()
12
16
 
13
- # print(f"SSL Config: {SSL_CONFIG.certfile}, {SSL_CONFIG.keyfile}")
14
17
 
18
+ def add_cleanup(proc: Process) -> None:
19
+ """Add a process to the cleanup list using weak references"""
20
+ _WEAK_CLEANUP_SET.add(proc)
15
21
 
16
- def _open_http_server_subprocess(
17
- fastled_js: Path, port: int, compile_server_port: int
18
- ) -> None:
19
- print("\n################################################################")
20
- print(f"# Opening browser to {fastled_js} on port {port}")
21
- print("################################################################\n")
22
- ssl = get_ssl_config()
23
- try:
24
- # Fallback to our Python server
25
- cmd = [
26
- PYTHON_EXE,
27
- "-m",
28
- "fastled.server_start",
29
- str(fastled_js),
30
- "--port",
31
- str(port),
32
- "--compile-server-port",
33
- str(compile_server_port),
34
- ]
35
- # Pass SSL flags if available
36
- if ssl:
37
- raise NotImplementedError("SSL is not implemented yet")
38
- print(f"Running server on port {port}.")
39
- print(f"Command: {subprocess.list2cmdline(cmd)}")
40
- # Suppress output
41
- subprocess.run(
42
- cmd,
43
- stdout=subprocess.DEVNULL,
44
- # stderr=subprocess.DEVNULL,
45
- ) # type ignore
46
- except KeyboardInterrupt:
47
- print("Exiting from server...")
48
- sys.exit(0)
22
+ # Register a cleanup function that checks if the process is still alive
23
+ def cleanup_if_alive():
24
+ if proc.is_alive():
25
+ try:
26
+ proc.terminate()
27
+ proc.join(timeout=1.0)
28
+ if proc.is_alive():
29
+ proc.kill()
30
+ except Exception:
31
+ pass
32
+
33
+ atexit.register(cleanup_if_alive)
49
34
 
50
35
 
51
36
  def is_port_free(port: int) -> bool:
@@ -88,7 +73,7 @@ def wait_for_server(port: int, timeout: int = 10) -> None:
88
73
  raise TimeoutError("Could not connect to server")
89
74
 
90
75
 
91
- def open_browser_process(
76
+ def spawn_http_server(
92
77
  fastled_js: Path,
93
78
  compile_server_port: int,
94
79
  port: int | None = None,
@@ -98,14 +83,26 @@ def open_browser_process(
98
83
  if port is not None and not is_port_free(port):
99
84
  raise ValueError(f"Port {port} was specified but in use")
100
85
  if port is None:
101
- port = find_free_port(DEFAULT_PORT)
86
+ offset = random.randint(0, 100)
87
+ port = find_free_port(DEFAULT_PORT + offset)
88
+
89
+ # port: int,
90
+ # cwd: Path,
91
+ # compile_server_port: int,
92
+ # certfile: Path | None = None,
93
+ # keyfile: Path | None = None,
102
94
 
103
95
  proc = Process(
104
- target=_open_http_server_subprocess,
105
- args=(fastled_js, port, compile_server_port),
96
+ target=run_flask_in_thread,
97
+ args=(port, fastled_js, compile_server_port),
106
98
  daemon=True,
107
99
  )
100
+ add_cleanup(proc)
108
101
  proc.start()
102
+
103
+ # Add to cleanup set with weak reference
104
+ add_cleanup(proc)
105
+
109
106
  wait_for_server(port)
110
107
  if open_browser:
111
108
  print(f"Opening browser to http://localhost:{port}")
@@ -136,5 +133,5 @@ if __name__ == "__main__":
136
133
  )
137
134
  args = parser.parse_args()
138
135
 
139
- proc = open_browser_process(args.fastled_js, args.port, open_browser=True)
136
+ proc = spawn_http_server(args.fastled_js, args.port, open_browser=True)
140
137
  proc.join()
@@ -61,6 +61,32 @@ def _run_flask_server(
61
61
 
62
62
  return response
63
63
 
64
+ @app.route("/sourcefiles/<path:path>")
65
+ def serve_source_files(path):
66
+ """Proxy requests to /sourcefiles/* to the compile server"""
67
+ from flask import Response, request
68
+
69
+ # Forward the request to the compile server
70
+ target_url = f"http://localhost:{compile_server_port}/sourcefiles/{path}"
71
+
72
+ # Forward the request with the same method, headers, and body
73
+ resp = requests.request(
74
+ method=request.method,
75
+ url=target_url,
76
+ headers={key: value for key, value in request.headers if key != "Host"},
77
+ data=request.get_data(),
78
+ cookies=request.cookies,
79
+ allow_redirects=True,
80
+ stream=False,
81
+ )
82
+
83
+ # Create a Flask Response object from the requests response
84
+ response = Response(
85
+ resp.raw.read(), status=resp.status_code, headers=dict(resp.headers)
86
+ )
87
+
88
+ return response
89
+
64
90
  @app.route("/<path:path>")
65
91
  def serve_files(path):
66
92
  response = send_from_directory(fastled_js, path)
@@ -112,7 +138,7 @@ def _run_flask_server(
112
138
  _thread.interrupt_main()
113
139
 
114
140
 
115
- def run(
141
+ def run_flask_in_thread(
116
142
  port: int,
117
143
  cwd: Path,
118
144
  compile_server_port: int,
@@ -169,7 +195,7 @@ def run_flask_server_process(
169
195
  ) -> Process:
170
196
  """Run the Flask server in a separate process."""
171
197
  process = Process(
172
- target=run,
198
+ target=run_flask_in_thread,
173
199
  args=(port, cwd, compile_server_port, certfile, keyfile),
174
200
  )
175
201
  process.start()
@@ -179,7 +205,7 @@ def run_flask_server_process(
179
205
  def main() -> None:
180
206
  """Main function."""
181
207
  args = parse_args()
182
- run(args.port, args.fastled_js, args.certfile, args.keyfile)
208
+ run_flask_in_thread(args.port, args.fastled_js, args.certfile, args.keyfile)
183
209
 
184
210
 
185
211
  if __name__ == "__main__":
@@ -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
+ )
@@ -151,3 +151,12 @@ class Platform(Enum):
151
151
  raise ValueError(
152
152
  f"Platform must be one of {valid_modes}, got {platform_str}"
153
153
  )
154
+
155
+
156
+ @dataclass
157
+ class FileResponse:
158
+ """File response from the server."""
159
+
160
+ filename: str
161
+ content: str
162
+ mimetype: str