@@ -61,7 +61,7 @@ def sleep(self, delay):
61
61
raise NotImplementedError ()
62
62
63
63
@abc .abstractmethod
64
- def fdopen (self , fd , mode , bufsize = 1 ):
64
+ def fdopen (self , fd , mode , bufsize = 1 , closefd = True ):
65
65
raise NotImplementedError ()
66
66
67
67
@abc .abstractmethod
@@ -113,10 +113,10 @@ def start(self, func, args=()):
113
113
114
114
return _thread .start_new_thread (func , args )
115
115
116
- def fdopen (self , fd , mode , bufsize = 1 ):
116
+ def fdopen (self , fd , mode , bufsize = 1 , closefd = True ):
117
117
import os
118
118
119
- return os .fdopen (fd , mode , bufsize , encoding = "utf-8" )
119
+ return os .fdopen (fd , mode , bufsize , encoding = "utf-8" , closefd = closefd )
120
120
121
121
def Lock (self ):
122
122
import threading
@@ -134,6 +134,10 @@ def Event(self):
134
134
return threading .Event ()
135
135
136
136
137
+ class MainThreadOnlyExecModel (ThreadExecModel ):
138
+ backend = "main_thread_only"
139
+
140
+
137
141
class EventletExecModel (ExecModel ):
138
142
backend = "eventlet"
139
143
@@ -170,10 +174,10 @@ def start(self, func, args=()):
170
174
171
175
return eventlet .spawn_n (func , * args )
172
176
173
- def fdopen (self , fd , mode , bufsize = 1 ):
177
+ def fdopen (self , fd , mode , bufsize = 1 , closefd = True ):
174
178
import eventlet .green .os
175
179
176
- return eventlet .green .os .fdopen (fd , mode , bufsize )
180
+ return eventlet .green .os .fdopen (fd , mode , bufsize , closefd = closefd )
177
181
178
182
def Lock (self ):
179
183
import eventlet .green .threading
@@ -227,11 +231,11 @@ def start(self, func, args=()):
227
231
228
232
return gevent .spawn (func , * args )
229
233
230
- def fdopen (self , fd , mode , bufsize = 1 ):
234
+ def fdopen (self , fd , mode , bufsize = 1 , closefd = True ):
231
235
# XXX
232
236
import gevent .fileobject
233
237
234
- return gevent .fileobject .FileObjectThread (fd , mode , bufsize )
238
+ return gevent .fileobject .FileObjectThread (fd , mode , bufsize , closefd = closefd )
235
239
236
240
def Lock (self ):
237
241
import gevent .lock
@@ -254,6 +258,8 @@ def get_execmodel(backend):
254
258
return backend
255
259
if backend == "thread" :
256
260
return ThreadExecModel ()
261
+ elif backend == "main_thread_only" :
262
+ return MainThreadOnlyExecModel ()
257
263
elif backend == "eventlet" :
258
264
return EventletExecModel ()
259
265
elif backend == "gevent" :
@@ -322,7 +328,7 @@ def __init__(self, execmodel, hasprimary=False):
322
328
self ._shuttingdown = False
323
329
self ._waitall_events = []
324
330
if hasprimary :
325
- if self .execmodel .backend != "thread" :
331
+ if self .execmodel .backend not in ( "thread" , "main_thread_only" ) :
326
332
raise ValueError ("hasprimary=True requires thread model" )
327
333
self ._primary_thread_task_ready = self .execmodel .Event ()
328
334
else :
@@ -332,7 +338,7 @@ def integrate_as_primary_thread(self):
332
338
"""integrate the thread with which we are called as a primary
333
339
thread for executing functions triggered with spawn().
334
340
"""
335
- assert self .execmodel .backend == "thread" , self .execmodel
341
+ assert self .execmodel .backend in ( "thread" , "main_thread_only" ) , self .execmodel
336
342
primary_thread_task_ready = self ._primary_thread_task_ready
337
343
# interacts with code at REF1
338
344
while 1 :
@@ -345,7 +351,11 @@ def integrate_as_primary_thread(self):
345
351
with self ._running_lock :
346
352
if self ._shuttingdown :
347
353
break
348
- primary_thread_task_ready .clear ()
354
+ # Only clear if _try_send_to_primary_thread has not
355
+ # yet set the next self._primary_thread_task reply
356
+ # after waiting for this one to complete.
357
+ if reply is self ._primary_thread_task :
358
+ primary_thread_task_ready .clear ()
349
359
350
360
def trigger_shutdown (self ):
351
361
with self ._running_lock :
@@ -376,6 +386,19 @@ def _try_send_to_primary_thread(self, reply):
376
386
# wake up primary thread
377
387
primary_thread_task_ready .set ()
378
388
return True
389
+ elif (
390
+ self .execmodel .backend == "main_thread_only"
391
+ and self ._primary_thread_task is not None
392
+ ):
393
+ self ._primary_thread_task .waitfinish ()
394
+ self ._primary_thread_task = reply
395
+ # wake up primary thread (it's okay if this is already set
396
+ # because we waited for the previous task to finish above
397
+ # and integrate_as_primary_thread will not clear it when
398
+ # it enters self._running_lock if it detects that a new
399
+ # task is available)
400
+ primary_thread_task_ready .set ()
401
+ return True
379
402
return False
380
403
381
404
def spawn (self , func , * args , ** kwargs ):
@@ -857,6 +880,9 @@ def reconfigure(self, py2str_as_py3str=True, py3str_as_py2str=False):
857
880
858
881
ENDMARKER = object ()
859
882
INTERRUPT_TEXT = "keyboard-interrupted"
883
+ MAIN_THREAD_ONLY_DEADLOCK_TEXT = (
884
+ "concurrent remote_exec would cause deadlock for main_thread_only execmodel"
885
+ )
860
886
861
887
862
888
class ChannelFactory :
@@ -1105,6 +1131,20 @@ def join(self, timeout=None):
1105
1131
1106
1132
class WorkerGateway (BaseGateway ):
1107
1133
def _local_schedulexec (self , channel , sourcetask ):
1134
+ if self ._execpool .execmodel .backend == "main_thread_only" :
1135
+ # It's necessary to wait for a short time in order to ensure
1136
+ # that we do not report a false-positive deadlock error, since
1137
+ # channel close does not elicit a response that would provide
1138
+ # a guarantee to remote_exec callers that the previous task
1139
+ # has released the main thread. If the timeout expires then it
1140
+ # should be practically impossible to report a false-positive.
1141
+ if not self ._executetask_complete .wait (timeout = 1 ):
1142
+ channel .close (MAIN_THREAD_ONLY_DEADLOCK_TEXT )
1143
+ return
1144
+ # It's only safe to clear here because the above wait proves
1145
+ # that there is not a previous task about to set it again.
1146
+ self ._executetask_complete .clear ()
1147
+
1108
1148
sourcetask = loads_internal (sourcetask )
1109
1149
self ._execpool .spawn (self .executetask , (channel , sourcetask ))
1110
1150
@@ -1132,8 +1172,14 @@ def serve(self):
1132
1172
def trace (msg ):
1133
1173
self ._trace ("[serve] " + msg )
1134
1174
1135
- hasprimary = self .execmodel .backend == "thread"
1175
+ hasprimary = self .execmodel .backend in ( "thread" , "main_thread_only" )
1136
1176
self ._execpool = WorkerPool (self .execmodel , hasprimary = hasprimary )
1177
+ self ._executetask_complete = None
1178
+ if self .execmodel .backend == "main_thread_only" :
1179
+ self ._executetask_complete = self .execmodel .Event ()
1180
+ # Initialize state to indicate that there is no previous task
1181
+ # executing so that we don't need a separate flag to track this.
1182
+ self ._executetask_complete .set ()
1137
1183
trace ("spawning receiver thread" )
1138
1184
self ._initreceive ()
1139
1185
try :
@@ -1176,6 +1222,11 @@ def executetask(self, item):
1176
1222
return
1177
1223
self ._trace ("ignoring EOFError because receiving finished" )
1178
1224
channel .close ()
1225
+ if self ._executetask_complete is not None :
1226
+ # Indicate that this task has finished executing, meaning
1227
+ # that there is no possibility of it triggering a deadlock
1228
+ # for the next spawn call.
1229
+ self ._executetask_complete .set ()
1179
1230
1180
1231
1181
1232
#
@@ -1631,8 +1682,10 @@ def init_popen_io(execmodel):
1631
1682
os .dup2 (fd , 2 )
1632
1683
os .close (fd )
1633
1684
io = Popen2IO (stdout , stdin , execmodel )
1634
- sys .stdin = execmodel .fdopen (0 , "r" , 1 )
1635
- sys .stdout = execmodel .fdopen (1 , "w" , 1 )
1685
+ # Use closefd=False since 0 and 1 are shared with
1686
+ # sys.__stdin__ and sys.__stdout__.
1687
+ sys .stdin = execmodel .fdopen (0 , "r" , 1 , closefd = False )
1688
+ sys .stdout = execmodel .fdopen (1 , "w" , 1 , closefd = False )
1636
1689
return io
1637
1690
1638
1691
0 commit comments