clan-vm-manager: Fixing vm starting.
This commit is contained in:
parent
4f7f34f9b4
commit
280bee0861
@ -32,7 +32,9 @@ class VMAttr:
|
|||||||
log.debug(f"qmp_socket: {self._qmp_socket}")
|
log.debug(f"qmp_socket: {self._qmp_socket}")
|
||||||
rpath = self._qmp_socket.resolve()
|
rpath = self._qmp_socket.resolve()
|
||||||
if not rpath.exists():
|
if not rpath.exists():
|
||||||
raise ClanError(f"qmp socket {rpath} does not exist. Is the VM running?")
|
raise ClanError(
|
||||||
|
f"qmp socket {rpath} does not exist. Is the VM running?"
|
||||||
|
)
|
||||||
self._qmp = QEMUMonitorProtocol(str(rpath))
|
self._qmp = QEMUMonitorProtocol(str(rpath))
|
||||||
self._qmp.connect()
|
self._qmp.connect()
|
||||||
try:
|
try:
|
||||||
|
@ -37,7 +37,7 @@ def facts_to_nixos_config(facts: dict[str, dict[str, bytes]]) -> dict:
|
|||||||
|
|
||||||
# TODO move this to the Machines class
|
# TODO move this to the Machines class
|
||||||
def build_vm(
|
def build_vm(
|
||||||
machine: Machine, vm: VmConfig, tmpdir: Path, nix_options: list[str]
|
machine: Machine, vm: VmConfig, tmpdir: Path, nix_options: list[str] = []
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
secrets_dir = get_secrets(machine, tmpdir)
|
secrets_dir = get_secrets(machine, tmpdir)
|
||||||
|
|
||||||
|
@ -43,7 +43,9 @@ def wait_vm_up(state_dir: Path) -> None:
|
|||||||
timeout: float = 300
|
timeout: float = 300
|
||||||
while True:
|
while True:
|
||||||
if timeout <= 0:
|
if timeout <= 0:
|
||||||
raise TimeoutError(f"qga socket {socket_file} not found. Is the VM running?")
|
raise TimeoutError(
|
||||||
|
f"qga socket {socket_file} not found. Is the VM running?"
|
||||||
|
)
|
||||||
if socket_file.exists():
|
if socket_file.exists():
|
||||||
break
|
break
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
@ -56,7 +58,9 @@ def wait_vm_down(state_dir: Path) -> None:
|
|||||||
timeout: float = 300
|
timeout: float = 300
|
||||||
while socket_file.exists():
|
while socket_file.exists():
|
||||||
if timeout <= 0:
|
if timeout <= 0:
|
||||||
raise TimeoutError(f"qga socket {socket_file} still exists. Is the VM down?")
|
raise TimeoutError(
|
||||||
|
f"qga socket {socket_file} still exists. Is the VM down?"
|
||||||
|
)
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
timeout -= 0.1
|
timeout -= 0.1
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ from gi.repository import Adw, Gdk, Gio, Gtk
|
|||||||
|
|
||||||
from clan_vm_manager.models.interfaces import ClanConfig
|
from clan_vm_manager.models.interfaces import ClanConfig
|
||||||
from clan_vm_manager.models.use_join import GLib, GObject
|
from clan_vm_manager.models.use_join import GLib, GObject
|
||||||
from clan_vm_manager.models.use_vms import VMS
|
from clan_vm_manager.models.use_vms import VMs
|
||||||
|
|
||||||
from .trayicon import TrayIcon
|
from .trayicon import TrayIcon
|
||||||
from .windows.main_window import MainWindow
|
from .windows.main_window import MainWindow
|
||||||
@ -44,7 +44,8 @@ class MainApplication(Adw.Application):
|
|||||||
"enable debug mode",
|
"enable debug mode",
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
self.vms = VMs.use()
|
||||||
|
log.debug(f"VMS object: {self.vms}")
|
||||||
self.window: Adw.ApplicationWindow | None = None
|
self.window: Adw.ApplicationWindow | None = None
|
||||||
self.connect("shutdown", self.on_shutdown)
|
self.connect("shutdown", self.on_shutdown)
|
||||||
self.connect("activate", self.show_window)
|
self.connect("activate", self.show_window)
|
||||||
@ -69,35 +70,16 @@ class MainApplication(Adw.Application):
|
|||||||
log.debug(f"Join request: {args[1]}")
|
log.debug(f"Join request: {args[1]}")
|
||||||
uri = args[1]
|
uri = args[1]
|
||||||
self.emit("join_request", uri)
|
self.emit("join_request", uri)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_application_icon_path(self) -> None:
|
|
||||||
self.icon_name = "lol.clan.vm.manager"
|
|
||||||
if not self.icon_name:
|
|
||||||
return None
|
|
||||||
|
|
||||||
icon_theme = Gtk.IconTheme.get_for_display(
|
|
||||||
self.get_active_window().get_display()
|
|
||||||
)
|
|
||||||
# Use the correct method to look up an icon
|
|
||||||
icon_lookup_flags = 16
|
|
||||||
icon = icon_theme.lookup_icon(
|
|
||||||
self.icon_name, 128, 1.0, Gtk.TextDirection.NONE, icon_lookup_flags
|
|
||||||
)
|
|
||||||
|
|
||||||
if icon:
|
|
||||||
return icon.get_file().get_path()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def on_shutdown(self, app: Gtk.Application) -> None:
|
def on_shutdown(self, app: Gtk.Application) -> None:
|
||||||
log.debug("Shutting down")
|
log.debug("Shutting down")
|
||||||
|
|
||||||
|
self.vms.kill_all()
|
||||||
|
|
||||||
if self.tray_icon is not None:
|
if self.tray_icon is not None:
|
||||||
self.tray_icon.destroy()
|
self.tray_icon.destroy()
|
||||||
|
|
||||||
VMS.use().kill_all()
|
|
||||||
|
|
||||||
def on_window_hide_unhide(self, *_args: Any) -> None:
|
def on_window_hide_unhide(self, *_args: Any) -> None:
|
||||||
assert self.window is not None
|
assert self.window is not None
|
||||||
if self.window.is_visible():
|
if self.window.is_visible():
|
||||||
|
@ -7,7 +7,6 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
from clan_cli.errors import ClanError
|
|
||||||
|
|
||||||
gi.require_version("GdkPixbuf", "2.0")
|
gi.require_version("GdkPixbuf", "2.0")
|
||||||
|
|
||||||
@ -24,7 +23,7 @@ def _kill_group(proc: mp.Process) -> None:
|
|||||||
if proc.is_alive() and pid:
|
if proc.is_alive() and pid:
|
||||||
os.killpg(pid, signal.SIGTERM)
|
os.killpg(pid, signal.SIGTERM)
|
||||||
else:
|
else:
|
||||||
log.warning(f"Process {proc.name} with pid {pid} is already dead")
|
log.warning(f"Process '{proc.name}' with pid '{pid}' is already dead")
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
@ -102,7 +101,7 @@ def _init_proc(
|
|||||||
|
|
||||||
def spawn(
|
def spawn(
|
||||||
*,
|
*,
|
||||||
log_dir: Path,
|
out_file: Path,
|
||||||
on_except: Callable[[Exception, mp.process.BaseProcess], None] | None,
|
on_except: Callable[[Exception, mp.process.BaseProcess], None] | None,
|
||||||
func: Callable,
|
func: Callable,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
@ -111,13 +110,8 @@ def spawn(
|
|||||||
if mp.get_start_method(allow_none=True) is None:
|
if mp.get_start_method(allow_none=True) is None:
|
||||||
mp.set_start_method(method="forkserver")
|
mp.set_start_method(method="forkserver")
|
||||||
|
|
||||||
if not log_dir.is_dir():
|
|
||||||
raise ClanError(f"Log path {log_dir} is not a directory")
|
|
||||||
log_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Set names
|
# Set names
|
||||||
proc_name = f"MPExec:{func.__name__}"
|
proc_name = f"MPExec:{func.__name__}"
|
||||||
out_file = log_dir / "out.log"
|
|
||||||
|
|
||||||
# Start the process
|
# Start the process
|
||||||
proc = mp.Process(
|
proc = mp.Process(
|
||||||
|
@ -9,7 +9,7 @@ from clan_cli.clan_uri import ClanURI
|
|||||||
from clan_cli.history.add import add_history
|
from clan_cli.history.add import add_history
|
||||||
|
|
||||||
from clan_vm_manager.errors.show_error import show_error_dialog
|
from clan_vm_manager.errors.show_error import show_error_dialog
|
||||||
from clan_vm_manager.models.use_vms import VMS, Clans
|
from clan_vm_manager.models.use_vms import Clans
|
||||||
|
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
gi.require_version("Adw", "1")
|
gi.require_version("Adw", "1")
|
||||||
@ -76,7 +76,7 @@ class Join:
|
|||||||
def after_join(item: JoinValue, _: Any) -> None:
|
def after_join(item: JoinValue, _: Any) -> None:
|
||||||
self.discard(item)
|
self.discard(item)
|
||||||
Clans.use().refresh()
|
Clans.use().refresh()
|
||||||
VMS.use().refresh()
|
# VMS.use().refresh()
|
||||||
print("Refreshed list after join")
|
print("Refreshed list after join")
|
||||||
on_join(item)
|
on_join(item)
|
||||||
|
|
||||||
|
@ -107,6 +107,7 @@ class VM(GObject.Object):
|
|||||||
data: HistoryEntry,
|
data: HistoryEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.KILL_TIMEOUT = 6 # seconds
|
||||||
self.data = data
|
self.data = data
|
||||||
self.process = MPProcess("dummy", mp.Process(), Path("./dummy"))
|
self.process = MPProcess("dummy", mp.Process(), Path("./dummy"))
|
||||||
self._watcher_id: int = 0
|
self._watcher_id: int = 0
|
||||||
@ -121,9 +122,8 @@ class VM(GObject.Object):
|
|||||||
self.log_dir = tempfile.TemporaryDirectory(
|
self.log_dir = tempfile.TemporaryDirectory(
|
||||||
prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}"
|
prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}"
|
||||||
)
|
)
|
||||||
self._finalizer = weakref.finalize(self, self.stop)
|
self._finalizer = weakref.finalize(self, self.kill)
|
||||||
self.connect("build_vm", self.build_vm)
|
self.connect("build_vm", self.build_vm)
|
||||||
|
|
||||||
uri = ClanURI.from_str(
|
uri = ClanURI.from_str(
|
||||||
url=self.data.flake.flake_url, flake_attr=self.data.flake.flake_attr
|
url=self.data.flake.flake_url, flake_attr=self.data.flake.flake_attr
|
||||||
)
|
)
|
||||||
@ -160,25 +160,25 @@ class VM(GObject.Object):
|
|||||||
log.info(f"Starting VM {self.get_id()}")
|
log.info(f"Starting VM {self.get_id()}")
|
||||||
vm = vms.run.inspect_vm(self.machine)
|
vm = vms.run.inspect_vm(self.machine)
|
||||||
|
|
||||||
GLib.idle_add(self.emit, "build_vm", self, True)
|
# GLib.idle_add(self.emit, "build_vm", self, True)
|
||||||
self.process = spawn(
|
# self.process = spawn(
|
||||||
on_except=None,
|
# on_except=None,
|
||||||
log_dir=Path(str(self.log_dir.name)),
|
# log_dir=Path(str(self.log_dir.name)),
|
||||||
func=vms.run.build_vm,
|
# func=vms.run.build_vm,
|
||||||
machine=self.machine,
|
# machine=self.machine,
|
||||||
vm=vm,
|
# vm=vm,
|
||||||
)
|
# )
|
||||||
self.process.proc.join()
|
# self.process.proc.join()
|
||||||
|
|
||||||
GLib.idle_add(self.emit, "build_vm", self, False)
|
# GLib.idle_add(self.emit, "build_vm", self, False)
|
||||||
|
|
||||||
if self.process.proc.exitcode != 0:
|
# if self.process.proc.exitcode != 0:
|
||||||
log.error(f"Failed to build VM {self.get_id()}")
|
# log.error(f"Failed to build VM {self.get_id()}")
|
||||||
return
|
# return
|
||||||
|
|
||||||
self.process = spawn(
|
self.process = spawn(
|
||||||
on_except=None,
|
on_except=None,
|
||||||
log_dir=Path(str(self.log_dir.name)),
|
out_file=Path(str(self.log_dir.name)) / "vm.log",
|
||||||
func=vms.run.run_vm,
|
func=vms.run.run_vm,
|
||||||
vm=vm,
|
vm=vm,
|
||||||
)
|
)
|
||||||
@ -241,7 +241,7 @@ class VM(GObject.Object):
|
|||||||
if self.is_running():
|
if self.is_running():
|
||||||
assert self._stop_timer_init is not None
|
assert self._stop_timer_init is not None
|
||||||
diff = datetime.now() - self._stop_timer_init
|
diff = datetime.now() - self._stop_timer_init
|
||||||
if diff.seconds > 10:
|
if diff.seconds > self.KILL_TIMEOUT:
|
||||||
log.error(f"VM {self.get_id()} has not stopped. Killing it")
|
log.error(f"VM {self.get_id()} has not stopped. Killing it")
|
||||||
self.process.kill_group()
|
self.process.kill_group()
|
||||||
return GLib.SOURCE_CONTINUE
|
return GLib.SOURCE_CONTINUE
|
||||||
@ -263,12 +263,19 @@ class VM(GObject.Object):
|
|||||||
if self._stop_watcher_id == 0:
|
if self._stop_watcher_id == 0:
|
||||||
raise ClanError("Failed to add stop watcher")
|
raise ClanError("Failed to add stop watcher")
|
||||||
|
|
||||||
def stop(self) -> None:
|
def shutdown(self) -> None:
|
||||||
if not self.is_running():
|
if not self.is_running():
|
||||||
return
|
return
|
||||||
log.info(f"Stopping VM {self.get_id()}")
|
log.info(f"Stopping VM {self.get_id()}")
|
||||||
threading.Thread(target=self.__stop).start()
|
threading.Thread(target=self.__stop).start()
|
||||||
|
|
||||||
|
def kill(self) -> None:
|
||||||
|
if not self.is_running():
|
||||||
|
log.warning(f"Tried to kill VM {self.get_id()} is not running")
|
||||||
|
return
|
||||||
|
log.info(f"Killing VM {self.get_id()} now")
|
||||||
|
self.process.kill_group()
|
||||||
|
|
||||||
def read_whole_log(self) -> str:
|
def read_whole_log(self) -> str:
|
||||||
if not self.process.out_file.exists():
|
if not self.process.out_file.exists():
|
||||||
log.error(f"Log file {self.process.out_file} does not exist")
|
log.error(f"Log file {self.process.out_file} does not exist")
|
||||||
@ -276,28 +283,16 @@ class VM(GObject.Object):
|
|||||||
return self.process.out_file.read_text()
|
return self.process.out_file.read_text()
|
||||||
|
|
||||||
|
|
||||||
class VMS:
|
class VMs:
|
||||||
"""
|
|
||||||
This is a singleton.
|
|
||||||
It is initialized with the first call of use()
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
VMS.use().get_running_vms()
|
|
||||||
|
|
||||||
VMS.use() can also be called before the data is needed. e.g. to eliminate/reduce waiting time.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
list_store: Gio.ListStore
|
list_store: Gio.ListStore
|
||||||
_instance: "None | VMS" = None
|
_instance: "None | VMs" = None
|
||||||
|
|
||||||
# Make sure the VMS class is used as a singleton
|
# Make sure the VMS class is used as a singleton
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
raise RuntimeError("Call use() instead")
|
raise RuntimeError("Call use() instead")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def use(cls: Any) -> "VMS":
|
def use(cls: Any) -> "VMs":
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = cls.__new__(cls)
|
cls._instance = cls.__new__(cls)
|
||||||
cls.list_store = Gio.ListStore.new(VM)
|
cls.list_store = Gio.ListStore.new(VM)
|
||||||
@ -327,10 +322,13 @@ class VMS:
|
|||||||
return list(filter(lambda vm: vm.is_running(), self.list_store))
|
return list(filter(lambda vm: vm.is_running(), self.list_store))
|
||||||
|
|
||||||
def kill_all(self) -> None:
|
def kill_all(self) -> None:
|
||||||
|
log.debug(f"Running vms: {self.get_running_vms()}")
|
||||||
for vm in self.get_running_vms():
|
for vm in self.get_running_vms():
|
||||||
vm.stop()
|
vm.kill()
|
||||||
|
|
||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
|
log.error("NEVER FUCKING DO THIS")
|
||||||
|
return
|
||||||
self.list_store.remove_all()
|
self.list_store.remove_all()
|
||||||
for vm in get_saved_vms():
|
for vm in get_saved_vms():
|
||||||
self.list_store.append(vm)
|
self.list_store.append(vm)
|
||||||
@ -338,7 +336,7 @@ class VMS:
|
|||||||
|
|
||||||
def get_saved_vms() -> list[VM]:
|
def get_saved_vms() -> list[VM]:
|
||||||
vm_list = []
|
vm_list = []
|
||||||
|
log.info("=====CREATING NEW VM OBJ====")
|
||||||
try:
|
try:
|
||||||
# Execute `clan flakes add <path>` to democlan for this to work
|
# Execute `clan flakes add <path>` to democlan for this to work
|
||||||
for entry in list_history():
|
for entry in list_history():
|
||||||
|
@ -14,7 +14,7 @@ from clan_vm_manager.models.use_views import Views
|
|||||||
gi.require_version("Adw", "1")
|
gi.require_version("Adw", "1")
|
||||||
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
|
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
|
||||||
|
|
||||||
from clan_vm_manager.models.use_vms import VM, VMS, ClanGroup, Clans
|
from clan_vm_manager.models.use_vms import VM, ClanGroup, Clans
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -48,8 +48,8 @@ class ClanList(Gtk.Box):
|
|||||||
def __init__(self, config: ClanConfig) -> None:
|
def __init__(self, config: ClanConfig) -> None:
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
|
||||||
app = Gio.Application.get_default()
|
self.app = Gio.Application.get_default()
|
||||||
app.connect("join_request", self.on_join_request)
|
self.app.connect("join_request", self.on_join_request)
|
||||||
|
|
||||||
groups = Clans.use()
|
groups = Clans.use()
|
||||||
join = Join.use()
|
join = Join.use()
|
||||||
@ -123,7 +123,7 @@ class ClanList(Gtk.Box):
|
|||||||
def on_search_changed(self, entry: Gtk.SearchEntry) -> None:
|
def on_search_changed(self, entry: Gtk.SearchEntry) -> None:
|
||||||
Clans.use().filter_by_name(entry.get_text())
|
Clans.use().filter_by_name(entry.get_text())
|
||||||
# Disable the shadow if the list is empty
|
# Disable the shadow if the list is empty
|
||||||
if not VMS.use().list_store.get_n_items():
|
if not self.app.vms.list_store.get_n_items():
|
||||||
self.group_list.add_css_class("no-shadow")
|
self.group_list.add_css_class("no-shadow")
|
||||||
|
|
||||||
def render_vm_row(self, boxed_list: Gtk.ListBox, vm: VM) -> Gtk.Widget:
|
def render_vm_row(self, boxed_list: Gtk.ListBox, vm: VM) -> Gtk.Widget:
|
||||||
@ -202,7 +202,7 @@ class ClanList(Gtk.Box):
|
|||||||
|
|
||||||
def on_edit(self, action: Any, parameter: Any) -> None:
|
def on_edit(self, action: Any, parameter: Any) -> None:
|
||||||
target = parameter.get_string()
|
target = parameter.get_string()
|
||||||
vm = VMS.use().get_by_id(target)
|
vm = self.app.vms.get_by_id(target)
|
||||||
|
|
||||||
if not vm:
|
if not vm:
|
||||||
raise ClanError("Something went wrong. Please restart the app.")
|
raise ClanError("Something went wrong. Please restart the app.")
|
||||||
@ -220,7 +220,7 @@ class ClanList(Gtk.Box):
|
|||||||
row.add_css_class("trust")
|
row.add_css_class("trust")
|
||||||
|
|
||||||
# TODO: figure out how to detect that
|
# TODO: figure out how to detect that
|
||||||
exist = VMS.use().get_by_id(item.url.get_id())
|
exist = self.app.vms.use().get_by_id(item.url.get_id())
|
||||||
if exist:
|
if exist:
|
||||||
sub = row.get_subtitle()
|
sub = row.get_subtitle()
|
||||||
row.set_subtitle(
|
row.set_subtitle(
|
||||||
@ -292,7 +292,7 @@ class ClanList(Gtk.Box):
|
|||||||
|
|
||||||
if not row.get_active():
|
if not row.get_active():
|
||||||
row.set_state(True)
|
row.set_state(True)
|
||||||
vm.stop()
|
vm.shutdown()
|
||||||
|
|
||||||
def vm_status_changed(self, switch: Gtk.Switch, vm: VM, _vm: VM) -> None:
|
def vm_status_changed(self, switch: Gtk.Switch, vm: VM, _vm: VM) -> None:
|
||||||
switch.set_active(vm.is_running())
|
switch.set_active(vm.is_running())
|
||||||
|
Loading…
Reference in New Issue
Block a user