17
17
"""
18
18
import pwd
19
19
import os
20
+ import asyncio
20
21
import re
21
22
22
23
import xml .etree .ElementTree as ET
@@ -183,56 +184,58 @@ def parse_job_id(self, output):
183
184
def cmd_formatted_for_batch (self ):
184
185
return ' ' .join (['batchspawner-singleuser' ] + self .cmd + self .get_args ())
185
186
186
- @gen .coroutine
187
- def run_command (self , cmd , input = None , env = None ):
188
- proc = Subprocess (cmd , shell = True , env = env , stdin = Subprocess .STREAM , stdout = Subprocess .STREAM ,stderr = Subprocess .STREAM )
189
- inbytes = None
187
+ async def run_command (self , cmd , input = None , env = None ):
188
+ proc = await asyncio .create_subprocess_shell (cmd , env = env ,
189
+ stdin = asyncio .subprocess .PIPE ,
190
+ stdout = asyncio .subprocess .PIPE ,
191
+ stderr = asyncio .subprocess .PIPE )
192
+ inbytes = None
193
+
190
194
if input :
191
- inbytes = input .encode ()
192
- try :
193
- yield proc .stdin .write (inbytes )
194
- except StreamClosedError as exp :
195
- # Apparently harmless
196
- pass
197
- proc .stdin .close ()
198
- out , eout = yield [proc .stdout .read_until_close (),
199
- proc .stderr .read_until_close ()]
200
- proc .stdout .close ()
201
- proc .stderr .close ()
202
- eout = eout .decode ().strip ()
195
+ inbytes = input .encode ()
196
+
203
197
try :
204
- err = yield proc .wait_for_exit ()
205
- except CalledProcessError :
198
+ out , eout = await proc .communicate (input = inbytes )
199
+ except :
200
+ self .log .debug ("Exception raised when trying to run command: %s" % command )
201
+ proc .kill ()
202
+ self .log .debug ("Running command failed done kill" )
203
+ out , eout = await proc .communicate ()
204
+ out = out .decode .strip ()
205
+ eout = eout .decode .strip ()
206
206
self .log .error ("Subprocess returned exitcode %s" % proc .returncode )
207
207
self .log .error ('Stdout:' )
208
208
self .log .error (out )
209
209
self .log .error ('Stderr:' )
210
210
self .log .error (eout )
211
211
raise RuntimeError ('{} exit status {}: {}' .format (cmd , proc .returncode , eout ))
212
- if err != 0 :
213
- return err # exit error?
214
212
else :
215
- out = out .decode ().strip ()
216
- return out
217
-
218
- @gen .coroutine
219
- def _get_batch_script (self , ** subvars ):
213
+ eout = eout .decode ().strip ()
214
+ err = proc .returncode
215
+ if err != 0 :
216
+ self .log .error ("Subprocess returned exitcode %s" % err )
217
+ self .log .error (eout )
218
+ raise RuntimeError (eout )
219
+
220
+ out = out .decode ().strip ()
221
+ return out
222
+
223
+ async def _get_batch_script (self , ** subvars ):
220
224
"""Format batch script from vars"""
221
- # Colud be overridden by subclasses, but mainly useful for testing
225
+ # Could be overridden by subclasses, but mainly useful for testing
222
226
return format_template (self .batch_script , ** subvars )
223
227
224
- @gen .coroutine
225
- def submit_batch_script (self ):
228
+ async def submit_batch_script (self ):
226
229
subvars = self .get_req_subvars ()
227
230
cmd = ' ' .join ((format_template (self .exec_prefix , ** subvars ),
228
231
format_template (self .batch_submit_cmd , ** subvars )))
229
232
subvars ['cmd' ] = self .cmd_formatted_for_batch ()
230
233
if hasattr (self , 'user_options' ):
231
234
subvars .update (self .user_options )
232
- script = yield self ._get_batch_script (** subvars )
235
+ script = await self ._get_batch_script (** subvars )
233
236
self .log .info ('Spawner submitting job using ' + cmd )
234
237
self .log .info ('Spawner submitted script:\n ' + script )
235
- out = yield self .run_command (cmd , input = script , env = self .get_env ())
238
+ out = await self .run_command (cmd , input = script , env = self .get_env ())
236
239
try :
237
240
self .log .info ('Job submitted. cmd: ' + cmd + ' output: ' + out )
238
241
self .job_id = self .parse_job_id (out )
@@ -247,8 +250,7 @@ def submit_batch_script(self):
247
250
"and self.job_id as {job_id}."
248
251
).tag (config = True )
249
252
250
- @gen .coroutine
251
- def read_job_state (self ):
253
+ async def read_job_state (self ):
252
254
if self .job_id is None or len (self .job_id ) == 0 :
253
255
# job not running
254
256
self .job_status = ''
@@ -259,7 +261,7 @@ def read_job_state(self):
259
261
format_template (self .batch_query_cmd , ** subvars )))
260
262
self .log .debug ('Spawner querying job: ' + cmd )
261
263
try :
262
- out = yield self .run_command (cmd , env = self . get_env () )
264
+ out = await self .run_command (cmd )
263
265
self .job_status = out
264
266
except Exception as e :
265
267
self .log .error ('Error querying job ' + self .job_id )
@@ -271,14 +273,13 @@ def read_job_state(self):
271
273
help = "Command to stop/cancel a previously submitted job. Formatted like batch_query_cmd."
272
274
).tag (config = True )
273
275
274
- @gen .coroutine
275
- def cancel_batch_job (self ):
276
+ async def cancel_batch_job (self ):
276
277
subvars = self .get_req_subvars ()
277
278
subvars ['job_id' ] = self .job_id
278
279
cmd = ' ' .join ((format_template (self .exec_prefix , ** subvars ),
279
280
format_template (self .batch_cancel_cmd , ** subvars )))
280
281
self .log .info ('Cancelling job ' + self .job_id + ': ' + cmd )
281
- yield self .run_command (cmd , env = self . get_env () )
282
+ await self .run_command (cmd )
282
283
283
284
def load_state (self , state ):
284
285
"""load job_id from state"""
@@ -317,11 +318,10 @@ def state_gethost(self):
317
318
"Return string, hostname or addr of running job, likely by parsing self.job_status"
318
319
raise NotImplementedError ("Subclass must provide implementation" )
319
320
320
- @gen .coroutine
321
- def poll (self ):
321
+ async def poll (self ):
322
322
"""Poll the process"""
323
323
if self .job_id is not None and len (self .job_id ) > 0 :
324
- yield self .read_job_state ()
324
+ await self .read_job_state ()
325
325
if self .state_isrunning () or self .state_ispending ():
326
326
return None
327
327
else :
@@ -337,16 +337,15 @@ def poll(self):
337
337
help = "Polling interval (seconds) to check job state during startup"
338
338
).tag (config = True )
339
339
340
- @gen .coroutine
341
- def start (self ):
340
+ async def start (self ):
342
341
"""Start the process"""
343
342
self .ip = self .traits ()['ip' ].default_value
344
343
self .port = self .traits ()['port' ].default_value
345
344
346
345
if jupyterhub .version_info >= (0 ,8 ) and self .server :
347
346
self .server .port = self .port
348
347
349
- job = yield self .submit_batch_script ()
348
+ job = await self .submit_batch_script ()
350
349
351
350
# We are called with a timeout, and if the timeout expires this function will
352
351
# be interrupted at the next yield, and self.stop() will be called.
@@ -355,7 +354,7 @@ def start(self):
355
354
if len (self .job_id ) == 0 :
356
355
raise RuntimeError ("Jupyter batch job submission failure (no jobid in output)" )
357
356
while True :
358
- yield self .poll ()
357
+ await self .poll ()
359
358
if self .state_isrunning ():
360
359
break
361
360
else :
@@ -367,11 +366,11 @@ def start(self):
367
366
raise RuntimeError ('The Jupyter batch job has disappeared'
368
367
' while pending in the queue or died immediately'
369
368
' after starting.' )
370
- yield gen .sleep (self .startup_poll_interval )
369
+ await gen .sleep (self .startup_poll_interval )
371
370
372
371
self .ip = self .state_gethost ()
373
372
while self .port == 0 :
374
- yield gen .sleep (self .startup_poll_interval )
373
+ await gen .sleep (self .startup_poll_interval )
375
374
# Test framework: For testing, mock_port is set because we
376
375
# don't actually run the single-user server yet.
377
376
if hasattr (self , 'mock_port' ):
@@ -388,22 +387,21 @@ def start(self):
388
387
389
388
return self .ip , self .port
390
389
391
- @gen .coroutine
392
- def stop (self , now = False ):
390
+ async def stop (self , now = False ):
393
391
"""Stop the singleuser server job.
394
392
395
393
Returns immediately after sending job cancellation command if now=True, otherwise
396
394
tries to confirm that job is no longer running."""
397
395
398
396
self .log .info ("Stopping server job " + self .job_id )
399
- yield self .cancel_batch_job ()
397
+ await self .cancel_batch_job ()
400
398
if now :
401
399
return
402
400
for i in range (10 ):
403
- yield self .poll ()
401
+ await self .poll ()
404
402
if not self .state_isrunning ():
405
403
return
406
- yield gen .sleep (1.0 )
404
+ await gen .sleep (1.0 )
407
405
if self .job_id :
408
406
self .log .warn ("Notebook server job {0} at {1}:{2} possibly failed to terminate" .format (
409
407
self .job_id , self .ip , self .port )
0 commit comments