comfygit-deploy 0.3.4__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.
- comfygit_deploy/__init__.py +3 -0
- comfygit_deploy/cli.py +374 -0
- comfygit_deploy/commands/__init__.py +5 -0
- comfygit_deploy/commands/custom.py +218 -0
- comfygit_deploy/commands/dev.py +356 -0
- comfygit_deploy/commands/instances.py +506 -0
- comfygit_deploy/commands/runpod.py +203 -0
- comfygit_deploy/commands/worker.py +266 -0
- comfygit_deploy/config.py +122 -0
- comfygit_deploy/providers/__init__.py +11 -0
- comfygit_deploy/providers/custom.py +238 -0
- comfygit_deploy/providers/runpod.py +549 -0
- comfygit_deploy/startup/__init__.py +1 -0
- comfygit_deploy/startup/scripts.py +210 -0
- comfygit_deploy/worker/__init__.py +12 -0
- comfygit_deploy/worker/mdns.py +154 -0
- comfygit_deploy/worker/native_manager.py +438 -0
- comfygit_deploy/worker/server.py +511 -0
- comfygit_deploy/worker/state.py +268 -0
- comfygit_deploy-0.3.4.dist-info/METADATA +38 -0
- comfygit_deploy-0.3.4.dist-info/RECORD +23 -0
- comfygit_deploy-0.3.4.dist-info/WHEEL +4 -0
- comfygit_deploy-0.3.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Worker instance state management with persistent port allocation.
|
|
2
|
+
|
|
3
|
+
Manages instance lifecycle and persists state to JSON for recovery across restarts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
import socket
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class InstanceState:
|
|
18
|
+
"""State for a single ComfyUI instance."""
|
|
19
|
+
|
|
20
|
+
id: str
|
|
21
|
+
name: str
|
|
22
|
+
environment_name: str
|
|
23
|
+
mode: str # "docker" | "native"
|
|
24
|
+
assigned_port: int
|
|
25
|
+
import_source: str
|
|
26
|
+
branch: str | None = None
|
|
27
|
+
status: str = "stopped" # "deploying" | "running" | "stopped" | "error"
|
|
28
|
+
container_id: str | None = None
|
|
29
|
+
pid: int | None = None
|
|
30
|
+
created_at: str = field(
|
|
31
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict[str, Any]:
|
|
35
|
+
"""Serialize to dict for JSON storage."""
|
|
36
|
+
return {
|
|
37
|
+
"id": self.id,
|
|
38
|
+
"name": self.name,
|
|
39
|
+
"environment_name": self.environment_name,
|
|
40
|
+
"mode": self.mode,
|
|
41
|
+
"assigned_port": self.assigned_port,
|
|
42
|
+
"import_source": self.import_source,
|
|
43
|
+
"branch": self.branch,
|
|
44
|
+
"status": self.status,
|
|
45
|
+
"container_id": self.container_id,
|
|
46
|
+
"pid": self.pid,
|
|
47
|
+
"created_at": self.created_at,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_dict(cls, data: dict[str, Any]) -> "InstanceState":
|
|
52
|
+
"""Deserialize from dict."""
|
|
53
|
+
return cls(
|
|
54
|
+
id=data["id"],
|
|
55
|
+
name=data["name"],
|
|
56
|
+
environment_name=data["environment_name"],
|
|
57
|
+
mode=data["mode"],
|
|
58
|
+
assigned_port=data["assigned_port"],
|
|
59
|
+
import_source=data["import_source"],
|
|
60
|
+
branch=data.get("branch"),
|
|
61
|
+
status=data.get("status", "stopped"),
|
|
62
|
+
container_id=data.get("container_id"),
|
|
63
|
+
pid=data.get("pid"),
|
|
64
|
+
created_at=data.get(
|
|
65
|
+
"created_at", datetime.now(timezone.utc).isoformat()
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PortAllocator:
|
|
71
|
+
"""Manages port allocation for instances.
|
|
72
|
+
|
|
73
|
+
Ports are allocated at instance creation and persist across stop/start.
|
|
74
|
+
Only released when instance is terminated.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
state_file: Path,
|
|
80
|
+
base_port: int = 8188,
|
|
81
|
+
max_instances: int = 10,
|
|
82
|
+
):
|
|
83
|
+
"""Initialize port allocator.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
state_file: Path to state JSON file
|
|
87
|
+
base_port: First port in range
|
|
88
|
+
max_instances: Maximum concurrent instances
|
|
89
|
+
"""
|
|
90
|
+
self.state_file = state_file
|
|
91
|
+
self.base_port = base_port
|
|
92
|
+
self.max_port = base_port + max_instances
|
|
93
|
+
self.allocated: dict[str, int] = {}
|
|
94
|
+
self._load()
|
|
95
|
+
|
|
96
|
+
def _load(self) -> None:
|
|
97
|
+
"""Load allocated ports from state file."""
|
|
98
|
+
if not self.state_file.exists():
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
data = json.loads(self.state_file.read_text())
|
|
103
|
+
instances = data.get("instances", {})
|
|
104
|
+
for inst_id, inst_data in instances.items():
|
|
105
|
+
if "assigned_port" in inst_data:
|
|
106
|
+
self.allocated[inst_id] = inst_data["assigned_port"]
|
|
107
|
+
except (json.JSONDecodeError, OSError):
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def _persist(self) -> None:
|
|
111
|
+
"""Persist port allocations (called from WorkerState.save)."""
|
|
112
|
+
pass # WorkerState handles persistence
|
|
113
|
+
|
|
114
|
+
def _is_port_in_use(self, port: int) -> bool:
|
|
115
|
+
"""Check if port is currently in use by another process."""
|
|
116
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
117
|
+
try:
|
|
118
|
+
s.bind(("127.0.0.1", port))
|
|
119
|
+
return False
|
|
120
|
+
except OSError:
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def allocate(self, instance_id: str) -> int:
|
|
124
|
+
"""Allocate port for instance.
|
|
125
|
+
|
|
126
|
+
Finds a port that is both not allocated to another instance AND
|
|
127
|
+
not currently in use by any process (handles orphan processes).
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
instance_id: Unique instance identifier
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Allocated port number
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
RuntimeError: If no ports available
|
|
137
|
+
"""
|
|
138
|
+
# Return existing allocation if present
|
|
139
|
+
if instance_id in self.allocated:
|
|
140
|
+
return self.allocated[instance_id]
|
|
141
|
+
|
|
142
|
+
# Find next available port (not allocated AND not in use)
|
|
143
|
+
used_ports = set(self.allocated.values())
|
|
144
|
+
for port in range(self.base_port, self.max_port):
|
|
145
|
+
if port not in used_ports and not self._is_port_in_use(port):
|
|
146
|
+
self.allocated[instance_id] = port
|
|
147
|
+
return port
|
|
148
|
+
|
|
149
|
+
raise RuntimeError("No available ports")
|
|
150
|
+
|
|
151
|
+
def release(self, instance_id: str) -> None:
|
|
152
|
+
"""Release port when instance terminated."""
|
|
153
|
+
self.allocated.pop(instance_id, None)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class WorkerState:
|
|
157
|
+
"""Persistent state for all instances managed by this worker."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, state_file: Path, workspace_path: Path | None = None):
|
|
160
|
+
"""Initialize worker state.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
state_file: Path to instances.json
|
|
164
|
+
workspace_path: ComfyGit workspace path (for environment validation)
|
|
165
|
+
"""
|
|
166
|
+
self.state_file = state_file
|
|
167
|
+
self.workspace_path = workspace_path
|
|
168
|
+
self.instances: dict[str, InstanceState] = {}
|
|
169
|
+
self._load()
|
|
170
|
+
if workspace_path:
|
|
171
|
+
self._validate_instances()
|
|
172
|
+
|
|
173
|
+
def _validate_instances(self) -> None:
|
|
174
|
+
"""Remove instances whose environments no longer exist.
|
|
175
|
+
|
|
176
|
+
Checks for .cec/.complete marker file to confirm environment is valid.
|
|
177
|
+
Kills any running processes before removing instances.
|
|
178
|
+
Persists cleanup to disk if any instances were removed.
|
|
179
|
+
"""
|
|
180
|
+
if not self.workspace_path:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
envs_dir = self.workspace_path / "environments"
|
|
184
|
+
orphans = []
|
|
185
|
+
|
|
186
|
+
for inst_id, inst in self.instances.items():
|
|
187
|
+
marker = envs_dir / inst.environment_name / ".cec" / ".complete"
|
|
188
|
+
if not marker.exists():
|
|
189
|
+
orphans.append((inst_id, inst))
|
|
190
|
+
|
|
191
|
+
if orphans:
|
|
192
|
+
for inst_id, inst in orphans:
|
|
193
|
+
# Kill any running process before removing
|
|
194
|
+
self._kill_instance_process(inst)
|
|
195
|
+
del self.instances[inst_id]
|
|
196
|
+
self.save()
|
|
197
|
+
|
|
198
|
+
def _kill_instance_process(self, inst: InstanceState) -> None:
|
|
199
|
+
"""Kill a running instance process by PID.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
inst: Instance to kill
|
|
203
|
+
"""
|
|
204
|
+
if not inst.pid:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
# Send SIGTERM to process group
|
|
209
|
+
os.killpg(os.getpgid(inst.pid), signal.SIGTERM)
|
|
210
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
211
|
+
# Process already gone or we can't kill it
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
def _load(self) -> None:
|
|
215
|
+
"""Load state from disk."""
|
|
216
|
+
if not self.state_file.exists():
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
data = json.loads(self.state_file.read_text())
|
|
221
|
+
for inst_id, inst_data in data.get("instances", {}).items():
|
|
222
|
+
self.instances[inst_id] = InstanceState.from_dict(inst_data)
|
|
223
|
+
except (json.JSONDecodeError, OSError):
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
def save(self) -> None:
|
|
227
|
+
"""Persist state to disk."""
|
|
228
|
+
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
229
|
+
|
|
230
|
+
data = {
|
|
231
|
+
"version": "1",
|
|
232
|
+
"instances": {
|
|
233
|
+
inst_id: inst.to_dict() for inst_id, inst in self.instances.items()
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
self.state_file.write_text(json.dumps(data, indent=2))
|
|
237
|
+
|
|
238
|
+
def add_instance(self, instance: InstanceState) -> None:
|
|
239
|
+
"""Add instance to state."""
|
|
240
|
+
self.instances[instance.id] = instance
|
|
241
|
+
|
|
242
|
+
def remove_instance(self, instance_id: str) -> None:
|
|
243
|
+
"""Remove instance from state."""
|
|
244
|
+
self.instances.pop(instance_id, None)
|
|
245
|
+
|
|
246
|
+
def update_status(
|
|
247
|
+
self,
|
|
248
|
+
instance_id: str,
|
|
249
|
+
status: str,
|
|
250
|
+
container_id: str | None = None,
|
|
251
|
+
pid: int | None = None,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Update instance status.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
instance_id: Instance to update
|
|
257
|
+
status: New status
|
|
258
|
+
container_id: Container ID (for docker mode)
|
|
259
|
+
pid: Process ID (for native mode)
|
|
260
|
+
"""
|
|
261
|
+
if instance_id not in self.instances:
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
self.instances[instance_id].status = status
|
|
265
|
+
if container_id is not None:
|
|
266
|
+
self.instances[instance_id].container_id = container_id
|
|
267
|
+
if pid is not None:
|
|
268
|
+
self.instances[instance_id].pid = pid
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: comfygit-deploy
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: ComfyGit Deploy - Remote deployment and worker management CLI
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
7
|
+
Requires-Dist: comfygit==0.3.4
|
|
8
|
+
Requires-Dist: zeroconf>=0.131.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# ComfyGit Deploy
|
|
12
|
+
|
|
13
|
+
Remote deployment and worker management CLI for ComfyGit.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install comfygit-deploy
|
|
19
|
+
# or
|
|
20
|
+
uv tool install comfygit-deploy
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Configure RunPod
|
|
27
|
+
cg-deploy runpod config --api-key <your-key>
|
|
28
|
+
|
|
29
|
+
# Deploy to RunPod
|
|
30
|
+
cg-deploy runpod deploy <git-url> --gpu "RTX 4090"
|
|
31
|
+
|
|
32
|
+
# Manage instances
|
|
33
|
+
cg-deploy instances
|
|
34
|
+
cg-deploy stop <instance-id>
|
|
35
|
+
cg-deploy terminate <instance-id>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See the [documentation](../../docs/comfygit-docs/) for full usage information.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
comfygit_deploy/__init__.py,sha256=Q7wYvVHTPr7HycCd3BE-urzDRTES1CRA7Gr7qLxpAIg,92
|
|
2
|
+
comfygit_deploy/cli.py,sha256=_bRjuxBrIpzOzuOXpjXXZsRL_Ptv11nIstNxJDRzVpk,15221
|
|
3
|
+
comfygit_deploy/config.py,sha256=64KWd0Ry5z7Iqpzx8YiE_c51Cs8iYVecQe8UxzZPUVk,3582
|
|
4
|
+
comfygit_deploy/commands/__init__.py,sha256=0bI5lasM3-FC-xS7vwsTMAInfkNqzvI6BztYQ_SvBes,139
|
|
5
|
+
comfygit_deploy/commands/custom.py,sha256=UyOngaDsZlzKLSNW873HD2ma7W0nFv_yYx9deud10Qk,6234
|
|
6
|
+
comfygit_deploy/commands/dev.py,sha256=3fQoppdMtxOiyN9Y--TT5GKTiiOWEI5JF1xYdE76x_0,11138
|
|
7
|
+
comfygit_deploy/commands/instances.py,sha256=R7edUNUZSgt_dB7OWUMQBwOg-dvCLR_dvax107OvsqE,16632
|
|
8
|
+
comfygit_deploy/commands/runpod.py,sha256=OJWk46I2yoCsu_o95C_ktmJ6EnPXU4rki4SraBUeaKo,6309
|
|
9
|
+
comfygit_deploy/commands/worker.py,sha256=CJaaSQ0w-dcLQI52ePSZgzYB4yf-2oC_2fXCl5Na-Ec,7657
|
|
10
|
+
comfygit_deploy/providers/__init__.py,sha256=75-usN95nTBMf_aEGkEZOFlNoz56TXZNSpL8huBDDdk,264
|
|
11
|
+
comfygit_deploy/providers/custom.py,sha256=DeOIvNy1xgN4GSU8lWlaLdKsue65c5tFTFJSoMrBfZo,7817
|
|
12
|
+
comfygit_deploy/providers/runpod.py,sha256=dFTSMzGzL3fY7GKzZ799ESjp-QvguQKmwvHMN923-lM,19220
|
|
13
|
+
comfygit_deploy/startup/__init__.py,sha256=qFZ7-9E7m8p6ulyvNak4Iz5B5m8pX2DhQGJFmgb17Bc,49
|
|
14
|
+
comfygit_deploy/startup/scripts.py,sha256=OsWIgOqQ-GbcIJ8S72cvL_e3r433z-LkM9Ycqft8c34,6899
|
|
15
|
+
comfygit_deploy/worker/__init__.py,sha256=ty_5VXMQXAr5w7XWi_m65e1Vw0NUlIwuxIGo24KadE8,294
|
|
16
|
+
comfygit_deploy/worker/mdns.py,sha256=sN6VnGVW8R-TyMrphZo4_WMGWEuhe5nrGDMR0hzIEtA,4899
|
|
17
|
+
comfygit_deploy/worker/native_manager.py,sha256=ltvjY5PI4bUBF9JkGHzfbMv1I6DncxzCWEPAsAVf4Sw,13304
|
|
18
|
+
comfygit_deploy/worker/server.py,sha256=UI1rdcMkM4xY1rDtm9ayAzKJauVD8323LpO4OwkQsYk,17503
|
|
19
|
+
comfygit_deploy/worker/state.py,sha256=HxqPNG5_ltf_KL2VM-3-ZVzUnZTqCTLgzF9AvIcvWqo,8655
|
|
20
|
+
comfygit_deploy-0.3.4.dist-info/METADATA,sha256=Hpc_tFfNo4BZp-s-kg87RQOTzVY4wXKt12fv_5kcg00,802
|
|
21
|
+
comfygit_deploy-0.3.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
22
|
+
comfygit_deploy-0.3.4.dist-info/entry_points.txt,sha256=ce_A6mSttdZGySZbWok6zySv93sjtDaKX6De23i3ylM,55
|
|
23
|
+
comfygit_deploy-0.3.4.dist-info/RECORD,,
|