forked from clan/clan-core
Merge pull request 'UI: Added signal handling for stopped and started vm.' (#750) from Qubasa-main into main
This commit is contained in:
commit
ceebfccc82
@ -38,11 +38,12 @@ FORMATTER = {
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
def __init__(self, log_locations: bool) -> None:
|
||||
super().__init__()
|
||||
self.log_locations = log_locations
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
if record.levelno == logging.DEBUG:
|
||||
return FORMATTER[record.levelno](record, True).format(record)
|
||||
else:
|
||||
return FORMATTER[record.levelno](record, False).format(record)
|
||||
return FORMATTER[record.levelno](record, self.log_locations).format(record)
|
||||
|
||||
|
||||
class ThreadFormatter(logging.Formatter):
|
||||
@ -75,7 +76,7 @@ def setup_logging(level: Any) -> None:
|
||||
|
||||
# Create and add your custom handler
|
||||
default_handler.setLevel(level)
|
||||
default_handler.setFormatter(CustomFormatter())
|
||||
default_handler.setFormatter(CustomFormatter(level == logging.DEBUG))
|
||||
main_logger.addHandler(default_handler)
|
||||
|
||||
# Set logging level for other modules used by this module
|
||||
|
@ -1,11 +1,10 @@
|
||||
import os
|
||||
import shutil
|
||||
from math import floor
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
def get_term_filler(name: str) -> int:
|
||||
width, height = os.get_terminal_size()
|
||||
width, height = shutil.get_terminal_size()
|
||||
|
||||
filler = floor((width - len(name)) / 2)
|
||||
return filler - 1
|
||||
@ -16,16 +15,25 @@ def text_heading(heading: str) -> str:
|
||||
return f"{'=' * filler} {heading} {'=' * filler}"
|
||||
|
||||
|
||||
class CmdOut(NamedTuple):
|
||||
stdout: str
|
||||
stderr: str
|
||||
cwd: Path
|
||||
command: str
|
||||
returncode: int
|
||||
msg: str | None = None
|
||||
class CmdOut:
|
||||
def __init__(
|
||||
self,
|
||||
stdout: str,
|
||||
stderr: str,
|
||||
cwd: Path,
|
||||
command: str,
|
||||
returncode: int,
|
||||
msg: str | None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.cwd = cwd
|
||||
self.command = command
|
||||
self.returncode = returncode
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""
|
||||
self.error_str = f"""
|
||||
{text_heading(heading="Command")}
|
||||
{self.command}
|
||||
{text_heading(heading="Stderr")}
|
||||
@ -36,7 +44,10 @@ class CmdOut(NamedTuple):
|
||||
Message: {self.msg}
|
||||
Working Directory: '{self.cwd}'
|
||||
Return Code: {self.returncode}
|
||||
"""
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.error_str
|
||||
|
||||
|
||||
class ClanError(Exception):
|
||||
|
@ -61,7 +61,7 @@ def _init_proc(
|
||||
func: Callable,
|
||||
out_file: Path,
|
||||
proc_name: str,
|
||||
on_except: Callable[[Exception, mp.process.BaseProcess], None],
|
||||
on_except: Callable[[Exception, mp.process.BaseProcess], None] | None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# Create a new process group
|
||||
@ -89,7 +89,8 @@ def _init_proc(
|
||||
func(**kwargs)
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
on_except(ex, mp.current_process())
|
||||
if on_except is not None:
|
||||
on_except(ex, mp.current_process())
|
||||
finally:
|
||||
pid = os.getpid()
|
||||
gpid = os.getpgid(pid=pid)
|
||||
@ -99,8 +100,8 @@ def _init_proc(
|
||||
|
||||
def spawn(
|
||||
*,
|
||||
log_path: Path,
|
||||
on_except: Callable[[Exception, mp.process.BaseProcess], None],
|
||||
log_dir: Path,
|
||||
on_except: Callable[[Exception, mp.process.BaseProcess], None] | None,
|
||||
func: Callable,
|
||||
**kwargs: Any,
|
||||
) -> MPProcess:
|
||||
@ -108,13 +109,13 @@ def spawn(
|
||||
if mp.get_start_method(allow_none=True) is None:
|
||||
mp.set_start_method(method="forkserver")
|
||||
|
||||
if not log_path.is_dir():
|
||||
raise ClanError(f"Log path {log_path} is not a directory")
|
||||
log_path.mkdir(parents=True, exist_ok=True)
|
||||
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
|
||||
proc_name = f"MPExec:{func.__name__}"
|
||||
out_file = log_path / "out.log"
|
||||
out_file = log_dir / "out.log"
|
||||
|
||||
# Start the process
|
||||
proc = mp.Process(
|
||||
|
@ -1,19 +1,11 @@
|
||||
import multiprocessing as mp
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.errors import ClanError
|
||||
from gi.repository import Gio
|
||||
|
||||
from clan_vm_manager.errors.show_error import show_error_dialog
|
||||
from clan_vm_manager.models import VM, get_initial_vms
|
||||
|
||||
|
||||
# https://amolenaar.pages.gitlab.gnome.org/pygobject-docs/Adw-1/class-ToolbarView.html
|
||||
# Will be executed in the context of the child process
|
||||
def on_except(error: Exception, proc: mp.process.BaseProcess) -> None:
|
||||
show_error_dialog(ClanError(str(error)))
|
||||
|
||||
|
||||
class VMS:
|
||||
"""
|
||||
This is a singleton.
|
||||
@ -45,6 +37,14 @@ class VMS:
|
||||
cls.list_store.append(vm)
|
||||
return cls._instance
|
||||
|
||||
def handle_vm_stopped(self, func: Callable[[VM, VM], None]) -> None:
|
||||
for vm in self.list_store:
|
||||
vm.connect("vm_stopped", func)
|
||||
|
||||
def handle_vm_started(self, func: Callable[[VM, VM], None]) -> None:
|
||||
for vm in self.list_store:
|
||||
vm.connect("vm_started", func)
|
||||
|
||||
def get_running_vms(self) -> list[VM]:
|
||||
return list(filter(lambda vm: vm.is_running(), self.list_store))
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
import multiprocessing as mp
|
||||
import sys
|
||||
import tempfile
|
||||
import weakref
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
import gi
|
||||
from clan_cli import vms
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.history.add import HistoryEntry
|
||||
@ -15,17 +17,24 @@ from clan_vm_manager import assets
|
||||
from .errors.show_error import show_error_dialog
|
||||
from .executor import MPProcess, spawn
|
||||
|
||||
gi.require_version("Gtk", "4.0")
|
||||
import threading
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
|
||||
class VMStatus(StrEnum):
|
||||
RUNNING = "Running"
|
||||
STOPPED = "Stopped"
|
||||
|
||||
|
||||
def on_except(error: Exception, proc: mp.process.BaseProcess) -> None:
|
||||
show_error_dialog(ClanError(str(error)))
|
||||
|
||||
|
||||
class VM(GObject.Object):
|
||||
# Define a custom signal with the name "vm_stopped" and a string argument for the message
|
||||
__gsignals__: ClassVar = {
|
||||
"vm_started": (GObject.SignalFlags.RUN_FIRST, None, [GObject.Object]),
|
||||
"vm_stopped": (GObject.SignalFlags.RUN_FIRST, None, [GObject.Object]),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
icon: Path,
|
||||
@ -37,6 +46,9 @@ class VM(GObject.Object):
|
||||
self.data = data
|
||||
self.process = process
|
||||
self.status = status
|
||||
self.log_dir = tempfile.TemporaryDirectory(
|
||||
prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}"
|
||||
)
|
||||
self._finalizer = weakref.finalize(self, self.stop)
|
||||
|
||||
def start(self) -> None:
|
||||
@ -46,14 +58,23 @@ class VM(GObject.Object):
|
||||
vm = vms.run.inspect_vm(
|
||||
flake_url=self.data.flake.flake_url, flake_attr=self.data.flake.flake_attr
|
||||
)
|
||||
log_path = Path(".")
|
||||
|
||||
self.process = spawn(
|
||||
on_except=on_except,
|
||||
log_path=log_path,
|
||||
on_except=None,
|
||||
log_dir=Path(str(self.log_dir.name)),
|
||||
func=vms.run.run_vm,
|
||||
vm=vm,
|
||||
)
|
||||
self.emit("vm_started", self)
|
||||
GLib.timeout_add(50, self.vm_stopped_task)
|
||||
|
||||
def start_async(self) -> None:
|
||||
threading.Thread(target=self.start).start()
|
||||
|
||||
def vm_stopped_task(self) -> bool:
|
||||
if not self.is_running():
|
||||
self.emit("vm_stopped", self)
|
||||
return GLib.SOURCE_REMOVE
|
||||
return GLib.SOURCE_CONTINUE
|
||||
|
||||
def is_running(self) -> bool:
|
||||
if self.process is not None:
|
||||
@ -63,6 +84,9 @@ class VM(GObject.Object):
|
||||
def get_id(self) -> str:
|
||||
return self.data.flake.flake_url + self.data.flake.flake_attr
|
||||
|
||||
def stop_async(self) -> None:
|
||||
threading.Thread(target=self.stop).start()
|
||||
|
||||
def stop(self) -> None:
|
||||
if self.process is None:
|
||||
print("VM is already stopped", file=sys.stderr)
|
||||
@ -71,6 +95,11 @@ class VM(GObject.Object):
|
||||
self.process.kill_group()
|
||||
self.process = None
|
||||
|
||||
def read_log(self) -> str:
|
||||
if self.process is None:
|
||||
return ""
|
||||
return self.process.out_file.read_text()
|
||||
|
||||
|
||||
def get_initial_vms() -> list[VM]:
|
||||
vm_list = []
|
||||
|
@ -64,17 +64,38 @@ class ClanList(Gtk.Box):
|
||||
|
||||
return row
|
||||
|
||||
list_store = VMS.use().list_store
|
||||
vms = VMS.use()
|
||||
|
||||
boxed_list.bind_model(list_store, create_widget_func=create_widget)
|
||||
# TODO: Move this up to create_widget and connect every VM signal to its corresponding switch
|
||||
vms.handle_vm_stopped(self.stopped_vm)
|
||||
vms.handle_vm_started(self.started_vm)
|
||||
|
||||
boxed_list.bind_model(vms.list_store, create_widget_func=create_widget)
|
||||
|
||||
self.append(boxed_list)
|
||||
|
||||
def started_vm(self, vm: VM, _vm: VM) -> None:
|
||||
print("VM started", vm.data.flake.flake_attr)
|
||||
|
||||
def stopped_vm(self, vm: VM, _vm: VM) -> None:
|
||||
print("VM stopped", vm.data.flake.flake_attr)
|
||||
|
||||
def show_error_dialog(self, error: str) -> None:
|
||||
dialog = Gtk.MessageDialog(
|
||||
parent=self.get_toplevel(),
|
||||
modal=True,
|
||||
message_type=Gtk.MessageType.ERROR,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text=error,
|
||||
)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def on_row_toggle(self, vm: VM, row: Adw.SwitchRow, state: bool) -> None:
|
||||
print("Toggled", vm.data.flake.flake_attr, "active:", row.get_active())
|
||||
|
||||
if row.get_active():
|
||||
vm.start()
|
||||
vm.start_async()
|
||||
|
||||
if not row.get_active():
|
||||
vm.stop()
|
||||
vm.stop_async()
|
||||
|
Loading…
Reference in New Issue
Block a user