Decouple job lifecycle from screen workers

Jobs were tied to screen @work tasks, so switch_screen cancelled the
worker and lost the process reference. Now start_job uses
asyncio.create_task so jobs survive screen changes and can be killed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-06 18:18:39 +02:00
parent 6a0389f437
commit 0110b00b67
3 changed files with 9 additions and 11 deletions

View File

@@ -54,6 +54,9 @@ class JobManager:
def remove_finished(self) -> None: def remove_finished(self) -> None:
self._jobs = {k: v for k, v in self._jobs.items() if v.status == "running"} self._jobs = {k: v for k, v in self._jobs.items() if v.status == "running"}
def start_job(self, app, job: Job, *cli_args: str) -> None:
asyncio.create_task(self.run_job(app, job, *cli_args))
async def run_job(self, app, job: Job, *cli_args: str) -> int: async def run_job(self, app, job: Job, *cli_args: str) -> int:
proc = await start_cli_process(*cli_args) proc = await start_cli_process(*cli_args)
job._proc = proc job._proc = proc

View File

@@ -2,8 +2,6 @@ from textual.app import ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button, Select from textual.widgets import Header, Footer, Static, Button, Select
from textual.containers import Vertical, Horizontal from textual.containers import Vertical, Horizontal
from textual import work
from tui.config import list_conf_dir, has_targets, has_remotes from tui.config import list_conf_dir, has_targets, has_remotes
from tui.jobs import job_manager from tui.jobs import job_manager
from tui.widgets import ConfirmDialog from tui.widgets import ConfirmDialog
@@ -65,20 +63,18 @@ class BackupScreen(Screen):
callback=lambda ok: self._do_backup_all() if ok else None, callback=lambda ok: self._do_backup_all() if ok else None,
) )
@work def _do_backup(self, target: str, remote: str) -> None:
async def _do_backup(self, target: str, remote: str) -> None:
job = job_manager.create_job("backup", f"Backup: {target}") job = job_manager.create_job("backup", f"Backup: {target}")
args = ["backup", f"--target={target}"] args = ["backup", f"--target={target}"]
if remote: if remote:
args.append(f"--remote={remote}") args.append(f"--remote={remote}")
job_manager.start_job(self.app, job, *args)
self.app.switch_screen("running_tasks") self.app.switch_screen("running_tasks")
await job_manager.run_job(self.app, job, *args)
@work def _do_backup_all(self) -> None:
async def _do_backup_all(self) -> None:
job = job_manager.create_job("backup", "Backup All Targets") job = job_manager.create_job("backup", "Backup All Targets")
job_manager.start_job(self.app, job, "backup", "--all")
self.app.switch_screen("running_tasks") self.app.switch_screen("running_tasks")
await job_manager.run_job(self.app, job, "backup", "--all")
def action_go_back(self) -> None: def action_go_back(self) -> None:
self.app.pop_screen() self.app.pop_screen()

View File

@@ -143,16 +143,15 @@ class RestoreScreen(Screen):
callback=lambda ok: self._do_restore(target, remote, snapshot, dest, skip_mysql) if ok else None, callback=lambda ok: self._do_restore(target, remote, snapshot, dest, skip_mysql) if ok else None,
) )
@work def _do_restore(self, target: str, remote: str, snapshot: str, dest: str, skip_mysql: bool = False) -> None:
async def _do_restore(self, target: str, remote: str, snapshot: str, dest: str, skip_mysql: bool = False) -> None:
job = job_manager.create_job("restore", f"Restore: {target}") job = job_manager.create_job("restore", f"Restore: {target}")
args = ["restore", f"--target={target}", f"--remote={remote}", f"--snapshot={snapshot}"] args = ["restore", f"--target={target}", f"--remote={remote}", f"--snapshot={snapshot}"]
if dest: if dest:
args.append(f"--dest={dest}") args.append(f"--dest={dest}")
if skip_mysql: if skip_mysql:
args.append("--skip-mysql") args.append("--skip-mysql")
job_manager.start_job(self.app, job, *args)
self.app.switch_screen("running_tasks") self.app.switch_screen("running_tasks")
await job_manager.run_job(self.app, job, *args)
def action_go_back(self) -> None: def action_go_back(self) -> None:
self.app.pop_screen() self.app.pop_screen()