Skip to content

Commit fe664f9

Browse files
committed
fixup! Merge branch 'main' into tachyon-file-does-not-exist
1 parent 7fbb890 commit fe664f9

File tree

3 files changed

+92
-90
lines changed

3 files changed

+92
-90
lines changed

Lib/profiling/sampling/_sync_coordinator.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
the startup of target processes. It should not be called directly by users.
66
"""
77

8+
import importlib.util
89
import os
910
import sys
1011
import socket
@@ -223,13 +224,14 @@ def main() -> NoReturn:
223224
module_name = target_args[1]
224225
module_args = target_args[2:]
225226

226-
import importlib.util
227227
if importlib.util.find_spec(module_name) is None:
228228
raise TargetError(f"Module not found: {module_name}")
229229
else:
230230
script_path = target_args[0]
231231
script_args = target_args[1:]
232-
if not os.path.isfile(os.path.join(cwd, script_path)):
232+
# Match the path resolution logic in _execute_script
233+
check_path = script_path if os.path.isabs(script_path) else os.path.join(cwd, script_path)
234+
if not os.path.isfile(check_path):
233235
raise TargetError(f"Script not found: {script_path}")
234236

235237
# Signal readiness to profiler

Lib/profiling/sampling/cli.py

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import argparse
44
import os
5-
import select
5+
import selectors
66
import socket
77
import subprocess
88
import sys
@@ -94,6 +94,54 @@ def _parse_mode(mode_string):
9494
return mode_map[mode_string]
9595

9696

97+
def _check_process_died(process):
98+
"""Check if process died and raise an error with stderr if available."""
99+
if process.poll() is None:
100+
return # Process still running
101+
102+
# Process died - try to get stderr for error message
103+
stderr_msg = ""
104+
if process.stderr:
105+
try:
106+
stderr_msg = process.stderr.read().decode().strip()
107+
except (OSError, UnicodeDecodeError):
108+
pass
109+
110+
if stderr_msg:
111+
raise RuntimeError(stderr_msg)
112+
raise RuntimeError(f"Process exited with code {process.returncode}")
113+
114+
115+
def _wait_for_ready_signal(sync_sock, process, timeout):
116+
"""Wait for the ready signal from the subprocess, checking for early death."""
117+
deadline = time.monotonic() + timeout
118+
sel = selectors.DefaultSelector()
119+
sel.register(sync_sock, selectors.EVENT_READ)
120+
121+
try:
122+
while True:
123+
_check_process_died(process)
124+
125+
remaining = deadline - time.monotonic()
126+
if remaining <= 0:
127+
raise socket.timeout("timed out")
128+
129+
if not sel.select(timeout=min(0.1, remaining)):
130+
continue
131+
132+
conn, _ = sync_sock.accept()
133+
try:
134+
ready_signal = conn.recv(_RECV_BUFFER_SIZE)
135+
finally:
136+
conn.close()
137+
138+
if ready_signal != _READY_MESSAGE:
139+
raise RuntimeError(f"Invalid ready signal received: {ready_signal!r}")
140+
return
141+
finally:
142+
sel.close()
143+
144+
97145
def _run_with_sync(original_cmd, suppress_output=False):
98146
"""Run a command with socket-based synchronization and return the process."""
99147
# Create a TCP socket for synchronization with better socket options
@@ -119,49 +167,19 @@ def _run_with_sync(original_cmd, suppress_output=False):
119167
) + tuple(target_args)
120168

121169
# Start the process with coordinator
122-
# Suppress stdout if requested (for live mode), but capture stderr
123-
# so we can report errors if the process fails to start
124-
popen_kwargs = {}
170+
# Suppress stdout/stderr if requested (for live mode)
171+
popen_kwargs = {"stderr": subprocess.PIPE}
125172
if suppress_output:
126173
popen_kwargs["stdin"] = subprocess.DEVNULL
127174
popen_kwargs["stdout"] = subprocess.DEVNULL
128-
popen_kwargs["stderr"] = subprocess.PIPE
129175

130176
process = subprocess.Popen(cmd, **popen_kwargs)
131177

132178
try:
133-
# Wait for ready signal, but also check if process dies early
134-
deadline = time.monotonic() + _SYNC_TIMEOUT
135-
while True:
136-
# Check if process died
137-
if process.poll() is not None:
138-
stderr_msg = ""
139-
if process.stderr:
140-
try:
141-
stderr_msg = process.stderr.read().decode().strip()
142-
except (OSError, UnicodeDecodeError):
143-
pass
144-
if stderr_msg:
145-
raise RuntimeError(stderr_msg)
146-
raise RuntimeError(
147-
f"Process exited with code {process.returncode}"
148-
)
149-
150-
# Check remaining timeout
151-
remaining = deadline - time.monotonic()
152-
if remaining <= 0:
153-
raise socket.timeout("timed out")
154-
155-
# Wait for socket with short timeout to allow process checks
156-
ready, _, _ = select.select([sync_sock], [], [], min(0.1, remaining))
157-
if ready:
158-
with sync_sock.accept()[0] as conn:
159-
ready_signal = conn.recv(_RECV_BUFFER_SIZE)
160-
if ready_signal != _READY_MESSAGE:
161-
raise RuntimeError(
162-
f"Invalid ready signal received: {ready_signal!r}"
163-
)
164-
break
179+
_wait_for_ready_signal(sync_sock, process, _SYNC_TIMEOUT)
180+
181+
if process.stderr:
182+
process.stderr.close()
165183

166184
except socket.timeout:
167185
# If we timeout, kill the process and raise an error
@@ -671,7 +689,10 @@ def _handle_run(args):
671689
cmd = (sys.executable, args.target, *args.args)
672690

673691
# Run with synchronization
674-
process = _run_with_sync(cmd, suppress_output=False)
692+
try:
693+
process = _run_with_sync(cmd, suppress_output=False)
694+
except RuntimeError as e:
695+
sys.exit(f"Error: {e}")
675696

676697
# Use PROFILING_MODE_ALL for gecko format
677698
mode = (
@@ -718,14 +739,6 @@ def _handle_run(args):
718739

719740
def _handle_live_attach(args, pid):
720741
"""Handle live mode for an existing process."""
721-
# Check if process exists
722-
try:
723-
os.kill(pid, 0)
724-
except ProcessLookupError:
725-
sys.exit(f"Error: process not found: {pid}")
726-
except PermissionError:
727-
pass # Process exists, permission error will be handled later
728-
729742
mode = _parse_mode(args.mode)
730743

731744
# Determine skip_idle based on mode
@@ -767,8 +780,10 @@ def _handle_live_run(args):
767780
cmd = (sys.executable, args.target, *args.args)
768781

769782
# Run with synchronization, suppressing output for live mode
770-
# Note: _run_with_sync will raise if the process dies before signaling ready
771-
process = _run_with_sync(cmd, suppress_output=True)
783+
try:
784+
process = _run_with_sync(cmd, suppress_output=True)
785+
except RuntimeError as e:
786+
sys.exit(f"Error: {e}")
772787

773788
mode = _parse_mode(args.mode)
774789

0 commit comments

Comments
 (0)