golem-vm-provider 0.1.57__tar.gz → 0.1.58__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 (44) hide show
  1. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/PKG-INFO +14 -1
  2. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/README.md +13 -0
  3. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/api/routes.py +87 -1
  4. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/main.py +253 -5
  5. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/utils/pricing.py +2 -1
  6. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/multipass_adapter.py +30 -7
  7. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/pyproject.toml +1 -1
  8. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/__init__.py +0 -0
  9. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/api/__init__.py +0 -0
  10. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/api/models.py +0 -0
  11. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/config.py +0 -0
  12. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/container.py +0 -0
  13. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/data/deployments/l2.json +0 -0
  14. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/discovery/__init__.py +0 -0
  15. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/discovery/advertiser.py +0 -0
  16. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/discovery/golem_base_advertiser.py +0 -0
  17. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/discovery/golem_base_utils.py +0 -0
  18. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/discovery/multi_advertiser.py +0 -0
  19. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/discovery/resource_monitor.py +0 -0
  20. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/discovery/resource_tracker.py +0 -0
  21. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/discovery/service.py +0 -0
  22. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/network/port_verifier.py +0 -0
  23. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/payments/blockchain_service.py +0 -0
  24. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/payments/monitor.py +0 -0
  25. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/payments/stream_map.py +0 -0
  26. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/security/ethereum.py +0 -0
  27. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/security/faucet.py +0 -0
  28. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/security/l2_faucet.py +0 -0
  29. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/service.py +0 -0
  30. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/utils/__init__.py +0 -0
  31. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/utils/ascii_art.py +0 -0
  32. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/utils/logging.py +0 -0
  33. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/utils/port_display.py +0 -0
  34. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/utils/retry.py +0 -0
  35. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/utils/setup.py +0 -0
  36. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/__init__.py +0 -0
  37. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/cloud_init.py +0 -0
  38. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/models.py +0 -0
  39. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/multipass.py +0 -0
  40. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/name_mapper.py +0 -0
  41. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/port_manager.py +0 -0
  42. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/provider.py +0 -0
  43. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/proxy_manager.py +0 -0
  44. {golem_vm_provider-0.1.57 → golem_vm_provider-0.1.58}/provider/vm/service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.57
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`)
@@ -425,6 +425,19 @@ golem-provider start
425
425
  GOLEM_PROVIDER_ENVIRONMENT=development golem-provider start --network testnet
426
426
  ```
427
427
 
428
+ Run as a background service (no terminal):
429
+
430
+ ```bash
431
+ # Start in background and write a PID file
432
+ golem-provider start --daemon [--network testnet|mainnet]
433
+
434
+ # Stop the background process
435
+ golem-provider stop
436
+
437
+ # Check environment and port health (unchanged)
438
+ golem-provider status [--json]
439
+ ```
440
+
428
441
  ### Mode vs. Network
429
442
 
430
443
  - Development Mode (`GOLEM_PROVIDER_ENVIRONMENT=development`)
@@ -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 CreateVMRequest, ProviderInfoResponse, StreamStatus, StreamOnChain, StreamComputed
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")
@@ -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
- run_server(dev_mode=False, no_verify_port=no_verify_port, network=network)
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 run_server(dev_mode: bool | None = None, no_verify_port: bool = False, network: str | None = None):
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,
@@ -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
- platform = getattr(settings, "ADVERTISER_TYPE", "discovery_server")
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")
@@ -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.keys():
160
+ vms: List[VMInfo] = []
161
+ for requestor_name, multipass_name in list(all_mappings.items()):
162
162
  try:
163
- vm_info = await self.get_vm_status(requestor_name)
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(f"VM {requestor_name} not found, but a mapping exists. It may have been deleted externally.")
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(f"Could not retrieve resources for VM {requestor_name} ({multipass_name}). It may have been deleted.")
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "golem-vm-provider"
3
- version = "0.1.57"
3
+ version = "0.1.58"
4
4
  description = "VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network"
5
5
  authors = ["Phillip Jensen <phillip+vm-on-golem@golemgrid.com>"]
6
6
  readme = "README.md"