fix
This commit is contained in:
parent
58a0aed4f3
commit
a8ff844139
1 changed files with 89 additions and 4 deletions
|
|
@ -25,7 +25,9 @@ import secrets
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -272,6 +274,20 @@ class ServerManager:
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def git_pull(self) -> tuple[bool, str]:
|
||||||
|
"""--ff-only, deliberately: this runs unattended from the panel, with
|
||||||
|
no way to resolve a merge conflict or review a merge commit before it
|
||||||
|
happens. A diverged branch or dirty tree just fails cleanly here
|
||||||
|
instead of silently rewriting history or leaving a merge commit
|
||||||
|
nobody asked for -- the operator resolves it by hand (git status)
|
||||||
|
and re-runs Update."""
|
||||||
|
r = subprocess.run(
|
||||||
|
["git", "pull", "--ff-only"],
|
||||||
|
cwd=ROOT, capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
ok = r.returncode == 0
|
||||||
|
return ok, f"$ git pull --ff-only\n{r.stdout}{r.stderr}"
|
||||||
|
|
||||||
def build(self) -> tuple[bool, str]:
|
def build(self) -> tuple[bool, str]:
|
||||||
BIN_DIR.mkdir(exist_ok=True)
|
BIN_DIR.mkdir(exist_ok=True)
|
||||||
log = []
|
log = []
|
||||||
|
|
@ -1108,6 +1124,10 @@ RESTART_STEPS: list[tuple[str, str]] = [
|
||||||
("stop", "Stopping current instance"),
|
("stop", "Stopping current instance"),
|
||||||
*START_STEPS,
|
*START_STEPS,
|
||||||
]
|
]
|
||||||
|
UPDATE_STEPS: list[tuple[str, str]] = [
|
||||||
|
("pull", "Pulling latest code (git pull)"),
|
||||||
|
*RESTART_STEPS,
|
||||||
|
]
|
||||||
|
|
||||||
_STEP_ICONS = {
|
_STEP_ICONS = {
|
||||||
"active": "[b yellow]●[/]",
|
"active": "[b yellow]●[/]",
|
||||||
|
|
@ -1122,11 +1142,13 @@ class StartupProgressScreen(Screen):
|
||||||
(success or failure) -- there's no Escape binding while it's running,
|
(success or failure) -- there's no Escape binding while it's running,
|
||||||
just the Close button, which stays disabled until then."""
|
just the Close button, which stays disabled until then."""
|
||||||
|
|
||||||
def __init__(self, title: str, steps: list[tuple[str, str]]) -> None:
|
def __init__(self, title: str, steps: list[tuple[str, str]], on_close: Callable[[bool], None] | None = None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._title = title
|
self._title = title
|
||||||
self._steps = steps
|
self._steps = steps
|
||||||
self._labels = dict(steps)
|
self._labels = dict(steps)
|
||||||
|
self._on_close = on_close
|
||||||
|
self._success = False
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header()
|
yield Header()
|
||||||
|
|
@ -1150,6 +1172,7 @@ class StartupProgressScreen(Screen):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def finish(self, success: bool, message: str) -> None:
|
def finish(self, success: bool, message: str) -> None:
|
||||||
|
self._success = success
|
||||||
try:
|
try:
|
||||||
result = self.query_one("#startup-result", Static)
|
result = self.query_one("#startup-result", Static)
|
||||||
result.update(f"[b green]{message}[/]" if success else f"[b red]{message}[/]")
|
result.update(f"[b green]{message}[/]" if success else f"[b red]{message}[/]")
|
||||||
|
|
@ -1160,6 +1183,8 @@ class StartupProgressScreen(Screen):
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
if event.button.id == "startup-close-button":
|
if event.button.id == "startup-close-button":
|
||||||
self.dismiss()
|
self.dismiss()
|
||||||
|
if self._on_close:
|
||||||
|
self._on_close(self._success)
|
||||||
|
|
||||||
|
|
||||||
class MainScreen(Screen):
|
class MainScreen(Screen):
|
||||||
|
|
@ -1167,8 +1192,9 @@ class MainScreen(Screen):
|
||||||
Binding("1", "start", "Start"),
|
Binding("1", "start", "Start"),
|
||||||
Binding("2", "stop", "Stop"),
|
Binding("2", "stop", "Stop"),
|
||||||
Binding("3", "restart", "Restart"),
|
Binding("3", "restart", "Restart"),
|
||||||
Binding("4", "logs", "Logs"),
|
Binding("4", "update", "Update"),
|
||||||
Binding("5", "env", "Configure .env"),
|
Binding("5", "logs", "Logs"),
|
||||||
|
Binding("6", "env", "Configure .env"),
|
||||||
Binding("q", "quit_panel", "Exit"),
|
Binding("q", "quit_panel", "Exit"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1203,6 +1229,7 @@ class MainScreen(Screen):
|
||||||
Option("Start server", id="start"),
|
Option("Start server", id="start"),
|
||||||
Option("Stop server", id="stop"),
|
Option("Stop server", id="stop"),
|
||||||
Option("Restart server", id="restart"),
|
Option("Restart server", id="restart"),
|
||||||
|
Option("Update (git pull + rebuild + restart)", id="update"),
|
||||||
Option("View logs", id="logs"),
|
Option("View logs", id="logs"),
|
||||||
Option("Configure .env", id="env"),
|
Option("Configure .env", id="env"),
|
||||||
Option("Exit panel (server keeps running)", id="exit"),
|
Option("Exit panel (server keeps running)", id="exit"),
|
||||||
|
|
@ -1334,6 +1361,8 @@ class MainScreen(Screen):
|
||||||
self.action_stop()
|
self.action_stop()
|
||||||
elif option_id == "restart":
|
elif option_id == "restart":
|
||||||
self.action_restart()
|
self.action_restart()
|
||||||
|
elif option_id == "update":
|
||||||
|
self.action_update()
|
||||||
elif option_id == "logs":
|
elif option_id == "logs":
|
||||||
self.action_logs()
|
self.action_logs()
|
||||||
elif option_id == "env":
|
elif option_id == "env":
|
||||||
|
|
@ -1508,9 +1537,62 @@ class MainScreen(Screen):
|
||||||
# unlocking the menu once it's done.
|
# unlocking the menu once it's done.
|
||||||
self.app.call_from_thread(self._begin_start, progress)
|
self.app.call_from_thread(self._begin_start, progress)
|
||||||
|
|
||||||
|
def action_update(self) -> None:
|
||||||
|
self.query_one(OptionList).disabled = True
|
||||||
|
progress = StartupProgressScreen("Updating server", UPDATE_STEPS, on_close=self._after_update)
|
||||||
|
self.app.push_screen(progress)
|
||||||
|
self._update_worker(progress)
|
||||||
|
|
||||||
|
@work(thread=True, exclusive=True)
|
||||||
|
def _update_worker(self, progress: StartupProgressScreen) -> None:
|
||||||
|
try:
|
||||||
|
self.app.call_from_thread(progress.set_step, "pull", "active")
|
||||||
|
ok, out = MANAGER.git_pull()
|
||||||
|
if not ok:
|
||||||
|
self.app.call_from_thread(progress.set_step, "pull", "failed")
|
||||||
|
self.app.call_from_thread(progress.finish, False, f"git pull failed:\n{out[-500:]}")
|
||||||
|
self.app.call_from_thread(self._unlock_menu)
|
||||||
|
return
|
||||||
|
self.app.call_from_thread(progress.set_step, "pull", "done")
|
||||||
|
|
||||||
|
self.app.call_from_thread(progress.set_step, "stop", "active")
|
||||||
|
status = MANAGER.status()
|
||||||
|
if status.running:
|
||||||
|
MANAGER.stop()
|
||||||
|
self.app.call_from_thread(progress.set_step, "stop", "done")
|
||||||
|
except Exception as exc: # noqa: BLE001 - never leave the menu stuck
|
||||||
|
self.app.call_from_thread(progress.finish, False, f"Update failed: {exc}")
|
||||||
|
self.app.call_from_thread(self._unlock_menu)
|
||||||
|
return
|
||||||
|
# Same hand-off as restart -- _start_rest takes over unlocking the
|
||||||
|
# menu. Once the operator closes the progress screen, _after_update
|
||||||
|
# decides whether to re-exec the panel process itself (see its
|
||||||
|
# docstring for why that's needed on top of the Go binaries restart).
|
||||||
|
self.app.call_from_thread(self._begin_start, progress)
|
||||||
|
|
||||||
|
def _after_update(self, success: bool) -> None:
|
||||||
|
"""git pull can change server-panel.py itself (or any module it
|
||||||
|
imports) -- restarting the Go binaries alone leaves this already-
|
||||||
|
running Python process on the old code. Re-exec'ing replaces the
|
||||||
|
process image with a fresh interpreter run of the (now updated)
|
||||||
|
script, so the panel picks up its own changes too. Only fires once
|
||||||
|
the operator has actually seen and dismissed the "Server started"
|
||||||
|
confirmation, and only on success -- a failed pull/build leaves the
|
||||||
|
panel exactly as it was, still showing the failure, nothing to
|
||||||
|
re-exec into."""
|
||||||
|
if not success:
|
||||||
|
return
|
||||||
|
self.app.request_restart = True
|
||||||
|
self.app.exit()
|
||||||
|
|
||||||
|
|
||||||
class ServerPanelApp(App):
|
class ServerPanelApp(App):
|
||||||
TITLE = "OwpenGram Server Panel"
|
TITLE = "OwpenGram Server Panel"
|
||||||
|
# Set by MainScreen._after_update once a git pull + rebuild succeeds;
|
||||||
|
# __main__ checks this after run() returns to decide whether to re-exec
|
||||||
|
# the whole process (picking up changes to this script itself) instead
|
||||||
|
# of just exiting.
|
||||||
|
request_restart = False
|
||||||
CSS = """
|
CSS = """
|
||||||
#banner {
|
#banner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -1649,4 +1731,7 @@ class ServerPanelApp(App):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
ServerPanelApp().run()
|
app = ServerPanelApp()
|
||||||
|
app.run()
|
||||||
|
if app.request_restart:
|
||||||
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue