Skip to content

Commit adf66c8

Browse files
ZDStudiosclaude
andcommitted
Stopping a program now stops the voice it started
A speaking block starts a helper of its own: Python asks PowerShell, or say, or espeak, to do the talking. Stop only killed the Python process, so on macOS and Linux the voice carried on, and clicking a block that was still speaking after fifteen seconds reported "I stopped it" while the sentence kept going. The green flag's stop button and the click-a-block runner now both start programs in their own process group (a new session on macOS and Linux, taskkill /T on Windows) and stop the whole tree. What the studio says happened is now what happened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ad170fb commit adf66c8

1 file changed

Lines changed: 60 additions & 27 deletions

File tree

scratchpy_studio.py

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import queue
3737
import re
3838
import shutil
39+
import signal
3940
import subprocess
4041
import sys
4142
import threading
@@ -3997,19 +3998,29 @@ def run_piece(self, anchor: Block, piece: Block):
39973998
label = describe_block(piece)
39983999

39994000
def worker():
4000-
kw = dict(capture_output=True, text=True, encoding="utf-8",
4001-
errors="replace", cwd=folder, timeout=PIECE_TIMEOUT,
4002-
input="")
4003-
if IS_WINDOWS:
4004-
kw["creationflags"] = NO_WINDOW
4001+
kw = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE,
4002+
stdin=subprocess.PIPE, text=True, encoding="utf-8",
4003+
errors="replace", cwd=folder)
4004+
kw.update(spawn_kwargs())
40054005
try:
4006-
res = subprocess.run(command, **kw)
4007-
out, err, code = res.stdout, res.stderr, res.returncode
4008-
except subprocess.TimeoutExpired as slow:
4009-
out = (slow.stdout or "")
4010-
if isinstance(out, bytes):
4011-
out = out.decode("utf-8", "replace")
4012-
err, code = "", -9
4006+
proc = subprocess.Popen(command, **kw)
4007+
except Exception as exc:
4008+
self.app.ui(lambda: self.piece_done(anchor, label, "",
4009+
str(exc), -1))
4010+
return
4011+
try:
4012+
out, err = proc.communicate(input="", timeout=PIECE_TIMEOUT)
4013+
code = proc.returncode
4014+
except subprocess.TimeoutExpired:
4015+
# a block that is still speaking or playing has children of
4016+
# its own, so the whole tree has to go - otherwise the voice
4017+
# carries on after we have said we stopped it
4018+
kill_tree(proc)
4019+
try:
4020+
out, err = proc.communicate(timeout=5)
4021+
except Exception:
4022+
out, err = "", ""
4023+
code = -9
40134024
except Exception as exc:
40144025
out, err, code = "", str(exc), -1
40154026
self.app.ui(lambda: self.piece_done(anchor, label, out, err, code))
@@ -5217,6 +5228,40 @@ def venv_folder_for(settings: Settings, project_folder: str) -> str:
52175228
NO_WINDOW = 0x08000000 if IS_WINDOWS else 0
52185229

52195230

5231+
def spawn_kwargs() -> dict:
5232+
"""Start a program so that it can be stopped along with its children.
5233+
5234+
A block that speaks or plays a sound starts a helper of its own, and
5235+
stopping only the Python process would leave the voice talking.
5236+
"""
5237+
if IS_WINDOWS:
5238+
return {"creationflags": NO_WINDOW}
5239+
return {"start_new_session": True}
5240+
5241+
5242+
def kill_tree(proc):
5243+
"""Stop a running program, and anything it started."""
5244+
if proc is None or proc.poll() is not None:
5245+
return
5246+
if IS_WINDOWS:
5247+
try:
5248+
subprocess.run(["taskkill", "/F", "/T", "/PID", str(proc.pid)],
5249+
capture_output=True, creationflags=NO_WINDOW)
5250+
return
5251+
except Exception:
5252+
pass
5253+
else:
5254+
try:
5255+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
5256+
return
5257+
except Exception:
5258+
pass
5259+
try:
5260+
proc.kill()
5261+
except Exception:
5262+
pass
5263+
5264+
52205265
def popen_kwargs() -> dict:
52215266
kw = dict(stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
52225267
stdin=subprocess.DEVNULL, text=True, encoding="utf-8",
@@ -6017,8 +6062,7 @@ def start(self, path: str, cwd: str):
60176062
kw = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE,
60186063
stdin=subprocess.PIPE, text=True, encoding="utf-8",
60196064
errors="replace", bufsize=1, cwd=cwd)
6020-
if IS_WINDOWS:
6021-
kw["creationflags"] = NO_WINDOW
6065+
kw.update(spawn_kwargs())
60226066
env = dict(os.environ)
60236067
env["PYTHONUNBUFFERED"] = "1"
60246068
env["PYTHONIOENCODING"] = "utf-8"
@@ -6070,19 +6114,8 @@ def send(self, text: str):
60706114
def stop(self):
60716115
if not self.running:
60726116
return
6073-
try:
6074-
if IS_WINDOWS:
6075-
subprocess.run(["taskkill", "/F", "/T", "/PID",
6076-
str(self.proc.pid)],
6077-
capture_output=True,
6078-
creationflags=NO_WINDOW)
6079-
else:
6080-
self.proc.terminate()
6081-
except Exception:
6082-
try:
6083-
self.proc.kill()
6084-
except Exception:
6085-
pass
6117+
# the whole tree, so a voice or a sound it started stops as well
6118+
kill_tree(self.proc)
60866119
self.q.put(("sys", "--- stopped ---"))
60876120

60886121

0 commit comments

Comments
 (0)