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,511 @@
|
|
|
1
|
+
"""Worker HTTP server for managing ComfyUI instances.
|
|
2
|
+
|
|
3
|
+
Provides REST API for creating, starting, stopping, and terminating instances.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import secrets
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from aiohttp import web
|
|
13
|
+
|
|
14
|
+
from .. import __version__
|
|
15
|
+
from .native_manager import NativeManager
|
|
16
|
+
from .state import InstanceState, PortAllocator, WorkerState
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_instance_id() -> str:
|
|
20
|
+
"""Generate unique instance ID."""
|
|
21
|
+
return f"inst_{secrets.token_hex(4)}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_instance_name(user_name: str | None) -> str:
|
|
25
|
+
"""Generate instance name with timestamp."""
|
|
26
|
+
import re
|
|
27
|
+
base = user_name or "unnamed"
|
|
28
|
+
# Sanitize: lowercase, replace non-alphanumeric with hyphen, collapse multiples
|
|
29
|
+
base = re.sub(r'[^a-z0-9]+', '-', base.lower()).strip('-')[:32] or "unnamed"
|
|
30
|
+
date = datetime.now(timezone.utc).strftime("%Y%m%d")
|
|
31
|
+
suffix = secrets.token_hex(2)
|
|
32
|
+
return f"deploy-{base}-{date}-{suffix}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WorkerServer:
|
|
36
|
+
"""Worker HTTP server managing ComfyUI instances."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
api_key: str,
|
|
41
|
+
workspace_path: Path,
|
|
42
|
+
default_mode: str = "docker",
|
|
43
|
+
port_range_start: int = 8200,
|
|
44
|
+
port_range_end: int = 8210,
|
|
45
|
+
state_dir: Path | None = None,
|
|
46
|
+
):
|
|
47
|
+
"""Initialize worker server.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
api_key: API key for authentication
|
|
51
|
+
workspace_path: ComfyGit workspace path
|
|
52
|
+
default_mode: Default instance mode (docker/native)
|
|
53
|
+
port_range_start: First port for instances
|
|
54
|
+
port_range_end: Last port for instances
|
|
55
|
+
state_dir: Directory for state files
|
|
56
|
+
"""
|
|
57
|
+
self.api_key = api_key
|
|
58
|
+
self.workspace_path = workspace_path
|
|
59
|
+
self.default_mode = default_mode
|
|
60
|
+
self.port_range_start = port_range_start
|
|
61
|
+
self.port_range_end = port_range_end
|
|
62
|
+
|
|
63
|
+
state_dir = state_dir or Path.home() / ".config" / "comfygit" / "deploy"
|
|
64
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
self.state = WorkerState(
|
|
67
|
+
state_dir / "instances.json", workspace_path=workspace_path
|
|
68
|
+
)
|
|
69
|
+
self.port_allocator = PortAllocator(
|
|
70
|
+
state_dir / "instances.json",
|
|
71
|
+
base_port=port_range_start,
|
|
72
|
+
max_instances=port_range_end - port_range_start,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Instance managers by mode
|
|
76
|
+
self.native_manager = NativeManager(workspace_path)
|
|
77
|
+
# self.docker_manager = DockerManager(workspace_path) # Future
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@web.middleware
|
|
81
|
+
async def auth_middleware(
|
|
82
|
+
request: web.Request, handler: Any
|
|
83
|
+
) -> web.StreamResponse:
|
|
84
|
+
"""Validate API key in Authorization header."""
|
|
85
|
+
# Skip auth for certain paths if needed
|
|
86
|
+
auth_header = request.headers.get("Authorization", "")
|
|
87
|
+
expected_key = request.app["worker"].api_key
|
|
88
|
+
|
|
89
|
+
if not auth_header.startswith("Bearer "):
|
|
90
|
+
return web.json_response({"error": "Missing authorization"}, status=401)
|
|
91
|
+
|
|
92
|
+
provided_key = auth_header[7:]
|
|
93
|
+
if provided_key != expected_key:
|
|
94
|
+
return web.json_response({"error": "Invalid API key"}, status=401)
|
|
95
|
+
|
|
96
|
+
response: web.StreamResponse = await handler(request)
|
|
97
|
+
return response
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def handle_health(request: web.Request) -> web.Response:
|
|
101
|
+
"""GET /api/v1/health - Health check endpoint."""
|
|
102
|
+
return web.json_response({"status": "ok", "worker_version": __version__})
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def handle_system_info(request: web.Request) -> web.Response:
|
|
106
|
+
"""GET /api/v1/system/info - System information."""
|
|
107
|
+
worker: WorkerServer = request.app["worker"]
|
|
108
|
+
|
|
109
|
+
# Count instance states
|
|
110
|
+
instances = worker.state.instances
|
|
111
|
+
running = sum(1 for i in instances.values() if i.status == "running")
|
|
112
|
+
stopped = sum(1 for i in instances.values() if i.status == "stopped")
|
|
113
|
+
|
|
114
|
+
return web.json_response({
|
|
115
|
+
"worker_version": __version__,
|
|
116
|
+
"workspace_path": str(worker.workspace_path),
|
|
117
|
+
"default_mode": worker.default_mode,
|
|
118
|
+
"instances": {
|
|
119
|
+
"total": len(instances),
|
|
120
|
+
"running": running,
|
|
121
|
+
"stopped": stopped,
|
|
122
|
+
},
|
|
123
|
+
"ports": {
|
|
124
|
+
"range_start": worker.port_range_start,
|
|
125
|
+
"range_end": worker.port_range_end,
|
|
126
|
+
"allocated": list(worker.port_allocator.allocated.values()),
|
|
127
|
+
"available": (worker.port_range_end - worker.port_range_start)
|
|
128
|
+
- len(worker.port_allocator.allocated),
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def handle_list_instances(request: web.Request) -> web.Response:
|
|
134
|
+
"""GET /api/v1/instances - List all instances."""
|
|
135
|
+
worker: WorkerServer = request.app["worker"]
|
|
136
|
+
|
|
137
|
+
instances = [
|
|
138
|
+
{
|
|
139
|
+
"id": inst.id,
|
|
140
|
+
"name": inst.name,
|
|
141
|
+
"status": inst.status,
|
|
142
|
+
"mode": inst.mode,
|
|
143
|
+
"assigned_port": inst.assigned_port,
|
|
144
|
+
"comfyui_url": f"http://localhost:{inst.assigned_port}"
|
|
145
|
+
if inst.status == "running"
|
|
146
|
+
else None,
|
|
147
|
+
"created_at": inst.created_at,
|
|
148
|
+
}
|
|
149
|
+
for inst in worker.state.instances.values()
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
return web.json_response({
|
|
153
|
+
"instances": instances,
|
|
154
|
+
"port_range": {
|
|
155
|
+
"start": worker.port_range_start,
|
|
156
|
+
"end": worker.port_range_end,
|
|
157
|
+
},
|
|
158
|
+
"ports_available": (worker.port_range_end - worker.port_range_start)
|
|
159
|
+
- len(worker.port_allocator.allocated),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def handle_create_instance(request: web.Request) -> web.Response:
|
|
164
|
+
"""POST /api/v1/instances - Create new instance."""
|
|
165
|
+
worker: WorkerServer = request.app["worker"]
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
data = await request.json()
|
|
169
|
+
except Exception:
|
|
170
|
+
return web.json_response({"error": "Invalid JSON"}, status=400)
|
|
171
|
+
|
|
172
|
+
import_source = data.get("import_source")
|
|
173
|
+
if not import_source:
|
|
174
|
+
return web.json_response(
|
|
175
|
+
{"error": "import_source is required"}, status=400
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
name = data.get("name")
|
|
179
|
+
mode = data.get("mode", worker.default_mode)
|
|
180
|
+
branch = data.get("branch")
|
|
181
|
+
|
|
182
|
+
# Generate IDs and allocate port
|
|
183
|
+
instance_id = generate_instance_id()
|
|
184
|
+
instance_name = generate_instance_name(name)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
port = worker.port_allocator.allocate(instance_id)
|
|
188
|
+
except RuntimeError as e:
|
|
189
|
+
return web.json_response({"error": str(e)}, status=503)
|
|
190
|
+
|
|
191
|
+
# Create instance state
|
|
192
|
+
instance = InstanceState(
|
|
193
|
+
id=instance_id,
|
|
194
|
+
name=instance_name,
|
|
195
|
+
environment_name=instance_name,
|
|
196
|
+
mode=mode,
|
|
197
|
+
assigned_port=port,
|
|
198
|
+
import_source=import_source,
|
|
199
|
+
branch=branch,
|
|
200
|
+
status="deploying",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
worker.state.add_instance(instance)
|
|
204
|
+
worker.state.save()
|
|
205
|
+
|
|
206
|
+
# Start deployment in background task
|
|
207
|
+
asyncio.create_task(_deploy_instance(worker, instance))
|
|
208
|
+
|
|
209
|
+
return web.json_response(
|
|
210
|
+
{
|
|
211
|
+
"id": instance.id,
|
|
212
|
+
"name": instance.name,
|
|
213
|
+
"environment_name": instance.environment_name,
|
|
214
|
+
"status": instance.status,
|
|
215
|
+
"mode": instance.mode,
|
|
216
|
+
"assigned_port": instance.assigned_port,
|
|
217
|
+
"created_at": instance.created_at,
|
|
218
|
+
},
|
|
219
|
+
status=201,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def _deploy_instance(worker: WorkerServer, instance: InstanceState) -> None:
|
|
224
|
+
"""Background task to deploy and start an instance."""
|
|
225
|
+
try:
|
|
226
|
+
if instance.mode == "native":
|
|
227
|
+
# Deploy environment (may skip if already exists)
|
|
228
|
+
result = await worker.native_manager.deploy(
|
|
229
|
+
instance_id=instance.id,
|
|
230
|
+
environment_name=instance.environment_name,
|
|
231
|
+
import_source=instance.import_source,
|
|
232
|
+
branch=instance.branch,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if not result.success:
|
|
236
|
+
worker.state.update_status(instance.id, "error")
|
|
237
|
+
worker.state.save()
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
# Start ComfyUI process
|
|
241
|
+
worker.state.update_status(instance.id, "starting")
|
|
242
|
+
worker.state.save()
|
|
243
|
+
|
|
244
|
+
proc_info = worker.native_manager.start(
|
|
245
|
+
instance_id=instance.id,
|
|
246
|
+
environment_name=instance.environment_name,
|
|
247
|
+
port=instance.assigned_port,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if not proc_info:
|
|
251
|
+
worker.state.update_status(instance.id, "error")
|
|
252
|
+
worker.state.save()
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Wait for ComfyUI to become ready
|
|
256
|
+
is_ready = await worker.native_manager.wait_for_ready(
|
|
257
|
+
port=instance.assigned_port,
|
|
258
|
+
timeout_seconds=120.0,
|
|
259
|
+
poll_interval=2.0,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if is_ready:
|
|
263
|
+
worker.state.update_status(instance.id, "running", pid=proc_info.pid)
|
|
264
|
+
else:
|
|
265
|
+
# Process started but HTTP not responding
|
|
266
|
+
worker.state.update_status(instance.id, "error")
|
|
267
|
+
else:
|
|
268
|
+
# Docker mode - not yet implemented
|
|
269
|
+
worker.state.update_status(instance.id, "error")
|
|
270
|
+
|
|
271
|
+
worker.state.save()
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
print(f"Deployment failed for {instance.id}: {e}")
|
|
275
|
+
worker.state.update_status(instance.id, "error")
|
|
276
|
+
worker.state.save()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def handle_get_instance(request: web.Request) -> web.Response:
|
|
280
|
+
"""GET /api/v1/instances/{id} - Get instance details."""
|
|
281
|
+
worker: WorkerServer = request.app["worker"]
|
|
282
|
+
instance_id = request.match_info["id"]
|
|
283
|
+
|
|
284
|
+
instance = worker.state.instances.get(instance_id)
|
|
285
|
+
if not instance:
|
|
286
|
+
return web.json_response({"error": "Instance not found"}, status=404)
|
|
287
|
+
|
|
288
|
+
return web.json_response({
|
|
289
|
+
"id": instance.id,
|
|
290
|
+
"name": instance.name,
|
|
291
|
+
"environment_name": instance.environment_name,
|
|
292
|
+
"status": instance.status,
|
|
293
|
+
"mode": instance.mode,
|
|
294
|
+
"assigned_port": instance.assigned_port,
|
|
295
|
+
"import_source": instance.import_source,
|
|
296
|
+
"branch": instance.branch,
|
|
297
|
+
"container_id": instance.container_id,
|
|
298
|
+
"pid": instance.pid,
|
|
299
|
+
"created_at": instance.created_at,
|
|
300
|
+
"comfyui_url": f"http://localhost:{instance.assigned_port}"
|
|
301
|
+
if instance.status == "running"
|
|
302
|
+
else None,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
async def handle_stop_instance(request: web.Request) -> web.Response:
|
|
307
|
+
"""POST /api/v1/instances/{id}/stop - Stop instance."""
|
|
308
|
+
worker: WorkerServer = request.app["worker"]
|
|
309
|
+
instance_id = request.match_info["id"]
|
|
310
|
+
|
|
311
|
+
instance = worker.state.instances.get(instance_id)
|
|
312
|
+
if not instance:
|
|
313
|
+
return web.json_response({"error": "Instance not found"}, status=404)
|
|
314
|
+
|
|
315
|
+
# Stop based on mode
|
|
316
|
+
if instance.mode == "native":
|
|
317
|
+
worker.native_manager.stop(instance_id, pid=instance.pid)
|
|
318
|
+
# Docker mode would go here
|
|
319
|
+
|
|
320
|
+
worker.state.update_status(instance_id, "stopped")
|
|
321
|
+
worker.state.save()
|
|
322
|
+
|
|
323
|
+
return web.json_response({
|
|
324
|
+
"id": instance.id,
|
|
325
|
+
"status": "stopped",
|
|
326
|
+
"assigned_port": instance.assigned_port,
|
|
327
|
+
"message": f"Instance stopped. Port {instance.assigned_port} remains reserved.",
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
async def handle_start_instance(request: web.Request) -> web.Response:
|
|
332
|
+
"""POST /api/v1/instances/{id}/start - Start stopped instance."""
|
|
333
|
+
worker: WorkerServer = request.app["worker"]
|
|
334
|
+
instance_id = request.match_info["id"]
|
|
335
|
+
|
|
336
|
+
instance = worker.state.instances.get(instance_id)
|
|
337
|
+
if not instance:
|
|
338
|
+
return web.json_response({"error": "Instance not found"}, status=404)
|
|
339
|
+
|
|
340
|
+
# Start based on mode
|
|
341
|
+
if instance.mode == "native":
|
|
342
|
+
proc_info = worker.native_manager.start(
|
|
343
|
+
instance_id=instance_id,
|
|
344
|
+
environment_name=instance.environment_name,
|
|
345
|
+
port=instance.assigned_port,
|
|
346
|
+
)
|
|
347
|
+
if proc_info:
|
|
348
|
+
worker.state.update_status(instance_id, "running", pid=proc_info.pid)
|
|
349
|
+
else:
|
|
350
|
+
return web.json_response({"error": "Failed to start instance"}, status=500)
|
|
351
|
+
else:
|
|
352
|
+
return web.json_response({"error": "Docker mode not yet supported"}, status=501)
|
|
353
|
+
|
|
354
|
+
worker.state.save()
|
|
355
|
+
|
|
356
|
+
return web.json_response({
|
|
357
|
+
"id": instance.id,
|
|
358
|
+
"status": "running",
|
|
359
|
+
"assigned_port": instance.assigned_port,
|
|
360
|
+
"comfyui_url": f"http://localhost:{instance.assigned_port}",
|
|
361
|
+
"message": f"Instance started on port {instance.assigned_port}.",
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
async def handle_terminate_instance(request: web.Request) -> web.Response:
|
|
366
|
+
"""DELETE /api/v1/instances/{id} - Terminate instance."""
|
|
367
|
+
worker: WorkerServer = request.app["worker"]
|
|
368
|
+
instance_id = request.match_info["id"]
|
|
369
|
+
keep_env = request.query.get("keep_env", "false").lower() == "true"
|
|
370
|
+
|
|
371
|
+
instance = worker.state.instances.get(instance_id)
|
|
372
|
+
if not instance:
|
|
373
|
+
return web.json_response({"error": "Instance not found"}, status=404)
|
|
374
|
+
|
|
375
|
+
# Terminate based on mode
|
|
376
|
+
if instance.mode == "native":
|
|
377
|
+
worker.native_manager.terminate(instance_id, pid=instance.pid)
|
|
378
|
+
if not keep_env:
|
|
379
|
+
worker.native_manager.delete_environment(instance.environment_name)
|
|
380
|
+
# Docker mode would go here
|
|
381
|
+
|
|
382
|
+
# Release port and remove from state
|
|
383
|
+
worker.port_allocator.release(instance_id)
|
|
384
|
+
worker.state.remove_instance(instance_id)
|
|
385
|
+
worker.state.save()
|
|
386
|
+
|
|
387
|
+
msg = f"Instance terminated. Port {instance.assigned_port} released."
|
|
388
|
+
if not keep_env:
|
|
389
|
+
msg += f" Environment '{instance.environment_name}' deleted."
|
|
390
|
+
|
|
391
|
+
return web.json_response({
|
|
392
|
+
"id": instance_id,
|
|
393
|
+
"status": "terminated",
|
|
394
|
+
"message": msg,
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
async def handle_logs(request: web.Request) -> web.Response | web.WebSocketResponse:
|
|
399
|
+
"""Handle /api/v1/instances/{id}/logs - GET for fetch, WebSocket for streaming."""
|
|
400
|
+
# Check if this is a WebSocket upgrade request
|
|
401
|
+
if request.headers.get("Upgrade", "").lower() == "websocket":
|
|
402
|
+
return await _handle_logs_websocket(request)
|
|
403
|
+
|
|
404
|
+
# Regular HTTP GET request
|
|
405
|
+
worker: WorkerServer = request.app["worker"]
|
|
406
|
+
instance_id = request.match_info["id"]
|
|
407
|
+
|
|
408
|
+
instance = worker.state.instances.get(instance_id)
|
|
409
|
+
if not instance:
|
|
410
|
+
return web.json_response({"error": "Instance not found"}, status=404)
|
|
411
|
+
|
|
412
|
+
lines = int(request.query.get("lines", "100"))
|
|
413
|
+
|
|
414
|
+
if instance.mode == "native":
|
|
415
|
+
process_logs = worker.native_manager.get_logs(instance_id, lines=lines)
|
|
416
|
+
logs = [{"level": "INFO", "message": line} for line in process_logs.stdout]
|
|
417
|
+
else:
|
|
418
|
+
logs = []
|
|
419
|
+
|
|
420
|
+
return web.json_response({"logs": logs})
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
async def _handle_logs_websocket(request: web.Request) -> web.WebSocketResponse:
|
|
424
|
+
"""WebSocket /api/v1/instances/{id}/logs - Stream instance logs."""
|
|
425
|
+
worker: WorkerServer = request.app["worker"]
|
|
426
|
+
instance_id = request.match_info["id"]
|
|
427
|
+
|
|
428
|
+
instance = worker.state.instances.get(instance_id)
|
|
429
|
+
if not instance:
|
|
430
|
+
raise web.HTTPNotFound(text="Instance not found")
|
|
431
|
+
|
|
432
|
+
ws = web.WebSocketResponse()
|
|
433
|
+
await ws.prepare(request)
|
|
434
|
+
|
|
435
|
+
# Stream logs (no initial connection message - tests expect first message to be log type)
|
|
436
|
+
last_index = 0
|
|
437
|
+
try:
|
|
438
|
+
while not ws.closed:
|
|
439
|
+
if instance.mode == "native":
|
|
440
|
+
buf = worker.native_manager._log_buffers.get(instance_id, [])
|
|
441
|
+
# Send new lines since last check
|
|
442
|
+
if len(buf) > last_index:
|
|
443
|
+
for line in buf[last_index:]:
|
|
444
|
+
await ws.send_json({
|
|
445
|
+
"type": "log",
|
|
446
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
447
|
+
"level": "INFO",
|
|
448
|
+
"message": line,
|
|
449
|
+
})
|
|
450
|
+
last_index = len(buf)
|
|
451
|
+
|
|
452
|
+
await asyncio.sleep(0.5)
|
|
453
|
+
except asyncio.CancelledError:
|
|
454
|
+
# Server shutdown - close websocket gracefully
|
|
455
|
+
await ws.close()
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
finally:
|
|
459
|
+
if not ws.closed:
|
|
460
|
+
await ws.close()
|
|
461
|
+
|
|
462
|
+
return ws
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def create_worker_app(
|
|
466
|
+
api_key: str,
|
|
467
|
+
workspace_path: Path,
|
|
468
|
+
default_mode: str = "docker",
|
|
469
|
+
port_range_start: int = 8200,
|
|
470
|
+
port_range_end: int = 8210,
|
|
471
|
+
state_dir: Path | None = None,
|
|
472
|
+
) -> web.Application:
|
|
473
|
+
"""Create aiohttp application for worker server.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
api_key: API key for authentication
|
|
477
|
+
workspace_path: ComfyGit workspace path
|
|
478
|
+
default_mode: Default instance mode
|
|
479
|
+
port_range_start: First port for instances
|
|
480
|
+
port_range_end: Last port for instances
|
|
481
|
+
state_dir: Directory for state files
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Configured aiohttp Application
|
|
485
|
+
"""
|
|
486
|
+
app = web.Application(middlewares=[auth_middleware])
|
|
487
|
+
|
|
488
|
+
# Create worker server instance
|
|
489
|
+
worker = WorkerServer(
|
|
490
|
+
api_key=api_key,
|
|
491
|
+
workspace_path=workspace_path,
|
|
492
|
+
default_mode=default_mode,
|
|
493
|
+
port_range_start=port_range_start,
|
|
494
|
+
port_range_end=port_range_end,
|
|
495
|
+
state_dir=state_dir,
|
|
496
|
+
)
|
|
497
|
+
app["worker"] = worker
|
|
498
|
+
|
|
499
|
+
# Register routes
|
|
500
|
+
app.router.add_get("/api/v1/health", handle_health)
|
|
501
|
+
app.router.add_get("/api/v1/system/info", handle_system_info)
|
|
502
|
+
app.router.add_get("/api/v1/instances", handle_list_instances)
|
|
503
|
+
app.router.add_post("/api/v1/instances", handle_create_instance)
|
|
504
|
+
app.router.add_get("/api/v1/instances/{id}", handle_get_instance)
|
|
505
|
+
app.router.add_post("/api/v1/instances/{id}/stop", handle_stop_instance)
|
|
506
|
+
app.router.add_post("/api/v1/instances/{id}/start", handle_start_instance)
|
|
507
|
+
app.router.add_delete("/api/v1/instances/{id}", handle_terminate_instance)
|
|
508
|
+
# Combined handler for both HTTP GET and WebSocket upgrade
|
|
509
|
+
app.router.add_get("/api/v1/instances/{id}/logs", handle_logs)
|
|
510
|
+
|
|
511
|
+
return app
|