golem-vm-provider 0.1.57__py3-none-any.whl → 0.1.58__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.
- {golem_vm_provider-0.1.57.dist-info → golem_vm_provider-0.1.58.dist-info}/METADATA +14 -1
- {golem_vm_provider-0.1.57.dist-info → golem_vm_provider-0.1.58.dist-info}/RECORD +8 -8
- provider/api/routes.py +87 -1
- provider/main.py +253 -5
- provider/utils/pricing.py +2 -1
- provider/vm/multipass_adapter.py +30 -7
- {golem_vm_provider-0.1.57.dist-info → golem_vm_provider-0.1.58.dist-info}/WHEEL +0 -0
- {golem_vm_provider-0.1.57.dist-info → golem_vm_provider-0.1.58.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: golem-vm-provider
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.58
|
4
4
|
Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
|
5
5
|
Keywords: golem,vm,provider,cloud,decentralized
|
6
6
|
Author: Phillip Jensen
|
@@ -470,6 +470,19 @@ golem-provider start
|
|
470
470
|
GOLEM_PROVIDER_ENVIRONMENT=development golem-provider start --network testnet
|
471
471
|
```
|
472
472
|
|
473
|
+
Run as a background service (no terminal):
|
474
|
+
|
475
|
+
```bash
|
476
|
+
# Start in background and write a PID file
|
477
|
+
golem-provider start --daemon [--network testnet|mainnet]
|
478
|
+
|
479
|
+
# Stop the background process
|
480
|
+
golem-provider stop
|
481
|
+
|
482
|
+
# Check environment and port health (unchanged)
|
483
|
+
golem-provider status [--json]
|
484
|
+
```
|
485
|
+
|
473
486
|
### Mode vs. Network
|
474
487
|
|
475
488
|
- Development Mode (`GOLEM_PROVIDER_ENVIRONMENT=development`)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
provider/__init__.py,sha256=HO1fkPpZqPO3z8O8-eVIyx8xXSMIVuTR_b1YF0RtXOg,45
|
2
2
|
provider/api/__init__.py,sha256=ssX1ugDqEPt8Fn04IymgmG-Ev8PiXLsCSaiZVvHQnec,344
|
3
3
|
provider/api/models.py,sha256=CmfgXqSH3m0HLqY6JvUFI-2IrdGf3EhNKtZ5kbIAX-U,4304
|
4
|
-
provider/api/routes.py,sha256=
|
4
|
+
provider/api/routes.py,sha256=E3AxzzgqR_UgLy_I8YvtSkhy_GQlIW5EzF5FmYjF90k,17548
|
5
5
|
provider/config.py,sha256=nQzYBujgn-Z7Rqh6q0eOsTpk6R9-V-YF1OysmPpSH0Q,28993
|
6
6
|
provider/container.py,sha256=xN1a9qClciGomppCBnEGuPPNzGQkYIWlw1lzexrjptM,3726
|
7
7
|
provider/data/deployments/l2.json,sha256=XTNN2C5LkBfp4YbDKdUKfWMdp1fKnfv8D3TgcwVWxtQ,249
|
@@ -13,7 +13,7 @@ provider/discovery/multi_advertiser.py,sha256=_J79wA1-XQ4GsLzt9KrKpWigGSGBqtut7D
|
|
13
13
|
provider/discovery/resource_monitor.py,sha256=AmiEc7yBGEGXCunQ-QKmVgosDX3gOhK1Y58LJZXrwAs,949
|
14
14
|
provider/discovery/resource_tracker.py,sha256=MP7IXd3aIMsjB4xz5Oj9zFDTEnvrnw-Cyxpl33xcJcc,6006
|
15
15
|
provider/discovery/service.py,sha256=vX_mVSxvn3arnb2cKDM_SeJp1ZgPdImP2aUubeXgdRg,915
|
16
|
-
provider/main.py,sha256=
|
16
|
+
provider/main.py,sha256=Iho5lJEgGCYb1BNHqMR8IJCCRJ2qGdeUbSwyF49hmRw,64487
|
17
17
|
provider/network/port_verifier.py,sha256=mlSzr9Z-W5Z5mL3EYg4zemgGoi8Z5ebNoeFgLGRaoH4,13253
|
18
18
|
provider/payments/blockchain_service.py,sha256=4GrzDKwCSUVoENqjD4RLyJ0qwBOJKMyVk5Li-XNsyTc,3567
|
19
19
|
provider/payments/monitor.py,sha256=seo8vE622IdbcRE3x69IpvHn2mel_tlMNGt_DxOIoww,5386
|
@@ -26,20 +26,20 @@ provider/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
|
27
27
|
provider/utils/logging.py,sha256=1Br806ohJyYpDIw1i8NsNpg8Xc-8-rUYwKBU4LFomLk,2623
|
28
28
|
provider/utils/port_display.py,sha256=u1HWQFA2kPbsM-TnsQfL6Hr4KmjIZWZfsjoxarHpbW0,11981
|
29
|
-
provider/utils/pricing.py,sha256=
|
29
|
+
provider/utils/pricing.py,sha256=YeIeacjhb4wD0PucIKwimTy565N-8S6KJdmwhksXPtU,6716
|
30
30
|
provider/utils/retry.py,sha256=GvBjpr0DpTOgw28M2hI0yt17dpYLRwrxUUqVxWHQPtM,3148
|
31
31
|
provider/utils/setup.py,sha256=Z5dLuBQkb5vdoQsu1HJZwXmu9NWsiBYJ7Vq9-C-_tY8,2932
|
32
32
|
provider/vm/__init__.py,sha256=LJL504QGbqZvBbMN3G9ixMgAwvOWAKW37zUm_EiaW9M,508
|
33
33
|
provider/vm/cloud_init.py,sha256=E5dDH7dqStRcJNDfbarBBe83-c9N63W8B5ycIrHI8eU,4627
|
34
34
|
provider/vm/models.py,sha256=hNeXgOnXWyeSiYt07Pdks0B20cDi_VC8jV-tCxULNng,6350
|
35
35
|
provider/vm/multipass.py,sha256=rjO3GtuS4O_wXyYXSUiDGWYtQV2LpGxm6kITrA-ghBQ,617
|
36
|
-
provider/vm/multipass_adapter.py,sha256=
|
36
|
+
provider/vm/multipass_adapter.py,sha256=HW4_7cs3O_SCi1unfIvhC44duO561bpO6yrHGkqhLQA,11578
|
37
37
|
provider/vm/name_mapper.py,sha256=14nKfCjJ1WkXfC4vnCYIxNGQUwcl2vcxrJYUAz4fL40,4073
|
38
38
|
provider/vm/port_manager.py,sha256=iYSwjTjD_ziOhG8aI7juKHw1OwwRUTJQyQoRUNQvz9w,12514
|
39
39
|
provider/vm/provider.py,sha256=A7QN89EJjcSS40_SmKeinG1Jp_NGffJaLse-XdKciAs,1164
|
40
40
|
provider/vm/proxy_manager.py,sha256=n4NTsyz2rtrvjtf_ceKBk-g2q_mzqPwruB1q7UlQVBc,14928
|
41
41
|
provider/vm/service.py,sha256=Ki4SGNIZUq3XmaPMwAOoNzdZzKQsmFXid374wgjFPes,4636
|
42
|
-
golem_vm_provider-0.1.
|
43
|
-
golem_vm_provider-0.1.
|
44
|
-
golem_vm_provider-0.1.
|
45
|
-
golem_vm_provider-0.1.
|
42
|
+
golem_vm_provider-0.1.58.dist-info/METADATA,sha256=Hb6wIjErVQNKcOJva7xXtt-wGT6CIK6t5GYzVh-TVUc,21221
|
43
|
+
golem_vm_provider-0.1.58.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
44
|
+
golem_vm_provider-0.1.58.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
|
45
|
+
golem_vm_provider-0.1.58.dist-info/RECORD,,
|
provider/api/routes.py
CHANGED
@@ -12,7 +12,13 @@ from ..container import Container
|
|
12
12
|
from ..utils.logging import setup_logger
|
13
13
|
from ..utils.ascii_art import vm_creation_animation, vm_status_change
|
14
14
|
from ..vm.models import VMInfo, VMAccessInfo, VMConfig, VMResources, VMNotFoundError
|
15
|
-
from .models import
|
15
|
+
from .models import (
|
16
|
+
CreateVMRequest,
|
17
|
+
ProviderInfoResponse,
|
18
|
+
StreamStatus,
|
19
|
+
StreamOnChain,
|
20
|
+
StreamComputed,
|
21
|
+
)
|
16
22
|
from ..payments.blockchain_service import StreamPaymentReader
|
17
23
|
from ..vm.service import VMService
|
18
24
|
from ..vm.multipass_adapter import MultipassError
|
@@ -299,3 +305,83 @@ async def list_stream_statuses(
|
|
299
305
|
logger.warning(f"stream {stream_id} lookup failed: {e}")
|
300
306
|
continue
|
301
307
|
return resp
|
308
|
+
|
309
|
+
|
310
|
+
# --- GUI support endpoints ---
|
311
|
+
@router.get("/summary")
|
312
|
+
@inject
|
313
|
+
async def provider_summary(
|
314
|
+
vm_service: VMService = Depends(Provide[Container.vm_service]),
|
315
|
+
settings: Any = Depends(Provide[Container.config]),
|
316
|
+
container: Container = Depends(Provide[Container]),
|
317
|
+
):
|
318
|
+
"""Concise provider summary for GUI: status, resources, pricing, VMs."""
|
319
|
+
try:
|
320
|
+
# Resources
|
321
|
+
rt = container.resource_tracker()
|
322
|
+
total = getattr(rt, "total_resources", {})
|
323
|
+
available = rt.get_available_resources() if hasattr(rt, "get_available_resources") else {}
|
324
|
+
|
325
|
+
# Pricing (both USD and GLM per month per unit)
|
326
|
+
pricing = {
|
327
|
+
"usd_per_core_month": float(settings["PRICE_USD_PER_CORE_MONTH"]) if isinstance(settings, dict) else float(getattr(settings, "PRICE_USD_PER_CORE_MONTH", 0)),
|
328
|
+
"usd_per_gb_ram_month": float(settings["PRICE_USD_PER_GB_RAM_MONTH"]) if isinstance(settings, dict) else float(getattr(settings, "PRICE_USD_PER_GB_RAM_MONTH", 0)),
|
329
|
+
"usd_per_gb_storage_month": float(settings["PRICE_USD_PER_GB_STORAGE_MONTH"]) if isinstance(settings, dict) else float(getattr(settings, "PRICE_USD_PER_GB_STORAGE_MONTH", 0)),
|
330
|
+
"glm_per_core_month": float(settings["PRICE_GLM_PER_CORE_MONTH"]) if isinstance(settings, dict) else float(getattr(settings, "PRICE_GLM_PER_CORE_MONTH", 0)),
|
331
|
+
"glm_per_gb_ram_month": float(settings["PRICE_GLM_PER_GB_RAM_MONTH"]) if isinstance(settings, dict) else float(getattr(settings, "PRICE_GLM_PER_GB_RAM_MONTH", 0)),
|
332
|
+
"glm_per_gb_storage_month": float(settings["PRICE_GLM_PER_GB_STORAGE_MONTH"]) if isinstance(settings, dict) else float(getattr(settings, "PRICE_GLM_PER_GB_STORAGE_MONTH", 0)),
|
333
|
+
}
|
334
|
+
|
335
|
+
# VMs
|
336
|
+
vms = []
|
337
|
+
try:
|
338
|
+
items = await vm_service.list_vms()
|
339
|
+
for vm in items:
|
340
|
+
vms.append({
|
341
|
+
"id": vm.id,
|
342
|
+
"status": vm.status.value if hasattr(vm, "status") else str(getattr(vm, "status", "")),
|
343
|
+
"ssh_port": getattr(vm, "ssh_port", None),
|
344
|
+
"resources": {
|
345
|
+
"cpu": getattr(getattr(vm, "resources", None), "cpu", None),
|
346
|
+
"memory": getattr(getattr(vm, "resources", None), "memory", None),
|
347
|
+
"storage": getattr(getattr(vm, "resources", None), "storage", None),
|
348
|
+
},
|
349
|
+
})
|
350
|
+
except Exception:
|
351
|
+
vms = []
|
352
|
+
|
353
|
+
# Basic environment info
|
354
|
+
env = {
|
355
|
+
"environment": settings["ENVIRONMENT"] if isinstance(settings, dict) else getattr(settings, "ENVIRONMENT", None),
|
356
|
+
"network": settings.get("NETWORK") if isinstance(settings, dict) else getattr(settings, "NETWORK", None),
|
357
|
+
}
|
358
|
+
|
359
|
+
return {
|
360
|
+
"status": "running",
|
361
|
+
"resources": {"total": total, "available": available},
|
362
|
+
"pricing": pricing,
|
363
|
+
"vms": vms,
|
364
|
+
"env": env,
|
365
|
+
}
|
366
|
+
except Exception as e:
|
367
|
+
logger.error(f"summary endpoint failed: {e}")
|
368
|
+
raise HTTPException(status_code=500, detail="failed to collect summary")
|
369
|
+
|
370
|
+
|
371
|
+
@router.post("/admin/shutdown")
|
372
|
+
async def admin_shutdown():
|
373
|
+
"""Schedule a graceful provider shutdown. Returns immediately."""
|
374
|
+
try:
|
375
|
+
import asyncio, os, signal
|
376
|
+
loop = asyncio.get_running_loop()
|
377
|
+
# Try to signal our own process for a clean exit shortly after responding
|
378
|
+
def _sig():
|
379
|
+
try:
|
380
|
+
os.kill(os.getpid(), signal.SIGTERM)
|
381
|
+
except Exception:
|
382
|
+
os._exit(0) # last resort
|
383
|
+
loop.call_later(0.2, _sig)
|
384
|
+
return {"ok": True}
|
385
|
+
except Exception as e:
|
386
|
+
logger.error(f"shutdown scheduling failed: {e}")
|
387
|
+
raise HTTPException(status_code=500, detail="failed to schedule shutdown")
|
provider/main.py
CHANGED
@@ -137,6 +137,11 @@ async def verify_provider_port(port: int) -> bool:
|
|
137
137
|
|
138
138
|
|
139
139
|
import typer
|
140
|
+
import platform as _platform
|
141
|
+
import signal as _signal
|
142
|
+
import time as _time
|
143
|
+
import shutil as _shutil
|
144
|
+
import psutil
|
140
145
|
try:
|
141
146
|
from importlib import metadata
|
142
147
|
except ImportError:
|
@@ -171,6 +176,91 @@ def _get_latest_version_from_pypi(pkg_name: str) -> Optional[str]:
|
|
171
176
|
# Avoid network in pytest runs
|
172
177
|
if os.environ.get("PYTEST_CURRENT_TEST"):
|
173
178
|
return None
|
179
|
+
|
180
|
+
|
181
|
+
# ---------------------------
|
182
|
+
# Daemon/PID file management
|
183
|
+
# ---------------------------
|
184
|
+
|
185
|
+
def _pid_dir() -> str:
|
186
|
+
from pathlib import Path
|
187
|
+
plat = _platform.system().lower()
|
188
|
+
if plat.startswith("darwin"):
|
189
|
+
base = Path.home() / "Library" / "Application Support" / "Golem Provider"
|
190
|
+
elif plat.startswith("windows"):
|
191
|
+
base = Path(os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming"))) / "Golem Provider"
|
192
|
+
else:
|
193
|
+
base = Path(os.environ.get("XDG_STATE_HOME", str(Path.home() / ".local" / "state"))) / "golem-provider"
|
194
|
+
base.mkdir(parents=True, exist_ok=True)
|
195
|
+
return str(base)
|
196
|
+
|
197
|
+
|
198
|
+
def _pid_path() -> str:
|
199
|
+
from pathlib import Path
|
200
|
+
return str(Path(_pid_dir()) / "provider.pid")
|
201
|
+
|
202
|
+
|
203
|
+
def _write_pid(pid: int) -> None:
|
204
|
+
with open(_pid_path(), "w") as fh:
|
205
|
+
fh.write(str(pid))
|
206
|
+
|
207
|
+
|
208
|
+
def _read_pid() -> int | None:
|
209
|
+
try:
|
210
|
+
with open(_pid_path(), "r") as fh:
|
211
|
+
c = fh.read().strip()
|
212
|
+
return int(c)
|
213
|
+
except Exception:
|
214
|
+
return None
|
215
|
+
|
216
|
+
|
217
|
+
def _remove_pid_file() -> None:
|
218
|
+
try:
|
219
|
+
os.remove(_pid_path())
|
220
|
+
except Exception:
|
221
|
+
pass
|
222
|
+
|
223
|
+
|
224
|
+
def _is_running(pid: int) -> bool:
|
225
|
+
try:
|
226
|
+
return psutil.pid_exists(pid) and psutil.Process(pid).is_running()
|
227
|
+
except Exception:
|
228
|
+
return False
|
229
|
+
|
230
|
+
|
231
|
+
def _spawn_detached(argv: list[str], env: dict | None = None) -> int:
|
232
|
+
import subprocess
|
233
|
+
popen_kwargs = {
|
234
|
+
"stdin": subprocess.DEVNULL,
|
235
|
+
"stdout": subprocess.DEVNULL,
|
236
|
+
"stderr": subprocess.DEVNULL,
|
237
|
+
"env": env or os.environ.copy(),
|
238
|
+
}
|
239
|
+
if _platform.system().lower().startswith("windows"):
|
240
|
+
creationflags = 0
|
241
|
+
for flag in ("CREATE_NEW_PROCESS_GROUP", "DETACHED_PROCESS"):
|
242
|
+
v = getattr(subprocess, flag, 0)
|
243
|
+
if v:
|
244
|
+
creationflags |= v
|
245
|
+
if creationflags:
|
246
|
+
popen_kwargs["creationflags"] = creationflags # type: ignore[assignment]
|
247
|
+
else:
|
248
|
+
popen_kwargs["preexec_fn"] = os.setsid # type: ignore[assignment]
|
249
|
+
proc = subprocess.Popen(argv, **popen_kwargs)
|
250
|
+
return int(proc.pid)
|
251
|
+
|
252
|
+
|
253
|
+
def _self_command(base_args: list[str]) -> list[str]:
|
254
|
+
import sys
|
255
|
+
# When frozen (PyInstaller), sys.executable is the CLI binary
|
256
|
+
if getattr(sys, "frozen", False):
|
257
|
+
return [sys.executable] + base_args
|
258
|
+
# Prefer the console_script when available
|
259
|
+
exe = _shutil.which("golem-provider")
|
260
|
+
if exe:
|
261
|
+
return [exe] + base_args
|
262
|
+
# Fallback to module execution
|
263
|
+
return [sys.executable, "-m", "provider.main"] + base_args
|
174
264
|
try:
|
175
265
|
import json as _json
|
176
266
|
from urllib.request import urlopen
|
@@ -1149,10 +1239,77 @@ def streams_withdraw(
|
|
1149
1239
|
@cli.command()
|
1150
1240
|
def start(
|
1151
1241
|
no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification."),
|
1152
|
-
network: str = typer.Option(None, "--network", help="Target network: 'testnet' or 'mainnet' (overrides env)")
|
1242
|
+
network: str = typer.Option(None, "--network", help="Target network: 'testnet' or 'mainnet' (overrides env)"),
|
1243
|
+
gui: Optional[bool] = typer.Option(None, "--gui/--no-gui", help="Auto-launch Electron GUI (default: auto)"),
|
1244
|
+
daemon: bool = typer.Option(False, "--daemon", help="Start in background and write a PID file"),
|
1153
1245
|
):
|
1154
1246
|
"""Start the provider server."""
|
1155
|
-
|
1247
|
+
if daemon:
|
1248
|
+
# If a previous daemon is active, do not start another
|
1249
|
+
pid = _read_pid()
|
1250
|
+
if pid and _is_running(pid):
|
1251
|
+
print(f"Provider already running (pid={pid})")
|
1252
|
+
raise typer.Exit(code=0)
|
1253
|
+
# Build child command and detach
|
1254
|
+
args = ["start"]
|
1255
|
+
if no_verify_port:
|
1256
|
+
args.append("--no-verify-port")
|
1257
|
+
if network:
|
1258
|
+
args += ["--network", network]
|
1259
|
+
# Force no GUI for daemonized child to avoid duplicates
|
1260
|
+
args.append("--no-gui")
|
1261
|
+
cmd = _self_command(args)
|
1262
|
+
# Ensure GUI not auto-launched via env, regardless of defaults
|
1263
|
+
env = {**os.environ, "GOLEM_PROVIDER_LAUNCH_GUI": "0"}
|
1264
|
+
child_pid = _spawn_detached(cmd, env)
|
1265
|
+
_write_pid(child_pid)
|
1266
|
+
print(f"Started provider in background (pid={child_pid})")
|
1267
|
+
raise typer.Exit(code=0)
|
1268
|
+
else:
|
1269
|
+
run_server(dev_mode=False, no_verify_port=no_verify_port, network=network, launch_gui=gui)
|
1270
|
+
|
1271
|
+
|
1272
|
+
@cli.command()
|
1273
|
+
def stop(timeout: int = typer.Option(15, "--timeout", help="Seconds to wait for graceful shutdown")):
|
1274
|
+
"""Stop a background provider started with --daemon."""
|
1275
|
+
pid = _read_pid()
|
1276
|
+
if not pid:
|
1277
|
+
print("No PID file found; nothing to stop")
|
1278
|
+
raise typer.Exit(code=0)
|
1279
|
+
if not _is_running(pid):
|
1280
|
+
print("No running provider process; cleaning up PID file")
|
1281
|
+
_remove_pid_file()
|
1282
|
+
raise typer.Exit(code=0)
|
1283
|
+
try:
|
1284
|
+
p = psutil.Process(pid)
|
1285
|
+
p.terminate()
|
1286
|
+
except Exception:
|
1287
|
+
# Fallback to signal/kill
|
1288
|
+
try:
|
1289
|
+
if _platform.system().lower().startswith("windows"):
|
1290
|
+
os.system(f"taskkill /PID {pid} /T /F >NUL 2>&1")
|
1291
|
+
else:
|
1292
|
+
os.kill(pid, _signal.SIGTERM)
|
1293
|
+
except Exception:
|
1294
|
+
pass
|
1295
|
+
# Wait for exit
|
1296
|
+
start_ts = _time.time()
|
1297
|
+
while _time.time() - start_ts < max(0, int(timeout)):
|
1298
|
+
if not _is_running(pid):
|
1299
|
+
break
|
1300
|
+
_time.sleep(0.2)
|
1301
|
+
if _is_running(pid):
|
1302
|
+
print("Process did not exit in time; sending kill")
|
1303
|
+
try:
|
1304
|
+
psutil.Process(pid).kill()
|
1305
|
+
except Exception:
|
1306
|
+
try:
|
1307
|
+
if not _platform.system().lower().startswith("windows"):
|
1308
|
+
os.kill(pid, _signal.SIGKILL)
|
1309
|
+
except Exception:
|
1310
|
+
pass
|
1311
|
+
_remove_pid_file()
|
1312
|
+
print("Provider stopped")
|
1156
1313
|
|
1157
1314
|
# Removed separate 'dev' command; use environment GOLEM_PROVIDER_ENVIRONMENT=development instead.
|
1158
1315
|
|
@@ -1288,7 +1445,83 @@ def _print_pricing_examples(glm_usd):
|
|
1288
1445
|
f"- {name} ({res.cpu}C, {res.memory}GB RAM, {res.storage}GB Disk): ~{usd_str} per month (~{glm_str})"
|
1289
1446
|
)
|
1290
1447
|
|
1291
|
-
def
|
1448
|
+
def _can_launch_gui() -> bool:
|
1449
|
+
import shutil
|
1450
|
+
plat = _sys.platform
|
1451
|
+
# Basic headless checks
|
1452
|
+
if plat.startswith("linux"):
|
1453
|
+
if not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
|
1454
|
+
return False
|
1455
|
+
# Require npm (or electron) available
|
1456
|
+
return bool(shutil.which("npm") or shutil.which("electron"))
|
1457
|
+
|
1458
|
+
|
1459
|
+
def _maybe_launch_gui(port: int):
|
1460
|
+
import subprocess, shutil
|
1461
|
+
import os as _os
|
1462
|
+
from pathlib import Path
|
1463
|
+
root = Path(__file__).parent.parent.parent
|
1464
|
+
gui_dir = root / "provider-gui"
|
1465
|
+
if not gui_dir.exists():
|
1466
|
+
logger.info("GUI directory not found; running headless")
|
1467
|
+
return
|
1468
|
+
cmd = None
|
1469
|
+
npm = shutil.which("npm")
|
1470
|
+
electron_bin = gui_dir / "node_modules" / "electron" / "dist" / ("electron.exe" if _sys.platform.startswith("win") else "electron")
|
1471
|
+
try:
|
1472
|
+
# Ensure dependencies (electron) are present
|
1473
|
+
if npm and not electron_bin.exists():
|
1474
|
+
install_cmd = [npm, "ci", "--silent"] if (gui_dir / "package-lock.json").exists() else [npm, "install", "--silent"]
|
1475
|
+
logger.info("Installing Provider GUI dependencies…")
|
1476
|
+
subprocess.run(install_cmd, cwd=str(gui_dir), env=os.environ, check=True)
|
1477
|
+
except Exception as e:
|
1478
|
+
logger.warning(f"GUI dependencies install failed: {e}")
|
1479
|
+
|
1480
|
+
if npm:
|
1481
|
+
cmd = [npm, "start", "--silent"]
|
1482
|
+
elif shutil.which("electron"):
|
1483
|
+
cmd = ["electron", "."]
|
1484
|
+
else:
|
1485
|
+
logger.info("No npm/electron found; skipping GUI")
|
1486
|
+
return
|
1487
|
+
env = {**os.environ, "PROVIDER_API_URL": f"http://127.0.0.1:{port}/api/v1"}
|
1488
|
+
try:
|
1489
|
+
# Detach GUI so it won't receive terminal signals (e.g., Ctrl+C) or
|
1490
|
+
# be terminated when the provider process exits.
|
1491
|
+
popen_kwargs = {
|
1492
|
+
"cwd": str(gui_dir),
|
1493
|
+
"env": env,
|
1494
|
+
"stdin": subprocess.DEVNULL,
|
1495
|
+
"stdout": subprocess.DEVNULL,
|
1496
|
+
"stderr": subprocess.DEVNULL,
|
1497
|
+
}
|
1498
|
+
if _sys.platform.startswith("win"):
|
1499
|
+
# Create a new process group and detach from console on Windows
|
1500
|
+
creationflags = 0
|
1501
|
+
try:
|
1502
|
+
creationflags |= getattr(subprocess, "CREATE_NEW_PROCESS_GROUP")
|
1503
|
+
except Exception:
|
1504
|
+
pass
|
1505
|
+
try:
|
1506
|
+
creationflags |= getattr(subprocess, "DETACHED_PROCESS")
|
1507
|
+
except Exception:
|
1508
|
+
pass
|
1509
|
+
if creationflags:
|
1510
|
+
popen_kwargs["creationflags"] = creationflags # type: ignore[assignment]
|
1511
|
+
else:
|
1512
|
+
# Start a new session/process group on POSIX
|
1513
|
+
try:
|
1514
|
+
popen_kwargs["preexec_fn"] = _os.setsid # type: ignore[assignment]
|
1515
|
+
except Exception:
|
1516
|
+
pass
|
1517
|
+
|
1518
|
+
subprocess.Popen(cmd, **popen_kwargs)
|
1519
|
+
logger.info("Launched Provider GUI")
|
1520
|
+
except Exception as e:
|
1521
|
+
logger.warning(f"Failed to launch GUI: {e}")
|
1522
|
+
|
1523
|
+
|
1524
|
+
def run_server(dev_mode: bool | None = None, no_verify_port: bool = False, network: str | None = None, launch_gui: Optional[bool] = None):
|
1292
1525
|
"""Helper to run the uvicorn server."""
|
1293
1526
|
import sys
|
1294
1527
|
from pathlib import Path
|
@@ -1344,9 +1577,24 @@ def run_server(dev_mode: bool | None = None, no_verify_port: bool = False, netwo
|
|
1344
1577
|
log_config = uvicorn.config.LOGGING_CONFIG
|
1345
1578
|
log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
1346
1579
|
|
1580
|
+
# Optionally launch GUI (non-blocking)
|
1581
|
+
want_gui: bool
|
1582
|
+
if launch_gui is None:
|
1583
|
+
env_flag = os.environ.get("GOLEM_PROVIDER_LAUNCH_GUI")
|
1584
|
+
if env_flag is not None:
|
1585
|
+
want_gui = env_flag.strip().lower() in ("1", "true", "yes")
|
1586
|
+
else:
|
1587
|
+
want_gui = _can_launch_gui()
|
1588
|
+
else:
|
1589
|
+
want_gui = bool(launch_gui)
|
1590
|
+
if want_gui:
|
1591
|
+
try:
|
1592
|
+
_maybe_launch_gui(int(settings.PORT))
|
1593
|
+
except Exception:
|
1594
|
+
logger.warning("GUI launch attempt failed; continuing headless")
|
1595
|
+
|
1347
1596
|
# Run server
|
1348
|
-
logger.process(
|
1349
|
-
f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
|
1597
|
+
logger.process(f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
|
1350
1598
|
uvicorn.run(
|
1351
1599
|
"provider:app",
|
1352
1600
|
host=settings.HOST,
|
provider/utils/pricing.py
CHANGED
@@ -165,7 +165,8 @@ class PricingAutoUpdater:
|
|
165
165
|
update_glm_unit_prices_from_usd(glm_usd)
|
166
166
|
if callable(self._on_updated):
|
167
167
|
# Inform callback which advertising platform is active
|
168
|
-
|
168
|
+
_s = _get_settings()
|
169
|
+
platform = getattr(_s, "ADVERTISER_TYPE", "discovery_server")
|
169
170
|
await self._on_updated(platform=platform, glm_usd=glm_usd)
|
170
171
|
else:
|
171
172
|
logger.warning("Skipping pricing update; failed to fetch GLM price")
|
provider/vm/multipass_adapter.py
CHANGED
@@ -157,13 +157,25 @@ class MultipassAdapter(VMProvider):
|
|
157
157
|
async def list_vms(self) -> List[VMInfo]:
|
158
158
|
"""List all VMs."""
|
159
159
|
all_mappings = self.name_mapper.list_mappings()
|
160
|
-
vms = []
|
161
|
-
for requestor_name in all_mappings.
|
160
|
+
vms: List[VMInfo] = []
|
161
|
+
for requestor_name, multipass_name in list(all_mappings.items()):
|
162
162
|
try:
|
163
|
-
|
163
|
+
# get_vm_status expects multipass_name
|
164
|
+
vm_info = await self.get_vm_status(multipass_name)
|
164
165
|
vms.append(vm_info)
|
165
166
|
except VMNotFoundError:
|
166
|
-
logger.warning(
|
167
|
+
logger.warning(
|
168
|
+
f"VM {requestor_name} not found, but a mapping exists. It may have been deleted externally."
|
169
|
+
)
|
170
|
+
# Cleanup stale mapping and proxy allocation to avoid repeated warnings
|
171
|
+
try:
|
172
|
+
await self.proxy_manager.remove_vm(multipass_name)
|
173
|
+
except Exception:
|
174
|
+
pass
|
175
|
+
try:
|
176
|
+
await self.name_mapper.remove_mapping(requestor_name)
|
177
|
+
except Exception:
|
178
|
+
pass
|
167
179
|
return vms
|
168
180
|
|
169
181
|
async def start_vm(self, multipass_name: str) -> VMInfo:
|
@@ -211,8 +223,8 @@ class MultipassAdapter(VMProvider):
|
|
211
223
|
async def get_all_vms_resources(self) -> Dict[str, VMResources]:
|
212
224
|
"""Get resources for all running VMs."""
|
213
225
|
all_mappings = self.name_mapper.list_mappings()
|
214
|
-
vm_resources = {}
|
215
|
-
for requestor_name, multipass_name in all_mappings.items():
|
226
|
+
vm_resources: Dict[str, VMResources] = {}
|
227
|
+
for requestor_name, multipass_name in list(all_mappings.items()):
|
216
228
|
try:
|
217
229
|
info = await self._get_vm_info(multipass_name)
|
218
230
|
disks_info = info.get("disks", {})
|
@@ -223,7 +235,18 @@ class MultipassAdapter(VMProvider):
|
|
223
235
|
storage=round(total_storage / (1024**3)) if total_storage > 0 else 10
|
224
236
|
)
|
225
237
|
except (MultipassError, VMNotFoundError):
|
226
|
-
logger.warning(
|
238
|
+
logger.warning(
|
239
|
+
f"Could not retrieve resources for VM {requestor_name} ({multipass_name}). It may have been deleted."
|
240
|
+
)
|
241
|
+
# Cleanup stale mapping and proxy allocation
|
242
|
+
try:
|
243
|
+
await self.proxy_manager.remove_vm(multipass_name)
|
244
|
+
except Exception:
|
245
|
+
pass
|
246
|
+
try:
|
247
|
+
await self.name_mapper.remove_mapping(requestor_name)
|
248
|
+
except Exception:
|
249
|
+
pass
|
227
250
|
except Exception as e:
|
228
251
|
logger.error(f"Failed to get info for VM {requestor_name}: {e}")
|
229
252
|
return vm_resources
|
File without changes
|
File without changes
|