diff --git a/TODO.md b/TODO.md index fcf1641..d447959 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # Das OS — що зробити далі -Стан на 2026-10-25: версія 0.9.7 (не закомічено), етапи 1–8 готові (live ISO, власний репозиторій, оформлення, інсталятор Calamares зі знімками Btrfs, Go-утиліти `das-update` і `das-welcome`, серверний образ з консольним інсталятором, менші ISO, консольні помічники, профіль розробника), CI (етап 9: пакети й обидва ISO збираються на власному runner, ~45 хв). +Стан на 2026-10-25: версія 0.9.8 (не закомічено), етапи 1–8 готові (live ISO, власний репозиторій, оформлення, інсталятор Calamares зі знімками Btrfs, Go-утиліти `das-update` і `das-welcome`, серверний образ з консольним інсталятором, менші ISO, консольні помічники, профіль розробника), CI (етап 9: пакети й обидва ISO збираються на власному runner, ~45 хв). ## Етап 5. Go-утиліти (готово, лишилися перевірки) - [x] `das-update`: вікно, значок у треї (лише коли є оновлення), консольний режим `--cli`, знімок snapper pre/post, встановлення через pkexec, підказка про відкат. @@ -120,6 +120,7 @@ Live ISO без графіки з консольним інсталятором: - [x] Зміну шляхів конфігу та `history.log` перенесено в оригінальні `publisher` і `deployer` (`config.default_path()`, `pipeline._history_path()`): у checkout усе як було, у пакеті — `~/.config` і `~/.local/state`. Копії пакетів відрізняються від оригіналів лише значеннями за замовчуванням (localhost, user). - [x] У десктопний образ і в `das-server` 0.5.0 додано `nano`: `publish` і `deploy` відкривають конфіг у редакторі за F2 (`$VISUAL`/`$EDITOR`, інакше `nano`), без нього завершувались; 0.9.6. - [x] У десктопний образ додано `curl` (у `das-server` він уже був); 0.9.7. +- [x] `publish` і `deploy` падали з `BrokenPipeError`, коли `ssh` завершується одразу (недоступний хост): тепер це `RemoteError` з повідомленням `ssh` (код 255 завжди помилка), тест з фіктивним `ssh`; `publisher`/`deployer` 0.3.1, 0.9.8; виправлено й в оригіналах. - [ ] Прибрати значки томів live-носія («Том 6,6 ГБ») з робочого столу live-сесії. - [ ] Тема GRUB у встановленій системі (зараз стандартний чорний екран). - [ ] Перевірити меню Whisker вручну. diff --git a/VERSION b/VERSION index c81aa44..e3e1807 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.7 +0.9.8 diff --git a/srcpkgs/deployer/files/python/deployer/__init__.py b/srcpkgs/deployer/files/python/deployer/__init__.py index 571c4f0..e263a5c 100644 --- a/srcpkgs/deployer/files/python/deployer/__init__.py +++ b/srcpkgs/deployer/files/python/deployer/__init__.py @@ -1,2 +1,2 @@ """Deploy local projects to the home server over ssh + rsync.""" -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/srcpkgs/deployer/files/python/deployer/remote.py b/srcpkgs/deployer/files/python/deployer/remote.py index 030b5e5..6ddbd38 100644 --- a/srcpkgs/deployer/files/python/deployer/remote.py +++ b/srcpkgs/deployer/files/python/deployer/remote.py @@ -31,8 +31,11 @@ def _stream(cmd: list[str], log: Log | None, stdin: str | None = None, cwd=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=cwd, env=env, errors="replace") if stdin is not None: - proc.stdin.write(stdin) - proc.stdin.close() + try: + proc.stdin.write(stdin) + proc.stdin.close() + except BrokenPipeError: + pass # ssh already exited; its message is in the output below out = [] for line in proc.stdout: line = line.rstrip("\n") @@ -57,7 +60,7 @@ def run(server: Server, script: str, log: Log | None = None, check: bool = True, """Run a bash script on the server; values become shell variables in front of it.""" body = "set -uo pipefail\n" + shell_vars(**values) + script rc, out = _stream(["ssh", *ssh_opts(server), server.target, "bash", "-s"], log, stdin=body) - if check and rc != 0: + if rc == 255 or (check and rc != 0): raise RemoteError(f"remote command failed (exit {rc})" + (f": {out.splitlines()[-1]}" if out else "")) return out diff --git a/srcpkgs/deployer/files/tests/test_core.py b/srcpkgs/deployer/files/tests/test_core.py index e2d15b2..7dba138 100644 --- a/srcpkgs/deployer/files/tests/test_core.py +++ b/srcpkgs/deployer/files/tests/test_core.py @@ -1,3 +1,4 @@ +import os from pathlib import Path import pytest @@ -175,3 +176,14 @@ def test_python_config_layout(tmp_path): plan.dest = "/home/user/app/docker/stack" plan.app = config.App(name="stack", kind="docker", src=str(tmp_path), dest="docker/stack") assert get("docker").config_items(plan) == [(".env", "/home/user/app/configs/docker/stack.env")] + + +def test_ssh_exits_early(tmp_path, monkeypatch): + # ssh dies at once (e.g. unresolvable host): the broken pipe must surface as RemoteError with its message + fake = tmp_path / "ssh" + fake.write_text("#!/bin/sh\necho \"ssh: Could not resolve hostname\" >&2\nexit 255\n") + fake.chmod(0o755) + monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}") + srv = config.Server(host="nohost.invalid", user="u") + with pytest.raises(remote.RemoteError, match="Could not resolve"): + remote.run(srv, "x" * 5_000_000, check=False) diff --git a/srcpkgs/deployer/template b/srcpkgs/deployer/template index ad64e6b..5af1bb1 100644 --- a/srcpkgs/deployer/template +++ b/srcpkgs/deployer/template @@ -3,7 +3,7 @@ # Differences from the upstream: the config is in ~/.config/deployer (see bin/deploy) and # history.log is in ~/.local/state/deployer. pkgname=deployer -version=0.3.0 +version=0.3.1 revision=1 create_wrksrc=yes hostmakedepends="python3" diff --git a/srcpkgs/publisher/files/python/publisher/__init__.py b/srcpkgs/publisher/files/python/publisher/__init__.py index 5b10847..043252c 100644 --- a/srcpkgs/publisher/files/python/publisher/__init__.py +++ b/srcpkgs/publisher/files/python/publisher/__init__.py @@ -1,2 +1,2 @@ """Publish apk, AppImage and binaries to the apps page of the home Tomcat over ssh + rsync.""" -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/srcpkgs/publisher/files/python/publisher/remote.py b/srcpkgs/publisher/files/python/publisher/remote.py index d1cde1f..5aab562 100644 --- a/srcpkgs/publisher/files/python/publisher/remote.py +++ b/srcpkgs/publisher/files/python/publisher/remote.py @@ -26,8 +26,11 @@ def _stream(cmd: list[str], log: Log | None, stdin: str | None = None, cwd=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=cwd, env=env, errors="replace") if stdin is not None: - proc.stdin.write(stdin) - proc.stdin.close() + try: + proc.stdin.write(stdin) + proc.stdin.close() + except BrokenPipeError: + pass # ssh already exited; its message is in the output below out = [] for line in proc.stdout: line = line.rstrip("\n") @@ -52,7 +55,7 @@ def run(server: Server, script: str, log: Log | None = None, check: bool = True, """Run a bash script on the server; values become shell variables in front of it.""" body = "set -uo pipefail\n" + shell_vars(**values) + script rc, out = _stream(["ssh", *ssh_opts(server), server.target, "bash", "-s"], log, stdin=body) - if check and rc != 0: + if rc == 255 or (check and rc != 0): raise RemoteError(f"remote command failed (exit {rc})" + (f": {out.splitlines()[-1]}" if out else "")) return out diff --git a/srcpkgs/publisher/files/tests/test_core.py b/srcpkgs/publisher/files/tests/test_core.py index c5d8b7f..dff64d4 100644 --- a/srcpkgs/publisher/files/tests/test_core.py +++ b/srcpkgs/publisher/files/tests/test_core.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from publisher import config, detect, packages, pipeline +from publisher import config, detect, packages, pipeline, remote from publisher.packages import BINARY, KINDS, Published APK, APPIMAGE, TARGZ = KINDS[0], KINDS[1], KINDS[-1] @@ -215,3 +215,14 @@ def test_detect_go(tmp_path): def test_site_name_cleanup(): assert detect._site_name("Das Pressure 2") == "DasPressure2" assert os.path.basename(detect._home(Path.home() / "a")) == "a" + + +def test_ssh_exits_early(tmp_path, monkeypatch): + # ssh dies at once (e.g. unresolvable host): the broken pipe must surface as RemoteError with its message + fake = tmp_path / "ssh" + fake.write_text("#!/bin/sh\necho \"ssh: Could not resolve hostname\" >&2\nexit 255\n") + fake.chmod(0o755) + monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}") + srv = config.Server(host="nohost.invalid", user="u") + with pytest.raises(remote.RemoteError, match="Could not resolve"): + remote.run(srv, "x" * 5_000_000, check=False) diff --git a/srcpkgs/publisher/template b/srcpkgs/publisher/template index 38a5bf3..b549b95 100644 --- a/srcpkgs/publisher/template +++ b/srcpkgs/publisher/template @@ -3,7 +3,7 @@ # Differences from the upstream: the config is in ~/.config/publisher (see bin/publish) and # history.log is in ~/.local/state/publisher. pkgname=publisher -version=0.3.0 +version=0.3.1 revision=1 create_wrksrc=yes hostmakedepends="python3"