From d0cda51639389dd139403629695a2776d3bc80f5 Mon Sep 17 00:00:00 2001 From: AramJonghu Date: Sat, 23 May 2026 20:31:48 +0200 Subject: [PATCH 1/5] wait for instance to fully terminate before restart --- .../__pycache__/scheme.cpython-314.pyc | Bin 30628 -> 30628 bytes .../__pycache__/shell.cpython-314.pyc | Bin 3233 -> 3592 bytes cli/src/zshell/subcommands/shell.py | 8 +++++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cli/src/zshell/subcommands/__pycache__/scheme.cpython-314.pyc b/cli/src/zshell/subcommands/__pycache__/scheme.cpython-314.pyc index 010ddf9aca33ee16104f3c05a33b7ea086c845dd..e5535703c5f0e815418b2501a20b9cf701d2044c 100644 GIT binary patch delta 21 bcmZ4To^i>0MlNkWUM>b8SoD4)*Q{~?O>+j| delta 21 bcmZ4To^i>0MlNkWUM>b8$U3!=YgRb`OKJv# diff --git a/cli/src/zshell/subcommands/__pycache__/shell.cpython-314.pyc b/cli/src/zshell/subcommands/__pycache__/shell.cpython-314.pyc index a47f91f92d18495fe005df7c52d64cec7377d725..7f6e76463bf6cf7365accb2eab566883d2689ae9 100644 GIT binary patch delta 1001 zcmZ8fO-vI(6rQ&~-R*9FSOcY$mLjFqEl_@f(SVVtk@$-M^xy%6#G5*^ZI$?v`DW&OZ{Bc7721IxHLL%T1mD25isFr!ny~mHI#D)YKFGQB$5cr4u;*LF3HOw58#YGQ1T{}}ie!=2ZhHtExe;mHxx7Od;zW*4pTm}%y8-YLEfL$(W>`NEi$ zXJt#&*B3h>QPSx}jIxz<<)_FhS9Lc2UTE9&?wjp+aCHVYwc1B@^L0zYGuPtOvVTQb z(@xE}KC0Tf8eUbyi~O1z`{dC!WzR$9o-*6{K@M)ak*|7JL86~zg$9ywE&3DGJ7y4N zVIpKj4ijBk@E>G6gGR;&C|^mwNXrl>cjYFfiD?<=lG!_gI{fNecZ=_4+XD zKK*J7<5LV;DHMO2D9)T??m&usRt9B%v64~Jqb3{KAYxVlCqE|{C#EeQ$D@UDbHc(& za$k)EyQmGDuKYI&TRZ@4AZ-Jw(w(szW^KWqg?c}?vYiy276gzHJl7#2xAA_65 Ak^lez delta 691 zcmYk3&ubGw6vyZ7&i?)pgV82w<7yJuY%8@ws~3q>LGZGMZH`7GrHE}6oLxbXLJ`DX zDy%O;K@h}?Ud5A#B7%Q|P_OpjEkVJnGrOrehtItCo%eX(nc13pU2$hz$0U4h>>gTu zFQ;y~{s9QtBsWL|S4ooZkiaa7>}!xnJ?N---zd5BqKmJD_GrgSUrnalwxZ_oSw+i( zS~@Bz`naH|O_0A`#(}8+Cz@jsH^5dbk>E3!l%Z1=Bk>cQaGVGlByfL-(IKY#no8I1 ztgmG_e=k}e-jBp7-bwJ`Oa*9z6>~o5`4tIv52dRgMDinWbCVrWjYI|dBwqslt?-+ z<(Wq!mAlp$<@pt+aIy)3GDkOLnuseLF@JmFGlz3eV=HlTizTeav%}(W0WWA(iMsew z`&>BA6(*4N)zCAia7!PUb)gF!L*Lj15BH53^P~_38dx<)5TI@SYI{O#a&$9Cxs5<( zr-a@T2smPy!}oJjS$T#Qw(XwmG9BO8(@pV+na|8oHa;7-r!TVG5ALqqOIQ;}cI}Ax qe{sinpJONkz&BDv->F-3KV>2nh7!OWE;&I_e85-|;n2iw=g}W)m4*ZW diff --git a/cli/src/zshell/subcommands/shell.py b/cli/src/zshell/subcommands/shell.py index c6a8587..ae73d0b 100644 --- a/cli/src/zshell/subcommands/shell.py +++ b/cli/src/zshell/subcommands/shell.py @@ -1,4 +1,5 @@ import subprocess +import time import typer args = ["qs", "-c", "zshell"] @@ -8,7 +9,7 @@ app = typer.Typer() @app.command() def kill(): - subprocess.run(args + ["kill"], check=True) + subprocess.run(args + ["kill"], check=False) @app.command() @@ -19,6 +20,11 @@ def start(no_daemon: bool = False): @app.command() def restart(no_daemon: bool = False): subprocess.run(args + ["kill"], check=False) + for _ in range(50): + result = subprocess.run(args + ["kill"], capture_output=True) + if result.returncode == 255: + break + time.sleep(0.05) subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), check=True) From b49165e7eac336d6edfde36bd1ef92fbd1797186 Mon Sep 17 00:00:00 2001 From: AramJonghu Date: Sat, 23 May 2026 20:48:51 +0200 Subject: [PATCH 2/5] minor typer adjustments to use typer in error/exception throws --- .../__pycache__/shell.cpython-314.pyc | Bin 3592 -> 6639 bytes cli/src/zshell/subcommands/shell.py | 44 ++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/cli/src/zshell/subcommands/__pycache__/shell.cpython-314.pyc b/cli/src/zshell/subcommands/__pycache__/shell.cpython-314.pyc index 7f6e76463bf6cf7365accb2eab566883d2689ae9..a33767152f267077925eaebafc8768711f2566be 100644 GIT binary patch literal 6639 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 literal 3592 zcmcH*TWl0n^xm1-cONW8icl!ErLaPG5Gx=c2+^uBEbCYcCZ+2#JKe53yR+PxjfD?W z!35%m64SS&*x1l~S*mk{buj<$V9_a<&FnAm*LC`H!qg#`LJ5i+RHe-pY4BOy= zi3s0f4A2-FwD(b5aHh_r0}a~IfD{zPI~)oE85Chpue6%gE1VEK@$)b1F%CPFu37 zXJnJ8@>z34({&l{QH@+KnNOKA--#B^5GEc+ES}A^fX2`-QqzUlx!8E`8HOIg)H`gDT+%Yx?T-)^?zAD1Napi~m9(@(f;cY_fRBS0Fb|DUxbA+;zBES$ne{jf?XvTC73zy4 zRB(mbJ?EjQNVeMw!oafCc<9f)C+yH}M@QEW^6Jk-iL<<@0jMt?TfTPmqJmHWZMv6A z;awV7%LbosDUTv9sV061z5vhbJ|21fkYY`0vS1a7mN1G|p=hN)`jElQA<_!+lXZY% z5g%c_hy)m7I#Lfw=F=Jt5)F>#RU@TQ$;#w3>M?aqD?~&d%M>T5muO~Dw@l{v?#-La zf$f7$9@?F-%L8l_ChpL#l$beb=dH%omv+7T>P37jyy1gQ?{E54n)XhOe%5?Nx*mS^ zqW4B9Tne>)8)};puZKE+Xb9i(H@p{oCwQs%d;hwH0BUOeEr>kLmC98oV?Y20oB}Zf zTlW{KHU3zp7*H`&P^n~&7;m`i0SUo>^CaRnBi(GtP&<6)MnGxsV&mN4cD=ClB+UJdDR%=K1PlwLD&yF2DR`fc5F&O2c3Z z+TcX&6zn`m&kxi*x&~@`cE7M6+nqfP6173nMbVOH+A@jEV}mfI-9Z7hum`qClp$^w zreb$@xyN&Mju_qC2k76$$dzoV@Ax(Ny>fi6<;1nuQYA^_Arld)aN0a8^r+NZe&R)N zU!=KQ!CCE~9_vg&Bb7ql&*oH277El?c?XtXf9%ykIvL`XhvzQ~Vd_6j(#4#Xx5~1I z7z=B6<&6D z1kZ&HZ*4fgaY4ZNu|*LH>$F?{_G7VI9K`DTbHO*ps6|W7GLH&N+7DKIhmz*C|Gk o9g18-iQD&K9C!cMjRS|jiG3Bj+WXys_+1{k{E9npStW@70&SWktpET3 diff --git a/cli/src/zshell/subcommands/shell.py b/cli/src/zshell/subcommands/shell.py index ae73d0b..d66ea57 100644 --- a/cli/src/zshell/subcommands/shell.py +++ b/cli/src/zshell/subcommands/shell.py @@ -1,5 +1,8 @@ import subprocess +import sys import time + +import click import typer args = ["qs", "-c", "zshell"] @@ -9,40 +12,65 @@ app = typer.Typer() @app.command() def kill(): - subprocess.run(args + ["kill"], check=False) + result = subprocess.run(args + ["kill"], capture_output=True) + if result.returncode != 0: + raise click.ClickException("No running instance to kill.") + sys.stderr.write(result.stderr.decode()) @app.command() def start(no_daemon: bool = False): - subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), check=True) + check = subprocess.run(args + ["ipc"] + ["show"], capture_output=True) + if check.returncode == 0: + raise click.ClickException("An instance of this configuration is already running.") + result = subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True) + if result.returncode != 0: + raise click.ClickException(result.stderr.decode().strip()) + sys.stderr.write(result.stderr.decode()) @app.command() def restart(no_daemon: bool = False): - subprocess.run(args + ["kill"], check=False) + subprocess.run(args + ["kill"]) for _ in range(50): result = subprocess.run(args + ["kill"], capture_output=True) if result.returncode == 255: break time.sleep(0.05) - subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), check=True) + result = subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True) + if result.returncode != 0: + raise click.ClickException(result.stderr.decode().strip()) + sys.stderr.write(result.stderr.decode()) @app.command() def show(): - subprocess.run(args + ["ipc"] + ["show"], check=True) + result = subprocess.run(args + ["ipc"] + ["show"], capture_output=True) + if result.returncode != 0: + raise click.ClickException(result.stderr.decode().strip()) + sys.stderr.write(result.stderr.decode()) @app.command() def log(): - subprocess.run(args + ["log"], check=True) + result = subprocess.run(args + ["log"], capture_output=True) + if result.returncode != 0: + raise click.ClickException(result.stderr.decode().strip()) + sys.stdout.write(result.stdout.decode()) + sys.stderr.write(result.stderr.decode()) @app.command() def lock(): - subprocess.run(args + ["ipc"] + ["call"] + ["lock"] + ["lock"], check=True) + result = subprocess.run(args + ["ipc"] + ["call"] + ["lock"] + ["lock"], capture_output=True) + if result.returncode != 0: + raise click.ClickException(result.stderr.decode().strip()) + sys.stderr.write(result.stderr.decode()) @app.command() def call(target: str, method: str, method_args: list[str] = typer.Argument(None)): - subprocess.run(args + ["ipc"] + ["call"] + [target] + [method] + (method_args or []), check=True) + result = subprocess.run(args + ["ipc"] + ["call"] + [target] + [method] + (method_args or []), capture_output=True) + if result.returncode != 0: + raise click.ClickException(result.stderr.decode().strip()) + sys.stderr.write(result.stderr.decode()) From 5e9b3734059e7bafc9d3125f6178e072f8184c81 Mon Sep 17 00:00:00 2001 From: AramJonghu Date: Sat, 23 May 2026 20:54:35 +0200 Subject: [PATCH 3/5] tests did not match changed code logic --- cli/tests/test_shell.py | 82 ++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/cli/tests/test_shell.py b/cli/tests/test_shell.py index feb292c..9875d99 100644 --- a/cli/tests/test_shell.py +++ b/cli/tests/test_shell.py @@ -1,13 +1,15 @@ from __future__ import annotations +from subprocess import CompletedProcess from unittest.mock import patch, call + +from typer.testing import CliRunner from zshell.subcommands.shell import app +runner = CliRunner() -def invoke(*args: str) -> None: - from typer.testing import CliRunner - runner = CliRunner() +def invoke(*args: str): result = runner.invoke(app, args) if result.exit_code != 0: raise RuntimeError(result.output) @@ -16,72 +18,118 @@ def invoke(*args: str) -> None: class TestKill: @patch("zshell.subcommands.shell.subprocess.run") - def test_kill_runs_qs_kill(self, mock_run): + def test_kill_runs_qs_kill_success(self, mock_run): + mock_run.return_value = CompletedProcess([], 0, b"", b"Killed abc\n") invoke("kill") - mock_run.assert_called_once_with(["qs", "-c", "zshell", "kill"], check=True) + mock_run.assert_called_once_with(["qs", "-c", "zshell", "kill"], capture_output=True) + + @patch("zshell.subcommands.shell.subprocess.run") + def test_kill_no_instance_errors(self, mock_run): + mock_run.return_value = CompletedProcess([], 255, b"", b"No running instances\n") + result = runner.invoke(app, ["kill"]) + assert result.exit_code != 0 + assert "No running instance to kill" in result.output class TestStart: @patch("zshell.subcommands.shell.subprocess.run") def test_start_default_daemon(self, mock_run): + mock_run.side_effect = [ + CompletedProcess([], 1, b"", b""), # ipc show → no instance + CompletedProcess([], 0, b"", b"Launching config\n"), # launch ok + ] invoke("start") - mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n", "-d"], check=True) + assert mock_run.call_args_list == [ + call(["qs", "-c", "zshell", "ipc", "show"], capture_output=True), + call(["qs", "-c", "zshell", "-n", "-d"], capture_output=True), + ] @patch("zshell.subcommands.shell.subprocess.run") def test_start_no_daemon(self, mock_run): + mock_run.side_effect = [ + CompletedProcess([], 1, b"", b""), + CompletedProcess([], 0, b"", b"Launching config\n"), + ] invoke("start", "--no-daemon") - mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n"], check=True) + assert mock_run.call_args_list == [ + call(["qs", "-c", "zshell", "ipc", "show"], capture_output=True), + call(["qs", "-c", "zshell", "-n"], capture_output=True), + ] + + @patch("zshell.subcommands.shell.subprocess.run") + def test_start_already_running_errors(self, mock_run): + mock_run.return_value = CompletedProcess([], 0, b"", b"target visibilities\n") + result = runner.invoke(app, ["start"]) + assert result.exit_code != 0 + assert "already running" in result.output class TestShow: @patch("zshell.subcommands.shell.subprocess.run") def test_show_runs_ipc_show(self, mock_run): + mock_run.return_value = CompletedProcess([], 0, b"", b"target visibilities\n") invoke("show") - mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "show"], check=True) + mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "show"], capture_output=True) class TestLog: @patch("zshell.subcommands.shell.subprocess.run") def test_log_runs_qs_log(self, mock_run): + mock_run.return_value = CompletedProcess([], 0, b"log output\n", b"") invoke("log") - mock_run.assert_called_once_with(["qs", "-c", "zshell", "log"], check=True) + mock_run.assert_called_once_with(["qs", "-c", "zshell", "log"], capture_output=True) class TestLock: @patch("zshell.subcommands.shell.subprocess.run") def test_lock_runs_ipc_call_lock(self, mock_run): + mock_run.return_value = CompletedProcess([], 0, b"", b"") invoke("lock") - mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "lock", "lock"], check=True) + mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "lock", "lock"], capture_output=True) class TestCall: @patch("zshell.subcommands.shell.subprocess.run") def test_call_no_args(self, mock_run): + mock_run.return_value = CompletedProcess([], 0, b"", b"") invoke("call", "target", "method") - mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "target", "method"], check=True) + mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "target", "method"], capture_output=True) @patch("zshell.subcommands.shell.subprocess.run") def test_call_with_args(self, mock_run): + mock_run.return_value = CompletedProcess([], 0, b"", b"") invoke("call", "target", "method", "arg1", "arg2") mock_run.assert_called_once_with( ["qs", "-c", "zshell", "ipc", "call", "target", "method", "arg1", "arg2"], - check=True, + capture_output=True, ) class TestRestart: @patch("zshell.subcommands.shell.subprocess.run") - def test_restart_kills_then_starts_daemon(self, mock_run): + def test_restart_kills_then_starts(self, mock_run): + mock_run.side_effect = [ + CompletedProcess([], 0, b"", b"Killed abc\n"), # first kill (no capture) + CompletedProcess([], 255, b"", b""), # poll → no instance + CompletedProcess([], 0, b"", b"Launching config\n"), # launch ok + ] invoke("restart") assert mock_run.call_args_list == [ - call(["qs", "-c", "zshell", "kill"], check=False), - call(["qs", "-c", "zshell", "-n", "-d"], check=True), + call(["qs", "-c", "zshell", "kill"]), # no capture_output + call(["qs", "-c", "zshell", "kill"], capture_output=True), + call(["qs", "-c", "zshell", "-n", "-d"], capture_output=True), ] @patch("zshell.subcommands.shell.subprocess.run") def test_restart_no_daemon(self, mock_run): + mock_run.side_effect = [ + CompletedProcess([], 0, b"", b"Killed abc\n"), + CompletedProcess([], 255, b"", b""), + CompletedProcess([], 0, b"", b"Launching config\n"), + ] invoke("restart", "--no-daemon") assert mock_run.call_args_list == [ - call(["qs", "-c", "zshell", "kill"], check=False), - call(["qs", "-c", "zshell", "-n"], check=True), + call(["qs", "-c", "zshell", "kill"]), + call(["qs", "-c", "zshell", "kill"], capture_output=True), + call(["qs", "-c", "zshell", "-n"], capture_output=True), ] From 78fcf33b3a77003e102401acedf5e40f4ba69f62 Mon Sep 17 00:00:00 2001 From: AramJonghu Date: Sun, 24 May 2026 03:09:19 +0200 Subject: [PATCH 4/5] refactor(cli): clean shell start/restart, drop redundant ipc check --- .../__pycache__/scheme.cpython-314.pyc | Bin 30628 -> 30626 bytes .../__pycache__/screenshot.cpython-314.pyc | Bin 1042 -> 1040 bytes .../__pycache__/shell.cpython-314.pyc | Bin 6639 -> 6472 bytes .../__pycache__/wallpaper.cpython-314.pyc | Bin 2438 -> 2436 bytes cli/src/zshell/subcommands/shell.py | 29 ++++++----- cli/tests/test_shell.py | 47 ++++++++---------- 6 files changed, 37 insertions(+), 39 deletions(-) diff --git a/cli/src/zshell/subcommands/__pycache__/scheme.cpython-314.pyc b/cli/src/zshell/subcommands/__pycache__/scheme.cpython-314.pyc index e5535703c5f0e815418b2501a20b9cf701d2044c..06584ac0875cc72ca33cd0db9527c59ae4813dbb 100644 GIT binary patch delta 39 tcmZ4To^jE8Ms96BUM>b8P<9vE$gRi1ZL6P=pPQ0Ms96BUM>b8SoB_SBexz4uf2Xoer~FMc7A1kZsukembhX71CR{K diff --git a/cli/src/zshell/subcommands/__pycache__/screenshot.cpython-314.pyc b/cli/src/zshell/subcommands/__pycache__/screenshot.cpython-314.pyc index ecd3450d69ddad48296ea32af8832e861aa929c1..f1921ea0037d724a68c84ccba54f287a3d7a389b 100644 GIT binary patch delta 37 rcmbQlF@b|yn~#@^0SJ`cg*I}}W8`+!&&bbB)lV!+%-y_=F@^~Mjt~hU delta 39 tcmbQhF^Pj)n~#@^0SNN832fw^$H?ofpOK%Ns-K--nV*}vc?)9<69BdD3eo@o diff --git a/cli/src/zshell/subcommands/__pycache__/shell.cpython-314.pyc b/cli/src/zshell/subcommands/__pycache__/shell.cpython-314.pyc index a33767152f267077925eaebafc8768711f2566be..d6ca5095d3c790b30885eb76e34adb694c63a97e 100644 GIT binary patch delta 1671 zcmZ8h&u<$=6rNeH?akV|o3)+TiR~n=W5-TNoY17W6chrYqExt%Rar$OR8ed-M$X#I zZm8%Xb<3ep1gV*#h98F@kZ2`@it;BQpXu!EhXMy_T;?UAS2p7`4lXuwk9$sVaLaBq^trw8e|kycZSP z3%GJKgq08QgrubHU=>a;q1M^nE-iryX82&a%TWl*+9hewLr^qo*Vf z-N2IH38$sj>HAS?8i!H5ef=K(9y^{&EEE@Wi8(U2NUvJH7J0~Hfq5rJ|L{aYX-k5G zVE$JQY>hUpcr-hgTP$h~8_5|IzP>XxMpLo`><_S8yc@{YMO)v^+?Ww!-(>PH@8-%(d2dw7G*@Ih44wqy)ygG zY+VXfrBF=@*QIz>ir1u}^8TOg(xxM@X8Y3lne%h`nq#Et^449^tFGwU8#UL+hI3@o zF4gV6s@=CbbF+Wp`cPbyeQAwx=c!ZU}?7 ztw`u0z%C{Hl$~_KeTbJj+Qv>|+;bZNoIh+nSPYuqE)XnRYBCiSfK3_A-1lYBP8E7l z8bK9sDvr%>zJyJ-bQT(IQ3DJEH6#j_KA1XVwv2w^1=qQ~H~1j&&=Nnzkucn;Kft1N z#%N2AF{Z%Sc~ginQ-~S}af<#RigGWSu|c3YrUlkF{X314wtxojj}m?~(7h9w!AYoV z_7G^0Cah0GUIn_dD!G;al1IP+y%Rt$`DSeNrY%6PSe+fc=->iEGs;f(3`$#{pg%hN zue2=uGsiyuh>c5=Bm$;XTzFJ}ypdnbHQJyTi$+n)&zo8UAr^AEl1XCRVv^VpvY_Nb zUdz!}oq^|uAsfr6zYf3%l+UD2r_PVB?pcj}IC**eI-hJR?v>@U%QdBUEqq;xlwW9y z^2*5Bk($`OdgPiIpA$#hy(ks1+OXtH z^9^2KC?0Qh7G1Ls4U%4XH<1`H28aq^*1FXOWRw{hjIBGB3tany@E+z-Xv`Bd{|EZJ zR8>6df2zAUoS^L5cZVaA5rEiL9WgG|t$eBI)HFf!%qzf;^bwePMBf6=W@#97byPx;>p{>?$R-LW7} a*2b3T5qsVzI(tKtU1|B+j|{r|BBDpdh3v9j(ACxh+{{ z%*6R&x-8q8!y+0@@Z0ufUmCw@T;dmF#DGJm4Iz9mCVo+t>6Z9K&wU-lpeMP%dwR~f zzkBXE@ARvl-+R?|O;rfWoj*OVZxq!A`c1Lz2z3?562;x1t|n|cNr*a`bI;Kzk3%Ua z=%Q+|+-H+@0!=hR9dL}ct4^Kl$7sdffcTmi55THW*G8kXa<-Qy6lAPA!01lU5Q%6d zhp&Sqep^|K13GBri8kDDaZIsjitMYT$IOQ9GLk(5!4+Q)t jxo;yv^)AZWHF#=HumJ0D`=20vowI7 None: + result = subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True) + stdout = result.stdout.decode().strip() + if stdout: + if "already running" in stdout.lower(): + raise click.ClickException(stdout) + if result.returncode != 0: + stderr = result.stderr.decode().strip() + raise click.ClickException(stderr) + + @app.command() def start(no_daemon: bool = False): - check = subprocess.run(args + ["ipc"] + ["show"], capture_output=True) - if check.returncode == 0: - raise click.ClickException("An instance of this configuration is already running.") - result = subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True) - if result.returncode != 0: - raise click.ClickException(result.stderr.decode().strip()) - sys.stderr.write(result.stderr.decode()) + start_instance(no_daemon) @app.command() def restart(no_daemon: bool = False): - subprocess.run(args + ["kill"]) - for _ in range(50): + subprocess.run(args + ["kill"], capture_output=True) + deadline = time.monotonic() + 2.5 + while time.monotonic() < deadline: result = subprocess.run(args + ["kill"], capture_output=True) if result.returncode == 255: break time.sleep(0.05) - result = subprocess.run(args + ["-n"] + ([] if no_daemon else ["-d"]), capture_output=True) - if result.returncode != 0: - raise click.ClickException(result.stderr.decode().strip()) - sys.stderr.write(result.stderr.decode()) + start_instance(no_daemon=no_daemon) @app.command() diff --git a/cli/tests/test_shell.py b/cli/tests/test_shell.py index 9875d99..1763247 100644 --- a/cli/tests/test_shell.py +++ b/cli/tests/test_shell.py @@ -34,35 +34,30 @@ class TestKill: class TestStart: @patch("zshell.subcommands.shell.subprocess.run") def test_start_default_daemon(self, mock_run): - mock_run.side_effect = [ - CompletedProcess([], 1, b"", b""), # ipc show → no instance - CompletedProcess([], 0, b"", b"Launching config\n"), # launch ok - ] + mock_run.return_value = CompletedProcess([], 0, b"", b"Launching config\n") invoke("start") - assert mock_run.call_args_list == [ - call(["qs", "-c", "zshell", "ipc", "show"], capture_output=True), - call(["qs", "-c", "zshell", "-n", "-d"], capture_output=True), - ] + mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n", "-d"], capture_output=True) @patch("zshell.subcommands.shell.subprocess.run") def test_start_no_daemon(self, mock_run): - mock_run.side_effect = [ - CompletedProcess([], 1, b"", b""), - CompletedProcess([], 0, b"", b"Launching config\n"), - ] + mock_run.return_value = CompletedProcess([], 0, b"", b"Launching config\n") invoke("start", "--no-daemon") - assert mock_run.call_args_list == [ - call(["qs", "-c", "zshell", "ipc", "show"], capture_output=True), - call(["qs", "-c", "zshell", "-n"], capture_output=True), - ] + mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n"], capture_output=True) @patch("zshell.subcommands.shell.subprocess.run") def test_start_already_running_errors(self, mock_run): - mock_run.return_value = CompletedProcess([], 0, b"", b"target visibilities\n") + mock_run.return_value = CompletedProcess([], 0, b"An instance of this configuration is already running.\n", b"") result = runner.invoke(app, ["start"]) assert result.exit_code != 0 assert "already running" in result.output + @patch("zshell.subcommands.shell.subprocess.run") + def test_start_other_failure_errors(self, mock_run): + mock_run.return_value = CompletedProcess([], 1, b"", b"Config error\n") + result = runner.invoke(app, ["start"]) + assert result.exit_code != 0 + assert "Config error" in result.output + class TestShow: @patch("zshell.subcommands.shell.subprocess.run") @@ -106,30 +101,30 @@ class TestCall: class TestRestart: + @patch("zshell.subcommands.shell.start_instance") @patch("zshell.subcommands.shell.subprocess.run") - def test_restart_kills_then_starts(self, mock_run): + def test_restart_kills_then_starts(self, mock_run, mock_start): mock_run.side_effect = [ - CompletedProcess([], 0, b"", b"Killed abc\n"), # first kill (no capture) + CompletedProcess([], 0, b"", b"Killed abc\n"), # first kill (captured) CompletedProcess([], 255, b"", b""), # poll → no instance - CompletedProcess([], 0, b"", b"Launching config\n"), # launch ok ] invoke("restart") assert mock_run.call_args_list == [ - call(["qs", "-c", "zshell", "kill"]), # no capture_output call(["qs", "-c", "zshell", "kill"], capture_output=True), - call(["qs", "-c", "zshell", "-n", "-d"], capture_output=True), + call(["qs", "-c", "zshell", "kill"], capture_output=True), ] + mock_start.assert_called_once_with(no_daemon=False) + @patch("zshell.subcommands.shell.start_instance") @patch("zshell.subcommands.shell.subprocess.run") - def test_restart_no_daemon(self, mock_run): + def test_restart_no_daemon(self, mock_run, mock_start): mock_run.side_effect = [ CompletedProcess([], 0, b"", b"Killed abc\n"), CompletedProcess([], 255, b"", b""), - CompletedProcess([], 0, b"", b"Launching config\n"), ] invoke("restart", "--no-daemon") assert mock_run.call_args_list == [ - call(["qs", "-c", "zshell", "kill"]), call(["qs", "-c", "zshell", "kill"], capture_output=True), - call(["qs", "-c", "zshell", "-n"], capture_output=True), + call(["qs", "-c", "zshell", "kill"], capture_output=True), ] + mock_start.assert_called_once_with(no_daemon=True) From c30128cf95befc11a5deafa77b6c209f0bf56689 Mon Sep 17 00:00:00 2001 From: AramJonghu Date: Sun, 24 May 2026 18:03:32 +0200 Subject: [PATCH 5/5] check every 50ms -> 250ms for restart --- cli/src/zshell/subcommands/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/zshell/subcommands/shell.py b/cli/src/zshell/subcommands/shell.py index de8ca3b..4b30da7 100644 --- a/cli/src/zshell/subcommands/shell.py +++ b/cli/src/zshell/subcommands/shell.py @@ -42,7 +42,7 @@ def restart(no_daemon: bool = False): result = subprocess.run(args + ["kill"], capture_output=True) if result.returncode == 255: break - time.sleep(0.05) + time.sleep(0.25) start_instance(no_daemon=no_daemon)