Skip to content

Commit 9e69d7b

Browse files
Fook MeFook Me
authored andcommitted
Advance Welcome installers only after worker thread termination
1 parent 463ebbb commit 9e69d7b

2 files changed

Lines changed: 55 additions & 9 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Exercise real Qt thread completion, including a delayed return from run()."""
2+
import ast,os,pathlib,signal,subprocess,sys,threading,time,unittest
3+
try:
4+
from PyQt6.QtCore import QCoreApplication,QThread,pyqtSignal,pyqtSlot
5+
except ImportError:
6+
raise unittest.SkipTest("PyQt6 is required for real worker lifetime tests")
7+
SOURCE=pathlib.Path(__file__).resolve().parents[3]/'packages/shadowfetch-welcome/src/shadowfetch-welcome'
8+
nodes=[n for n in ast.parse(SOURCE.read_text()).body if isinstance(n,ast.ClassDef) and n.name in ('CommandWorker','StreamingCommandWorker')]
9+
namespace=dict(globals(),strip_terminal_escapes=lambda s:s)
10+
exec(compile(ast.Module(body=nodes,type_ignores=[]),str(SOURCE),'exec'),namespace)
11+
class LifetimeTests(unittest.TestCase):
12+
def test_completion_only_after_thread_termination(self):
13+
app=QCoreApplication.instance() or QCoreApplication([])
14+
for name in ['CommandWorker','StreamingCommandWorker']:
15+
for exit_code in [0,7]:
16+
with self.subTest(worker=name,exit_code=exit_code):
17+
base=namespace[name]
18+
class DelayedReturn(base):
19+
def run(self):
20+
super().run()
21+
time.sleep(.08)
22+
command=[sys.executable,'-c','raise SystemExit('+str(exit_code)+')']
23+
worker=DelayedReturn([command] if name=='CommandWorker' else command)
24+
results=[]
25+
event=getattr(worker,'completed',worker.finished)
26+
event.connect(lambda code,w=worker:results.append((code,w.isRunning())))
27+
worker.start();deadline=time.monotonic()+5
28+
while (not results or worker.isRunning()) and time.monotonic()<deadline:
29+
app.processEvents();time.sleep(.001)
30+
worker.wait(1000);app.processEvents()
31+
self.assertEqual([(exit_code,False)],results)
32+
if __name__=='__main__':unittest.main()

packages/shadowfetch-welcome/src/shadowfetch-welcome

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -638,15 +638,22 @@ def hwscan_has_nvidia(scan: dict) -> bool:
638638
class CommandWorker(QThread):
639639
"""Run cancellable argv commands off the UI thread."""
640640
progress = pyqtSignal(str)
641-
finished = pyqtSignal(int)
641+
completed = pyqtSignal(int)
642642

643643
def __init__(self, commands):
644644
super().__init__()
645645
self.commands = commands
646646
self.process = None
647+
self.returncode = 1
648+
self.finished.connect(self._publish_result)
647649
self.cancel_requested = False
648650
self.cancel_escalation_started = False
649651

652+
@pyqtSlot()
653+
def _publish_result(self):
654+
# QThread.finished runs after run() returns; advancing setup is now safe.
655+
self.completed.emit(self.returncode)
656+
650657
def cancel(self):
651658
self.cancel_requested = True
652659
process = self.process
@@ -713,21 +720,28 @@ class CommandWorker(QThread):
713720
self.progress.emit(f" ! {e}")
714721
finally:
715722
self.process = None
716-
self.finished.emit(rc)
723+
self.returncode = rc
717724

718725

719726
class StreamingCommandWorker(QThread):
720727
"""Run one long command and stream its combined output to the UI."""
721728
progress = pyqtSignal(str)
722-
finished = pyqtSignal(int)
729+
completed = pyqtSignal(int)
723730

724731
def __init__(self, command):
725732
super().__init__()
726733
self.command = command
727734
self.process = None
735+
self.returncode = 1
736+
self.finished.connect(self._publish_result)
728737
self.cancel_requested = False
729738
self.cancel_escalation_started = False
730739

740+
@pyqtSlot()
741+
def _publish_result(self):
742+
# QThread.finished runs after run() returns; advancing setup is now safe.
743+
self.completed.emit(self.returncode)
744+
731745
def cancel(self):
732746
self.cancel_requested = True
733747
process = self.process
@@ -774,7 +788,7 @@ class StreamingCommandWorker(QThread):
774788
self._begin_cancel(process)
775789
except OSError as error:
776790
self.progress.emit(f"Could not start setup: {error}")
777-
self.finished.emit(1)
791+
self.returncode = 1
778792
return
779793
assert process.stdout is not None
780794
for line in iter(process.stdout.readline, ""):
@@ -784,7 +798,7 @@ class StreamingCommandWorker(QThread):
784798
process.stdout.close()
785799
code = process.wait()
786800
self.process = None
787-
self.finished.emit(130 if self.cancel_requested else code)
801+
self.returncode = 130 if self.cancel_requested else code
788802

789803

790804
class Card(QFrame):
@@ -1184,7 +1198,7 @@ class InstallPanel(QWidget):
11841198
"system back atomically and asks for a reboot when done.")
11851199
self._restore_worker = CommandWorker(
11861200
[["pkexec", PHOENIX_RESTORE, str(self.point)]])
1187-
self._restore_worker.finished.connect(self._restored)
1201+
self._restore_worker.completed.connect(self._restored)
11881202
self._restore_worker.start()
11891203

11901204
def _restored(self, rc):
@@ -1612,7 +1626,7 @@ class IgnitionFlow(QWidget):
16121626

16131627
def _state_call(self, args, on_done):
16141628
self._state_worker = CommandWorker([["pkexec", STATE_HELPER] + args])
1615-
self._state_worker.finished.connect(on_done)
1629+
self._state_worker.completed.connect(on_done)
16161630
self._state_worker.start()
16171631

16181632
# -- result --
@@ -2685,7 +2699,7 @@ class InstallPage(QWidget):
26852699

26862700
self.worker = CommandWorker(commands)
26872701
self.worker.progress.connect(self.log.setText)
2688-
self.worker.finished.connect(self._on_done)
2702+
self.worker.completed.connect(self._on_done)
26892703
self.worker.start()
26902704

26912705
def _on_done(self, rc):
@@ -2758,7 +2772,7 @@ class InstallPage(QWidget):
27582772
command.extend(["setup", "--yes", "--no-open"])
27592773
self.coding_agent_worker = StreamingCommandWorker(command)
27602774
self.coding_agent_worker.progress.connect(self._coding_agent_progress)
2761-
self.coding_agent_worker.finished.connect(self._coding_agent_done)
2775+
self.coding_agent_worker.completed.connect(self._coding_agent_done)
27622776
self.coding_agent_worker.start()
27632777

27642778
def _coding_agent_progress(self, line):

0 commit comments

Comments
 (0)