Skip to content

Commit d072c00

Browse files
committed
fix(core): handle intermittent signal interrupt test failures
Signed-off-by: Eshaan Agrawal <agrawaleshaan12@gmail.com>
1 parent 058f439 commit d072c00

1 file changed

Lines changed: 81 additions & 77 deletions

File tree

src/scancode/interrupt.py

Lines changed: 81 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,79 @@ class TimeoutError(Exception): # NOQA
5151
NO_ERROR = None
5252
NO_VALUE = None
5353

54+
from ctypes import c_long
55+
from ctypes import py_object
56+
from ctypes import pythonapi
57+
from multiprocessing import TimeoutError as MpTimeoutError
58+
59+
from queue import Empty as Queue_Empty
60+
from queue import Queue
61+
from _thread import start_new_thread
62+
63+
def async_raise(tid, exctype=Exception):
64+
"""
65+
Raise an Exception in the Thread with id `tid`. Perform cleanup if
66+
needed.
67+
68+
Based on Killable Threads By Tomer Filiba
69+
from http://tomerfiliba.com/recipes/Thread2/
70+
license: public domain.
71+
"""
72+
assert isinstance(tid, int), 'Invalid thread id: must an integer'
73+
74+
tid = c_long(tid)
75+
exception = py_object(Exception)
76+
res = pythonapi.PyThreadState_SetAsyncExc(tid, exception)
77+
if res == 0:
78+
raise ValueError('Invalid thread id.')
79+
elif res != 1:
80+
# if it returns a number greater than one, you're in trouble,
81+
# and you should call it again with exc=NULL to revert the effect
82+
pythonapi.PyThreadState_SetAsyncExc(tid, 0)
83+
raise SystemError('PyThreadState_SetAsyncExc failed.')
84+
85+
def thread_interruptible(func, args=None, kwargs=None, timeout=DEFAULT_TIMEOUT):
86+
"""
87+
Threads-based interruptible runner. It can work on both Windows and POSIX,
88+
but is not reliable and works only if everything is pickable.
89+
"""
90+
# We run `func` in a thread and block on a queue until timeout
91+
results = Queue()
92+
93+
def runner():
94+
"""
95+
Run the func and send results back in a queue as a tuple of
96+
(`error`, `value`)
97+
"""
98+
try:
99+
_res = func(*(args or ()), **(kwargs or {}))
100+
results.put((NO_ERROR, _res,))
101+
except Exception:
102+
results.put((ERROR_MSG + traceback_format_exc(), NO_VALUE,))
103+
104+
tid = start_new_thread(runner, ())
105+
106+
try:
107+
# wait for the queue results up to timeout
108+
err_res = results.get(timeout=timeout)
109+
110+
if not err_res:
111+
return ERROR_MSG, NO_VALUE
112+
113+
return err_res
114+
115+
except (Queue_Empty, MpTimeoutError):
116+
return TIMEOUT_MSG % locals(), NO_VALUE
117+
118+
except Exception:
119+
return ERROR_MSG + traceback_format_exc(), NO_VALUE
120+
121+
finally:
122+
try:
123+
async_raise(tid, Exception)
124+
except (SystemExit, ValueError):
125+
pass
126+
54127
if not on_windows:
55128
"""
56129
Some code based in part and inspired from the RobotFramework and
@@ -93,92 +166,23 @@ def handler(signum, frame):
93166
except TimeoutError:
94167
return TIMEOUT_MSG % locals(), NO_VALUE
95168

96-
except Exception:
169+
except ValueError as ve:
170+
if 'signal only works in main thread' in str(ve):
171+
# Fallback to the thread-based implementation if we are not in the main thread of the main interpreter
172+
return thread_interruptible(func, args, kwargs, timeout)
97173
return ERROR_MSG + traceback_format_exc(), NO_VALUE
98174

99-
finally:
100-
setitimer(ITIMER_REAL, 0)
101-
102-
elif on_windows:
103-
"""
104-
Run a function in an interruptible thread with a timeout.
105-
Based on an idea of dano "Dan O'Reilly"
106-
http://stackoverflow.com/users/2073595/dano
107-
But no code has been reused from this post.
108-
"""
109-
110-
from ctypes import c_long
111-
from ctypes import py_object
112-
from ctypes import pythonapi
113-
from multiprocessing import TimeoutError as MpTimeoutError
114-
115-
from queue import Empty as Queue_Empty
116-
from queue import Queue
117-
from _thread import start_new_thread
118-
119-
def interruptible(func, args=None, kwargs=None, timeout=DEFAULT_TIMEOUT):
120-
"""
121-
Windows, threads-based interruptible runner. It can work also on
122-
POSIX, but is not reliable and works only if everything is pickable.
123-
"""
124-
# We run `func` in a thread and block on a queue until timeout
125-
results = Queue()
126-
127-
def runner():
128-
"""
129-
Run the func and send results back in a queue as a tuple of
130-
(`error`, `value`)
131-
"""
132-
try:
133-
_res = func(*(args or ()), **(kwargs or {}))
134-
results.put((NO_ERROR, _res,))
135-
except Exception:
136-
results.put((ERROR_MSG + traceback_format_exc(), NO_VALUE,))
137-
138-
tid = start_new_thread(runner, ())
139-
140-
try:
141-
# wait for the queue results up to timeout
142-
err_res = results.get(timeout=timeout)
143-
144-
if not err_res:
145-
return ERROR_MSG, NO_VALUE
146-
147-
return err_res
148-
149-
except (Queue_Empty, MpTimeoutError):
150-
return TIMEOUT_MSG % locals(), NO_VALUE
151-
152175
except Exception:
153176
return ERROR_MSG + traceback_format_exc(), NO_VALUE
154177

155178
finally:
156179
try:
157-
async_raise(tid, Exception)
158-
except (SystemExit, ValueError):
180+
setitimer(ITIMER_REAL, 0)
181+
except ValueError:
159182
pass
160183

161-
def async_raise(tid, exctype=Exception):
162-
"""
163-
Raise an Exception in the Thread with id `tid`. Perform cleanup if
164-
needed.
165-
166-
Based on Killable Threads By Tomer Filiba
167-
from http://tomerfiliba.com/recipes/Thread2/
168-
license: public domain.
169-
"""
170-
assert isinstance(tid, int), 'Invalid thread id: must an integer'
171-
172-
tid = c_long(tid)
173-
exception = py_object(Exception)
174-
res = pythonapi.PyThreadState_SetAsyncExc(tid, exception)
175-
if res == 0:
176-
raise ValueError('Invalid thread id.')
177-
elif res != 1:
178-
# if it returns a number greater than one, you're in trouble,
179-
# and you should call it again with exc=NULL to revert the effect
180-
pythonapi.PyThreadState_SetAsyncExc(tid, 0)
181-
raise SystemError('PyThreadState_SetAsyncExc failed.')
184+
elif on_windows:
185+
interruptible = thread_interruptible
182186

183187

184188
def fake_interruptible(func, args=None, kwargs=None, timeout=DEFAULT_TIMEOUT):

0 commit comments

Comments
 (0)