Skip to content

Commit 6de412f

Browse files
committed
Provide heuristics for the user to provide a library to set tracing to all threads. Fixes microsoft#617
The idea is that the user will be able to compile the target libraries/executables so that the features below work: - tracing to all the threads - attach to process Users will need to compile files in the expected location with the proper <arch> (where <arch> == platform.machine()). In Linux the following file is needed (see linux_and_mac/compile_linux.sh): attach_<arch>.so In Mac the following file is needed (see linux_and_mac/compile_mac.sh): attach_<arch>.dylib In Windows the following files are needed (see windows/compile_windows.bat): attach_<arch>.dll run_code_on_dllmain_<arch>.dll inject_dll_<arch>.exe Note: the actual compilation should use those compile scripts as a guide as different platforms may require different compiler flags.
1 parent 02477b5 commit 6de412f

File tree

2 files changed

+266
-88
lines changed

2 files changed

+266
-88
lines changed

src/debugpy/_vendored/pydevd/pydevd_attach_to_process/add_code_to_python_process.py

+164-67
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
import sys
7777
import time
7878
from contextlib import contextmanager
79+
import platform
80+
import traceback
7981

8082
try:
8183
TimeoutError = TimeoutError # @ReservedAssignment
@@ -122,13 +124,139 @@ def wait_for_event_set(self, timeout=None):
122124
CloseHandle(event)
123125

124126

127+
IS_WINDOWS = sys.platform == 'win32'
128+
IS_LINUX = sys.platform in ('linux', 'linux2')
129+
IS_MAC = sys.platform == 'darwin'
130+
131+
125132
def is_python_64bit():
126133
return (struct.calcsize('P') == 8)
127134

128135

129-
def is_mac():
130-
import platform
131-
return platform.system() == 'Darwin'
136+
def get_target_filename(is_target_process_64=None, prefix=None, extension=None):
137+
# Note: we have an independent (and similar -- but not equal) version of this method in
138+
# `pydevd_tracing.py` which should be kept synchronized with this one (we do a copy
139+
# because the `pydevd_attach_to_process` is mostly independent and shouldn't be imported in the
140+
# debugger -- the only situation where it's imported is if the user actually does an attach to
141+
# process, through `attach_pydevd.py`, but this should usually be called from the IDE directly
142+
# and not from the debugger).
143+
libdir = os.path.dirname(__file__)
144+
145+
if is_target_process_64 is None:
146+
if IS_WINDOWS:
147+
# i.e.: On windows the target process could have a different bitness (32bit is emulated on 64bit).
148+
raise AssertionError("On windows it's expected that the target bitness is specified.")
149+
150+
# For other platforms, just use the the same bitness of the process we're running in.
151+
is_target_process_64 = is_python_64bit()
152+
153+
arch = ''
154+
if IS_WINDOWS:
155+
# prefer not using platform.machine() when possible (it's a bit heavyweight as it may
156+
# spawn a subprocess).
157+
arch = os.environ.get("PROCESSOR_ARCHITEW6432", os.environ.get('PROCESSOR_ARCHITECTURE', ''))
158+
159+
if not arch:
160+
arch = platform.machine()
161+
if not arch:
162+
print('platform.machine() did not return valid value.') # This shouldn't happen...
163+
return None
164+
165+
if IS_WINDOWS:
166+
if not extension:
167+
extension = '.dll'
168+
suffix_64 = 'amd64'
169+
suffix_32 = 'x86'
170+
171+
elif IS_LINUX:
172+
if not extension:
173+
extension = '.so'
174+
suffix_64 = 'amd64'
175+
suffix_32 = 'x86'
176+
177+
elif IS_MAC:
178+
if not extension:
179+
extension = '.dylib'
180+
suffix_64 = 'x86_64'
181+
suffix_32 = 'x86'
182+
183+
else:
184+
print('Unable to attach to process in platform: %s', sys.platform)
185+
return None
186+
187+
if arch.lower() not in ('amd64', 'x86', 'x86_64', 'i386', 'x86'):
188+
# We don't support this processor by default. Still, let's support the case where the
189+
# user manually compiled it himself with some heuristics.
190+
#
191+
# Ideally the user would provide a library in the format: "attach_<arch>.<extension>"
192+
# based on the way it's currently compiled -- see:
193+
# - windows/compile_windows.bat
194+
# - linux_and_mac/compile_linux.sh
195+
# - linux_and_mac/compile_mac.sh
196+
197+
try:
198+
found = [name for name in os.listdir(libdir) if name.startswith('attach_') and name.endswith(extension)]
199+
except:
200+
print('Error listing dir: %s' % (libdir,))
201+
traceback.print_exc()
202+
return None
203+
204+
if prefix:
205+
expected_name = prefix + arch + extension
206+
expected_name_linux = prefix + 'linux_' + arch + extension
207+
else:
208+
# Default is looking for the attach_ / attach_linux
209+
expected_name = 'attach_' + arch + extension
210+
expected_name_linux = 'attach_linux_' + arch + extension
211+
212+
filename = None
213+
if expected_name in found: # Heuristic: user compiled with "attach_<arch>.<extension>"
214+
filename = os.path.join(libdir, expected_name)
215+
216+
elif IS_LINUX and expected_name_linux in found: # Heuristic: user compiled with "attach_linux_<arch>.<extension>"
217+
filename = os.path.join(libdir, expected_name_linux)
218+
219+
elif len(found) == 1: # Heuristic: user removed all libraries and just left his own lib.
220+
filename = os.path.join(libdir, found[0])
221+
222+
else: # Heuristic: there's one additional library which doesn't seem to be our own. Find the odd one.
223+
filtered = [name for name in found if not name.endswith((suffix_64 + extension, suffix_32 + extension))]
224+
if len(filtered) == 1: # If more than one is available we can't be sure...
225+
filename = os.path.join(libdir, found[0])
226+
227+
if filename is None:
228+
print(
229+
'Unable to attach to process in arch: %s (did not find %s in %s).' % (
230+
arch, expected_name, libdir
231+
)
232+
)
233+
return None
234+
235+
print('Using %s in arch: %s.' % (filename, arch))
236+
237+
else:
238+
if is_target_process_64:
239+
suffix = suffix_64
240+
else:
241+
suffix = suffix_32
242+
243+
if not prefix:
244+
# Default is looking for the attach_ / attach_linux
245+
if IS_WINDOWS or IS_MAC: # just the extension changes
246+
prefix = 'attach_'
247+
elif IS_LINUX:
248+
prefix = 'attach_linux_' # historically it has a different name
249+
else:
250+
print('Unable to attach to process in platform: %s' % (sys.platform,))
251+
return None
252+
253+
filename = os.path.join(libdir, '%s%s%s' % (prefix, suffix, extension))
254+
255+
if not os.path.exists(filename):
256+
print('Expected: %s to exist.' % (filename,))
257+
return None
258+
259+
return filename
132260

133261

134262
def run_python_code_windows(pid, python_code, connect_debugger_tracing=False, show_debug_info=0):
@@ -139,49 +267,41 @@ def run_python_code_windows(pid, python_code, connect_debugger_tracing=False, sh
139267

140268
process = Process(pid)
141269
bits = process.get_bits()
142-
is_64 = bits == 64
270+
is_target_process_64 = bits == 64
143271

144272
# Note: this restriction no longer applies (we create a process with the proper bitness from
145273
# this process so that the attach works).
146-
# if is_64 != is_python_64bit():
274+
# if is_target_process_64 != is_python_64bit():
147275
# raise RuntimeError("The architecture of the Python used to connect doesn't match the architecture of the target.\n"
148276
# "Target 64 bits: %s\n"
149-
# "Current Python 64 bits: %s" % (is_64, is_python_64bit()))
277+
# "Current Python 64 bits: %s" % (is_target_process_64, is_python_64bit()))
150278

151279
with _acquire_mutex('_pydevd_pid_attach_mutex_%s' % (pid,), 10):
152280
print('--- Connecting to %s bits target (current process is: %s) ---' % (bits, 64 if is_python_64bit() else 32))
153281

154282
with _win_write_to_shared_named_memory(python_code, pid):
155283

156-
filedir = os.path.dirname(__file__)
157-
if is_64:
158-
suffix = 'amd64'
159-
else:
160-
suffix = 'x86'
161-
162-
target_executable = os.path.join(filedir, 'inject_dll_%s.exe' % suffix)
163-
if not os.path.exists(target_executable):
164-
raise RuntimeError('Could not find exe file to inject: %s' % target_executable)
284+
target_executable = get_target_filename(is_target_process_64, 'inject_dll_', '.exe')
285+
if not target_executable:
286+
raise RuntimeError('Could not find expected .exe file to inject dll in attach to process.')
165287

166-
name = 'attach_%s.dll' % suffix
167-
target_dll = os.path.join(filedir, name)
168-
if not os.path.exists(target_dll):
169-
raise RuntimeError('Could not find dll file to inject: %s' % target_dll)
288+
target_dll = get_target_filename(is_target_process_64)
289+
if not target_dll:
290+
raise RuntimeError('Could not find expected .dll file in attach to process.')
170291

171-
print('\n--- Injecting attach dll: %s into pid: %s ---' % (name, pid))
292+
print('\n--- Injecting attach dll: %s into pid: %s ---' % (os.path.basename(target_dll), pid))
172293
args = [target_executable, str(pid), target_dll]
173294
subprocess.check_call(args)
174295

175296
# Now, if the first injection worked, go on to the second which will actually
176297
# run the code.
177-
name = 'run_code_on_dllmain_%s.dll' % suffix
178-
target_dll = os.path.join(filedir, name)
179-
if not os.path.exists(target_dll):
180-
raise RuntimeError('Could not find dll file to inject: %s' % target_dll)
298+
target_dll_run_on_dllmain = get_target_filename(is_target_process_64, 'run_code_on_dllmain_', '.dll')
299+
if not target_dll_run_on_dllmain:
300+
raise RuntimeError('Could not find expected .dll in attach to process.')
181301

182302
with _create_win_event('_pydevd_pid_event_%s' % (pid,)) as event:
183-
print('\n--- Injecting run code dll: %s into pid: %s ---' % (name, pid))
184-
args = [target_executable, str(pid), target_dll]
303+
print('\n--- Injecting run code dll: %s into pid: %s ---' % (os.path.basename(target_dll_run_on_dllmain), pid))
304+
args = [target_executable, str(pid), target_dll_run_on_dllmain]
185305
subprocess.check_call(args)
186306

187307
if not event.wait_for_event_set(10):
@@ -269,25 +389,10 @@ def _win_write_to_shared_named_memory(python_code, pid):
269389

270390
def run_python_code_linux(pid, python_code, connect_debugger_tracing=False, show_debug_info=0):
271391
assert '\'' not in python_code, 'Having a single quote messes with our command.'
272-
filedir = os.path.dirname(__file__)
273-
274-
# Valid arguments for arch are i386, i386:x86-64, i386:x64-32, i8086,
275-
# i386:intel, i386:x86-64:intel, i386:x64-32:intel, i386:nacl,
276-
# i386:x86-64:nacl, i386:x64-32:nacl, auto.
277-
278-
if is_python_64bit():
279-
suffix = 'amd64'
280-
arch = 'i386:x86-64'
281-
else:
282-
suffix = 'x86'
283-
arch = 'i386'
284392

285-
print('Attaching with arch: %s' % (arch,))
286-
287-
target_dll = os.path.join(filedir, 'attach_linux_%s.so' % suffix)
288-
target_dll = os.path.abspath(os.path.normpath(target_dll))
289-
if not os.path.exists(target_dll):
290-
raise RuntimeError('Could not find dll file to inject: %s' % target_dll)
393+
target_dll = get_target_filename()
394+
if not target_dll:
395+
raise RuntimeError('Could not find .so for attach to process.')
291396

292397
# Note: we currently don't support debug builds
293398
is_debug = 0
@@ -306,7 +411,9 @@ def run_python_code_linux(pid, python_code, connect_debugger_tracing=False, show
306411

307412
cmd.extend(["--eval-command='set scheduler-locking off'"]) # If on we'll deadlock.
308413

309-
cmd.extend(["--eval-command='set architecture %s'" % arch])
414+
# Leave auto by default (it should do the right thing as we're attaching to a process in the
415+
# current host).
416+
cmd.extend(["--eval-command='set architecture auto'"])
310417

311418
cmd.extend([
312419
"--eval-command='call (void*)dlopen(\"%s\", 2)'" % target_dll,
@@ -347,27 +454,13 @@ def find_helper_script(filedir, script_name):
347454

348455
def run_python_code_mac(pid, python_code, connect_debugger_tracing=False, show_debug_info=0):
349456
assert '\'' not in python_code, 'Having a single quote messes with our command.'
350-
filedir = os.path.dirname(__file__)
351-
352-
# Valid arguments for arch are i386, i386:x86-64, i386:x64-32, i8086,
353-
# i386:intel, i386:x86-64:intel, i386:x64-32:intel, i386:nacl,
354-
# i386:x86-64:nacl, i386:x64-32:nacl, auto.
355-
356-
if is_python_64bit():
357-
suffix = 'x86_64.dylib'
358-
arch = 'i386:x86-64'
359-
else:
360-
suffix = 'x86.dylib'
361-
arch = 'i386'
362457

363-
print('Attaching with arch: %s' % (arch,))
458+
target_dll = get_target_filename()
459+
if not target_dll:
460+
raise RuntimeError('Could not find .dylib for attach to process.')
364461

365-
target_dll = os.path.join(filedir, 'attach_%s' % suffix)
366-
target_dll = os.path.normpath(target_dll)
367-
if not os.path.exists(target_dll):
368-
raise RuntimeError('Could not find dll file to inject: %s' % target_dll)
369-
370-
lldb_prepare_file = find_helper_script(filedir, 'lldb_prepare.py')
462+
libdir = os.path.dirname(__file__)
463+
lldb_prepare_file = find_helper_script(libdir, 'lldb_prepare.py')
371464
# Note: we currently don't support debug builds
372465

373466
is_debug = 0
@@ -418,12 +511,16 @@ def run_python_code_mac(pid, python_code, connect_debugger_tracing=False, show_d
418511
return out, err
419512

420513

421-
if sys.platform == 'win32':
514+
if IS_WINDOWS:
422515
run_python_code = run_python_code_windows
423-
elif is_mac():
516+
elif IS_MAC:
424517
run_python_code = run_python_code_mac
425-
else:
518+
elif IS_LINUX:
426519
run_python_code = run_python_code_linux
520+
else:
521+
522+
def run_python_code(*args, **kwargs):
523+
print('Unable to attach to process in platform: %s', sys.platform)
427524

428525

429526
def test():

0 commit comments

Comments
 (0)