comfy-env 0.1.21__tar.gz → 0.1.23__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.
- {comfy_env-0.1.21 → comfy_env-0.1.23}/PKG-INFO +2 -1
- {comfy_env-0.1.21 → comfy_env-0.1.23}/pyproject.toml +2 -1
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/cli.py +15 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/install.py +7 -2
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/isolation/workers/subprocess.py +213 -17
- comfy_env-0.1.23/src/comfy_env/packages/apt.py +68 -0
- comfy_env-0.1.21/src/comfy_env/packages/apt.py +0 -36
- {comfy_env-0.1.21 → comfy_env-0.1.23}/.github/workflows/ci.yml +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/.github/workflows/publish.yml +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/.gitignore +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/LICENSE +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/README.md +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/__init__.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/config/__init__.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/config/parser.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/config/types.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/detection/__init__.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/detection/cuda.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/detection/gpu.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/detection/platform.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/detection/runtime.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/environment/__init__.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/environment/cache.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/environment/libomp.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/environment/paths.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/environment/setup.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/isolation/__init__.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/isolation/tensor_utils.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/isolation/workers/__init__.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/isolation/workers/base.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/isolation/wrap.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/packages/__init__.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/packages/cuda_wheels.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/packages/node_dependencies.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/packages/pixi.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/packages/toml_generator.py +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/templates/comfy-env-instructions.txt +0 -0
- {comfy_env-0.1.21 → comfy_env-0.1.23}/src/comfy_env/templates/comfy-env.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comfy-env
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.23
|
|
4
4
|
Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
|
|
5
5
|
Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
|
|
6
6
|
Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: numpy
|
|
20
21
|
Requires-Dist: pip>=21.0
|
|
21
22
|
Requires-Dist: tomli-w>=1.0.0
|
|
22
23
|
Requires-Dist: tomli>=2.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "comfy-env"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.23"
|
|
4
4
|
description = "Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -23,6 +23,7 @@ dependencies = [
|
|
|
23
23
|
"tomli-w>=1.0.0",
|
|
24
24
|
"uv>=0.4.0",
|
|
25
25
|
"pip>=21.0",
|
|
26
|
+
"numpy",
|
|
26
27
|
]
|
|
27
28
|
|
|
28
29
|
[project.optional-dependencies]
|
|
@@ -44,6 +44,9 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
44
44
|
p.add_argument("--config", "-c", type=str, help="Config path")
|
|
45
45
|
p.add_argument("--dry-run", action="store_true", help="Preview only")
|
|
46
46
|
|
|
47
|
+
# cleanup
|
|
48
|
+
sub.add_parser("cleanup", help="Remove orphaned environments")
|
|
49
|
+
|
|
47
50
|
parsed = parser.parse_args(args)
|
|
48
51
|
if not parsed.command:
|
|
49
52
|
parser.print_help()
|
|
@@ -52,6 +55,7 @@ def main(args: Optional[List[str]] = None) -> int:
|
|
|
52
55
|
commands = {
|
|
53
56
|
"init": cmd_init, "generate": cmd_generate, "install": cmd_install,
|
|
54
57
|
"info": cmd_info, "doctor": cmd_doctor, "apt-install": cmd_apt_install,
|
|
58
|
+
"cleanup": cmd_cleanup,
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
try:
|
|
@@ -237,5 +241,16 @@ def cmd_apt_install(args) -> int:
|
|
|
237
241
|
return result.returncode
|
|
238
242
|
|
|
239
243
|
|
|
244
|
+
def cmd_cleanup(args) -> int:
|
|
245
|
+
from .environment.cache import cleanup_orphaned_envs
|
|
246
|
+
print("Cleaning orphaned environments...")
|
|
247
|
+
cleaned = cleanup_orphaned_envs()
|
|
248
|
+
if cleaned:
|
|
249
|
+
print(f"Removed {cleaned} orphaned environment(s)")
|
|
250
|
+
else:
|
|
251
|
+
print("No orphaned environments found")
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
|
|
240
255
|
if __name__ == "__main__":
|
|
241
256
|
sys.exit(main())
|
|
@@ -59,7 +59,9 @@ def _install_apt_packages(packages: List[str], log: Callable[[str], None], dry_r
|
|
|
59
59
|
return
|
|
60
60
|
log(f"\n[apt] Installing: {', '.join(packages)}")
|
|
61
61
|
if not dry_run:
|
|
62
|
-
apt_install(packages, log)
|
|
62
|
+
success = apt_install(packages, log)
|
|
63
|
+
if not success:
|
|
64
|
+
log("[apt] WARNING: Some apt packages failed to install. This may cause issues.")
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
def _set_persistent_env_vars(env_vars: dict, log: Callable[[str], None], dry_run: bool) -> None:
|
|
@@ -116,9 +118,12 @@ def _install_via_pixi(cfg: ComfyEnvConfig, node_dir: Path, log: Callable[[str],
|
|
|
116
118
|
from .packages.toml_generator import write_pixi_toml
|
|
117
119
|
from .packages.cuda_wheels import get_wheel_url, CUDA_TORCH_MAP
|
|
118
120
|
from .detection import get_recommended_cuda_version
|
|
119
|
-
from .environment.cache import get_central_env_path, write_marker, write_env_metadata, MARKER_FILE, get_cache_dir
|
|
121
|
+
from .environment.cache import get_central_env_path, write_marker, write_env_metadata, MARKER_FILE, get_cache_dir, cleanup_orphaned_envs
|
|
120
122
|
import shutil, subprocess, sys
|
|
121
123
|
|
|
124
|
+
# Clean up orphaned environments before installing
|
|
125
|
+
cleanup_orphaned_envs(log)
|
|
126
|
+
|
|
122
127
|
deps = cfg.pixi_passthrough.get("dependencies", {})
|
|
123
128
|
pypi_deps = cfg.pixi_passthrough.get("pypi-dependencies", {})
|
|
124
129
|
if not cfg.cuda_packages and not deps and not pypi_deps:
|
|
@@ -297,6 +297,45 @@ def _to_shm(obj, registry, visited=None):
|
|
|
297
297
|
return obj
|
|
298
298
|
|
|
299
299
|
|
|
300
|
+
def _deserialize_tensor_ref(data):
|
|
301
|
+
"""Deserialize tensor from PyTorch shared memory (TensorRef format)."""
|
|
302
|
+
import torch
|
|
303
|
+
import torch.multiprocessing.reductions as reductions
|
|
304
|
+
|
|
305
|
+
dtype_str = data["dtype"]
|
|
306
|
+
dtype = getattr(torch, dtype_str.split(".")[-1])
|
|
307
|
+
|
|
308
|
+
manager_path = data["manager_path"]
|
|
309
|
+
storage_key = data["storage_key"]
|
|
310
|
+
storage_size = data["storage_size"]
|
|
311
|
+
|
|
312
|
+
# Encode to bytes if needed
|
|
313
|
+
if isinstance(manager_path, str):
|
|
314
|
+
manager_path = manager_path.encode("utf-8")
|
|
315
|
+
if isinstance(storage_key, str):
|
|
316
|
+
storage_key = storage_key.encode("utf-8")
|
|
317
|
+
|
|
318
|
+
# Rebuild storage from shared memory file
|
|
319
|
+
rebuilt_storage = reductions.rebuild_storage_filename(
|
|
320
|
+
torch.UntypedStorage, manager_path, storage_key, storage_size
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Wrap in TypedStorage
|
|
324
|
+
typed_storage = torch.storage.TypedStorage(
|
|
325
|
+
wrap_storage=rebuilt_storage, dtype=dtype, _internal=True
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Rebuild tensor
|
|
329
|
+
metadata = (
|
|
330
|
+
data["tensor_offset"],
|
|
331
|
+
tuple(data["tensor_size"]),
|
|
332
|
+
tuple(data["tensor_stride"]),
|
|
333
|
+
data["requires_grad"],
|
|
334
|
+
)
|
|
335
|
+
tensor = reductions.rebuild_tensor(torch.Tensor, typed_storage, metadata)
|
|
336
|
+
return tensor
|
|
337
|
+
|
|
338
|
+
|
|
300
339
|
def _from_shm(obj, unlink=True):
|
|
301
340
|
"""Reconstruct object from shared memory metadata."""
|
|
302
341
|
if not isinstance(obj, dict):
|
|
@@ -304,7 +343,15 @@ def _from_shm(obj, unlink=True):
|
|
|
304
343
|
return [_from_shm(v, unlink) for v in obj]
|
|
305
344
|
return obj
|
|
306
345
|
|
|
307
|
-
#
|
|
346
|
+
# TensorRef -> use PyTorch's native deserialization (new format)
|
|
347
|
+
if obj.get("__type__") == "TensorRef":
|
|
348
|
+
tensor = _deserialize_tensor_ref(obj)
|
|
349
|
+
# Convert back to numpy if it was originally numpy
|
|
350
|
+
if obj.get("__was_numpy__"):
|
|
351
|
+
return tensor.numpy()
|
|
352
|
+
return tensor
|
|
353
|
+
|
|
354
|
+
# numpy array (or tensor that was converted to numpy) - legacy format
|
|
308
355
|
if "__shm_np__" in obj:
|
|
309
356
|
block = shm.SharedMemory(name=obj["__shm_np__"])
|
|
310
357
|
arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
|
|
@@ -362,10 +409,10 @@ def _serialize_for_ipc(obj, visited=None):
|
|
|
362
409
|
if obj_id in visited:
|
|
363
410
|
return visited[obj_id] # Return cached serialized result
|
|
364
411
|
|
|
365
|
-
# Handle Path objects -
|
|
412
|
+
# Handle Path objects - mark for reconstruction
|
|
366
413
|
from pathlib import PurePath
|
|
367
414
|
if isinstance(obj, PurePath):
|
|
368
|
-
return str(obj)
|
|
415
|
+
return {"__path__": str(obj)}
|
|
369
416
|
|
|
370
417
|
# Check if this is a custom object with broken module path
|
|
371
418
|
if (hasattr(obj, '__dict__') and
|
|
@@ -434,6 +481,8 @@ import socket
|
|
|
434
481
|
import struct
|
|
435
482
|
import traceback
|
|
436
483
|
import faulthandler
|
|
484
|
+
import collections
|
|
485
|
+
import time
|
|
437
486
|
from types import SimpleNamespace
|
|
438
487
|
|
|
439
488
|
# Enable faulthandler to dump traceback on SIGSEGV/SIGABRT/etc
|
|
@@ -533,6 +582,34 @@ if sys.platform == "win32":
|
|
|
533
582
|
from multiprocessing import shared_memory as shm
|
|
534
583
|
import numpy as np
|
|
535
584
|
|
|
585
|
+
# Set PyTorch to use file_system sharing (uses /dev/shm, no resource_tracker)
|
|
586
|
+
try:
|
|
587
|
+
import torch
|
|
588
|
+
import torch.multiprocessing as mp
|
|
589
|
+
mp.set_sharing_strategy("file_system")
|
|
590
|
+
wlog("[worker] PyTorch sharing strategy set to file_system")
|
|
591
|
+
except ImportError:
|
|
592
|
+
wlog("[worker] PyTorch not available")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# Tensor keeper - holds tensor references to prevent GC before parent reads shared memory
|
|
596
|
+
class TensorKeeper:
|
|
597
|
+
"""Keep tensors alive for a retention period to prevent shared memory deletion."""
|
|
598
|
+
def __init__(self, retention_seconds=30.0):
|
|
599
|
+
self.retention_seconds = retention_seconds
|
|
600
|
+
self._keeper = collections.deque()
|
|
601
|
+
self._lock = threading.Lock()
|
|
602
|
+
|
|
603
|
+
def keep(self, t):
|
|
604
|
+
now = time.time()
|
|
605
|
+
with self._lock:
|
|
606
|
+
self._keeper.append((now, t))
|
|
607
|
+
# Cleanup old entries
|
|
608
|
+
while self._keeper and now - self._keeper[0][0] > self.retention_seconds:
|
|
609
|
+
self._keeper.popleft()
|
|
610
|
+
|
|
611
|
+
_tensor_keeper = TensorKeeper()
|
|
612
|
+
|
|
536
613
|
|
|
537
614
|
def _prepare_trimesh_for_pickle(mesh):
|
|
538
615
|
"""
|
|
@@ -548,6 +625,43 @@ def _prepare_trimesh_for_pickle(mesh):
|
|
|
548
625
|
return mesh
|
|
549
626
|
|
|
550
627
|
|
|
628
|
+
def _serialize_tensor_native(t, registry):
|
|
629
|
+
"""Serialize tensor using PyTorch's native shared memory (no resource_tracker)."""
|
|
630
|
+
import torch
|
|
631
|
+
import torch.multiprocessing.reductions as reductions
|
|
632
|
+
|
|
633
|
+
# Keep tensor alive until parent reads it
|
|
634
|
+
_tensor_keeper.keep(t)
|
|
635
|
+
|
|
636
|
+
# Put tensor in shared memory via PyTorch's manager
|
|
637
|
+
if not t.is_shared():
|
|
638
|
+
t.share_memory_()
|
|
639
|
+
|
|
640
|
+
storage = t.untyped_storage()
|
|
641
|
+
sfunc, sargs = reductions.reduce_storage(storage)
|
|
642
|
+
|
|
643
|
+
if sfunc.__name__ == "rebuild_storage_filename":
|
|
644
|
+
# sargs: (cls, manager_path, storage_key, size)
|
|
645
|
+
return {
|
|
646
|
+
"__type__": "TensorRef",
|
|
647
|
+
"strategy": "file_system",
|
|
648
|
+
"manager_path": sargs[1].decode("utf-8") if isinstance(sargs[1], bytes) else sargs[1],
|
|
649
|
+
"storage_key": sargs[2].decode("utf-8") if isinstance(sargs[2], bytes) else sargs[2],
|
|
650
|
+
"storage_size": sargs[3],
|
|
651
|
+
"dtype": str(t.dtype),
|
|
652
|
+
"tensor_size": list(t.size()),
|
|
653
|
+
"tensor_stride": list(t.stride()),
|
|
654
|
+
"tensor_offset": t.storage_offset(),
|
|
655
|
+
"requires_grad": t.requires_grad,
|
|
656
|
+
}
|
|
657
|
+
else:
|
|
658
|
+
# Fallback: force file_system strategy
|
|
659
|
+
import torch.multiprocessing as mp
|
|
660
|
+
mp.set_sharing_strategy("file_system")
|
|
661
|
+
t.share_memory_()
|
|
662
|
+
return _serialize_tensor_native(t, registry)
|
|
663
|
+
|
|
664
|
+
|
|
551
665
|
def _to_shm(obj, registry, visited=None):
|
|
552
666
|
"""Serialize to shared memory. Returns JSON-safe metadata."""
|
|
553
667
|
if visited is None:
|
|
@@ -557,22 +671,26 @@ def _to_shm(obj, registry, visited=None):
|
|
|
557
671
|
return visited[obj_id]
|
|
558
672
|
t = type(obj).__name__
|
|
559
673
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
registry
|
|
565
|
-
result = {"__shm_np__": block.name, "shape": list(arr.shape), "dtype": str(arr.dtype)}
|
|
674
|
+
# Tensor -> use PyTorch's native shared memory (bypasses resource_tracker)
|
|
675
|
+
if t == 'Tensor':
|
|
676
|
+
import torch
|
|
677
|
+
tensor = obj.detach().cpu().contiguous()
|
|
678
|
+
result = _serialize_tensor_native(tensor, registry)
|
|
566
679
|
visited[obj_id] = result
|
|
567
680
|
return result
|
|
568
681
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
682
|
+
# ndarray -> convert to tensor, use PyTorch's native shared memory
|
|
683
|
+
if t == 'ndarray':
|
|
684
|
+
import torch
|
|
685
|
+
arr = np.ascontiguousarray(obj)
|
|
686
|
+
tensor = torch.from_numpy(arr)
|
|
687
|
+
result = _serialize_tensor_native(tensor, registry)
|
|
688
|
+
result["__was_numpy__"] = True
|
|
689
|
+
result["numpy_dtype"] = str(arr.dtype)
|
|
690
|
+
visited[obj_id] = result
|
|
573
691
|
return result
|
|
574
692
|
|
|
575
|
-
# trimesh.Trimesh -> pickle -> shared memory
|
|
693
|
+
# trimesh.Trimesh -> pickle -> shared memory
|
|
576
694
|
if t == 'Trimesh':
|
|
577
695
|
import pickle
|
|
578
696
|
obj = _prepare_trimesh_for_pickle(obj)
|
|
@@ -601,12 +719,62 @@ def _to_shm(obj, registry, visited=None):
|
|
|
601
719
|
|
|
602
720
|
return obj
|
|
603
721
|
|
|
722
|
+
|
|
723
|
+
def _deserialize_tensor_native(data):
|
|
724
|
+
"""Deserialize tensor from PyTorch shared memory."""
|
|
725
|
+
import torch
|
|
726
|
+
import torch.multiprocessing.reductions as reductions
|
|
727
|
+
|
|
728
|
+
dtype_str = data["dtype"]
|
|
729
|
+
dtype = getattr(torch, dtype_str.split(".")[-1])
|
|
730
|
+
|
|
731
|
+
manager_path = data["manager_path"]
|
|
732
|
+
storage_key = data["storage_key"]
|
|
733
|
+
storage_size = data["storage_size"]
|
|
734
|
+
|
|
735
|
+
# Encode to bytes if needed
|
|
736
|
+
if isinstance(manager_path, str):
|
|
737
|
+
manager_path = manager_path.encode("utf-8")
|
|
738
|
+
if isinstance(storage_key, str):
|
|
739
|
+
storage_key = storage_key.encode("utf-8")
|
|
740
|
+
|
|
741
|
+
# Rebuild storage from shared memory file
|
|
742
|
+
rebuilt_storage = reductions.rebuild_storage_filename(
|
|
743
|
+
torch.UntypedStorage, manager_path, storage_key, storage_size
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Wrap in TypedStorage
|
|
747
|
+
typed_storage = torch.storage.TypedStorage(
|
|
748
|
+
wrap_storage=rebuilt_storage, dtype=dtype, _internal=True
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# Rebuild tensor
|
|
752
|
+
metadata = (
|
|
753
|
+
data["tensor_offset"],
|
|
754
|
+
tuple(data["tensor_size"]),
|
|
755
|
+
tuple(data["tensor_stride"]),
|
|
756
|
+
data["requires_grad"],
|
|
757
|
+
)
|
|
758
|
+
tensor = reductions.rebuild_tensor(torch.Tensor, typed_storage, metadata)
|
|
759
|
+
return tensor
|
|
760
|
+
|
|
761
|
+
|
|
604
762
|
def _from_shm(obj):
|
|
605
763
|
"""Reconstruct from shared memory metadata. Does NOT unlink - caller handles that."""
|
|
606
764
|
if not isinstance(obj, dict):
|
|
607
765
|
if isinstance(obj, list):
|
|
608
766
|
return [_from_shm(v) for v in obj]
|
|
609
767
|
return obj
|
|
768
|
+
|
|
769
|
+
# TensorRef -> use PyTorch's native deserialization (new format, worker->parent)
|
|
770
|
+
if obj.get("__type__") == "TensorRef":
|
|
771
|
+
tensor = _deserialize_tensor_native(obj)
|
|
772
|
+
# Convert back to numpy if it was originally numpy
|
|
773
|
+
if obj.get("__was_numpy__"):
|
|
774
|
+
return tensor.numpy()
|
|
775
|
+
return tensor
|
|
776
|
+
|
|
777
|
+
# __shm_np__ -> legacy format (parent->worker, uses Python SharedMemory)
|
|
610
778
|
if "__shm_np__" in obj:
|
|
611
779
|
block = shm.SharedMemory(name=obj["__shm_np__"])
|
|
612
780
|
arr = np.ndarray(tuple(obj["shape"]), dtype=np.dtype(obj["dtype"]), buffer=block.buf).copy()
|
|
@@ -616,13 +784,16 @@ def _from_shm(obj):
|
|
|
616
784
|
import torch
|
|
617
785
|
return torch.from_numpy(arr)
|
|
618
786
|
return arr
|
|
619
|
-
|
|
787
|
+
|
|
788
|
+
# trimesh (pickled)
|
|
620
789
|
if "__shm_trimesh__" in obj:
|
|
621
790
|
import pickle
|
|
622
791
|
block = shm.SharedMemory(name=obj["name"])
|
|
623
792
|
mesh_bytes = bytes(block.buf[:obj["size"]])
|
|
624
793
|
block.close()
|
|
794
|
+
block.unlink()
|
|
625
795
|
return pickle.loads(mesh_bytes)
|
|
796
|
+
|
|
626
797
|
return {k: _from_shm(v) for k, v in obj.items()}
|
|
627
798
|
|
|
628
799
|
def _cleanup_shm(registry):
|
|
@@ -634,6 +805,25 @@ def _cleanup_shm(registry):
|
|
|
634
805
|
pass
|
|
635
806
|
registry.clear()
|
|
636
807
|
|
|
808
|
+
# Shared memory keeper - holds references to prevent premature GC
|
|
809
|
+
class ShmKeeper:
|
|
810
|
+
"""Keep shm blocks alive for a retention period to prevent race conditions."""
|
|
811
|
+
def __init__(self, retention_seconds=30.0):
|
|
812
|
+
self.retention_seconds = retention_seconds
|
|
813
|
+
self._keeper = collections.deque()
|
|
814
|
+
self._lock = threading.Lock()
|
|
815
|
+
|
|
816
|
+
def keep(self, blocks):
|
|
817
|
+
now = time.time()
|
|
818
|
+
with self._lock:
|
|
819
|
+
self._keeper.append((now, list(blocks))) # Copy the list
|
|
820
|
+
# Cleanup old entries
|
|
821
|
+
while self._keeper and now - self._keeper[0][0] > self.retention_seconds:
|
|
822
|
+
old_time, old_blocks = self._keeper.popleft()
|
|
823
|
+
_cleanup_shm(old_blocks)
|
|
824
|
+
|
|
825
|
+
_shm_keeper = ShmKeeper()
|
|
826
|
+
|
|
637
827
|
# =============================================================================
|
|
638
828
|
# Object Reference System - keep complex objects in worker, pass refs to host
|
|
639
829
|
# =============================================================================
|
|
@@ -796,6 +986,9 @@ def _connect(addr):
|
|
|
796
986
|
def _deserialize_isolated_objects(obj):
|
|
797
987
|
"""Reconstruct objects serialized with __isolated_object__ marker."""
|
|
798
988
|
if isinstance(obj, dict):
|
|
989
|
+
if obj.get("__path__"):
|
|
990
|
+
from pathlib import Path
|
|
991
|
+
return Path(obj["__path__"])
|
|
799
992
|
if obj.get("__isolated_object__"):
|
|
800
993
|
attrs = {k: _deserialize_isolated_objects(v) for k, v in obj.get("__attrs__", {}).items()}
|
|
801
994
|
ns = SimpleNamespace(**attrs)
|
|
@@ -941,6 +1134,7 @@ def main():
|
|
|
941
1134
|
wlog(f"[worker] Creating instance...")
|
|
942
1135
|
instance = object.__new__(cls)
|
|
943
1136
|
if self_state:
|
|
1137
|
+
self_state = _deserialize_isolated_objects(self_state)
|
|
944
1138
|
instance.__dict__.update(self_state)
|
|
945
1139
|
wlog(f"[worker] Calling {method_name}...")
|
|
946
1140
|
method = getattr(instance, method_name)
|
|
@@ -957,7 +1151,7 @@ def main():
|
|
|
957
1151
|
wlog(f"[worker] Created {len(shm_registry)} shm blocks for result")
|
|
958
1152
|
|
|
959
1153
|
transport.send({"status": "ok", "result": result_meta})
|
|
960
|
-
#
|
|
1154
|
+
_shm_keeper.keep(shm_registry) # Keep alive for 30s until host reads
|
|
961
1155
|
|
|
962
1156
|
except Exception as e:
|
|
963
1157
|
# Cleanup shm on error since host won't read it
|
|
@@ -1134,7 +1328,9 @@ class SubprocessWorker(Worker):
|
|
|
1134
1328
|
lib_dir = self.python.parent.parent / "lib"
|
|
1135
1329
|
if lib_dir.is_dir():
|
|
1136
1330
|
existing = env.get("LD_LIBRARY_PATH", "")
|
|
1137
|
-
|
|
1331
|
+
# Also include system library paths so apt-installed libs (OpenGL, etc.) are found
|
|
1332
|
+
system_libs = "/usr/lib/x86_64-linux-gnu:/usr/lib:/lib/x86_64-linux-gnu"
|
|
1333
|
+
env["LD_LIBRARY_PATH"] = f"{lib_dir}:{system_libs}:{existing}" if existing else f"{lib_dir}:{system_libs}"
|
|
1138
1334
|
|
|
1139
1335
|
# On Windows, pass host Python directory so worker can add it via os.add_dll_directory()
|
|
1140
1336
|
# This fixes "DLL load failed" errors for packages like opencv-python-headless
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""APT package installation (Linux only)."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Callable, List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def apt_install(packages: List[str], log: Callable[[str], None] = print) -> bool:
|
|
9
|
+
"""Install system packages via apt-get. No-op on non-Linux."""
|
|
10
|
+
if not packages or sys.platform != "linux":
|
|
11
|
+
return True
|
|
12
|
+
|
|
13
|
+
log(f"[apt] Requested packages: {packages}")
|
|
14
|
+
|
|
15
|
+
# Check which packages are missing
|
|
16
|
+
missing = check_apt_packages(packages)
|
|
17
|
+
if not missing:
|
|
18
|
+
log("[apt] All packages already installed")
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
log(f"[apt] Missing packages: {missing}")
|
|
22
|
+
|
|
23
|
+
# Run apt-get update with full output
|
|
24
|
+
log("[apt] Running: sudo apt-get update")
|
|
25
|
+
update_result = subprocess.run(
|
|
26
|
+
["sudo", "apt-get", "update"],
|
|
27
|
+
capture_output=True, text=True
|
|
28
|
+
)
|
|
29
|
+
if update_result.returncode != 0:
|
|
30
|
+
log(f"[apt] WARNING: apt-get update failed (exit {update_result.returncode})")
|
|
31
|
+
log(f"[apt] stderr: {update_result.stderr}")
|
|
32
|
+
else:
|
|
33
|
+
log("[apt] apt-get update succeeded")
|
|
34
|
+
|
|
35
|
+
# Install missing packages
|
|
36
|
+
log(f"[apt] Running: sudo apt-get install -y {' '.join(missing)}")
|
|
37
|
+
result = subprocess.run(
|
|
38
|
+
["sudo", "apt-get", "install", "-y"] + missing,
|
|
39
|
+
capture_output=True, text=True
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if result.returncode != 0:
|
|
43
|
+
log(f"[apt] ERROR: apt-get install failed (exit {result.returncode})")
|
|
44
|
+
log(f"[apt] stdout: {result.stdout}")
|
|
45
|
+
log(f"[apt] stderr: {result.stderr}")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
log("[apt] Installation succeeded")
|
|
49
|
+
|
|
50
|
+
# Verify installation
|
|
51
|
+
still_missing = check_apt_packages(missing)
|
|
52
|
+
if still_missing:
|
|
53
|
+
log(f"[apt] WARNING: These packages still not found: {still_missing}")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
log("[apt] All packages verified installed")
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def check_apt_packages(packages: List[str]) -> List[str]:
|
|
61
|
+
"""Return list of packages NOT installed."""
|
|
62
|
+
if sys.platform != "linux":
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
pkg for pkg in packages
|
|
67
|
+
if subprocess.run(["dpkg", "-s", pkg], capture_output=True).returncode != 0
|
|
68
|
+
]
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"""APT package installation (Linux only)."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
from typing import Callable, List
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def apt_install(packages: List[str], log: Callable[[str], None] = print) -> bool:
|
|
9
|
-
"""Install system packages via apt-get. No-op on non-Linux."""
|
|
10
|
-
if not packages or sys.platform != "linux":
|
|
11
|
-
return True
|
|
12
|
-
|
|
13
|
-
log(f"Installing apt packages: {packages}")
|
|
14
|
-
|
|
15
|
-
subprocess.run(["sudo", "apt-get", "update"], capture_output=True, text=True)
|
|
16
|
-
|
|
17
|
-
result = subprocess.run(
|
|
18
|
-
["sudo", "apt-get", "install", "-y"] + packages,
|
|
19
|
-
capture_output=True, text=True
|
|
20
|
-
)
|
|
21
|
-
if result.returncode != 0:
|
|
22
|
-
log(f"Warning: apt-get install failed: {result.stderr[:200]}")
|
|
23
|
-
return False
|
|
24
|
-
|
|
25
|
-
return True
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def check_apt_packages(packages: List[str]) -> List[str]:
|
|
29
|
-
"""Return list of packages NOT installed."""
|
|
30
|
-
if sys.platform != "linux":
|
|
31
|
-
return []
|
|
32
|
-
|
|
33
|
-
return [
|
|
34
|
-
pkg for pkg in packages
|
|
35
|
-
if subprocess.run(["dpkg", "-s", pkg], capture_output=True).returncode != 0
|
|
36
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|