66import os
77import shlex
88import subprocess
9+ import sys
910import tarfile
1011import tempfile
1112from io import BytesIO
@@ -378,6 +379,7 @@ def load_task(
378379 remove : bool = True ,
379380 user : Optional [str ] = None ,
380381 custom_image : Optional [str ] = None ,
382+ interactive : Optional [bool ] = True ,
381383) -> int :
382384 """Load and run a task interactively in a Docker container.
383385
@@ -392,14 +394,12 @@ def load_task(
392394 remove: Whether to remove the container after exit (default True).
393395 user: The user to switch to in the container (default 'worker').
394396 custom_image: A custom image to use instead of the task's image.
397+ interactive: If True, execution of the task will be paused and user
398+ will be dropped into a shell. They can run `exec-task` to resume
399+ it (default: True).
395400
396401 Returns:
397402 int: The exit code from the Docker container.
398-
399- Note:
400- Only supports tasks that use 'run-task' and have a payload.image.
401- The task's actual command is made available via an 'exec-task' function
402- in the interactive shell.
403403 """
404404 user = user or "worker"
405405 if isinstance (task , str ):
@@ -419,9 +419,9 @@ def load_task(
419419
420420 return 1
421421
422- command = task_def ["payload" ].get ("command" ) # type: ignore
423- if not command or not command [0 ].endswith ("run-task" ):
424- logger .error ("Only tasks using `run-task` are supported!" )
422+ task_command = task_def ["payload" ].get ("command" ) # type: ignore
423+ if interactive and ( not task_command or not task_command [0 ].endswith ("run-task" ) ):
424+ logger .error ("Only tasks using `run-task` are supported with interactive !" )
425425 return 1
426426
427427 try :
@@ -431,33 +431,40 @@ def load_task(
431431 logger .exception (e )
432432 return 1
433433
434- # Remove the payload section of the task's command. This way run-task will
435- # set up the task (clone repos, download fetches, etc) but won't actually
436- # start the core of the task. Instead we'll drop the user into an interactive
437- # shell and provide the ability to resume the task command.
438- task_command = None
439- if index := _index (command , "--" ):
440- task_command = shlex .join (command [index + 1 :])
441- # I attempted to run the interactive bash shell here, but for some
442- # reason when executed through `run-task`, the interactive shell
443- # doesn't work well. There's no shell prompt on newlines and tab
444- # completion doesn't work. That's why it is executed outside of
445- # `run-task` below, and why we need to parse `--task-cwd`.
446- command [index + 1 :] = [
447- "echo" ,
448- "Task setup complete!\n Run `exec-task` to execute the task's command." ,
449- ]
450-
451- # Parse `--task-cwd` so we know where to execute the task's command later.
452- if index := _index (command , "--task-cwd" ):
453- task_cwd = command [index + 1 ]
454- else :
455- for arg in command :
456- if arg .startswith ("--task-cwd=" ):
457- task_cwd = arg .split ("=" , 1 )[1 ]
458- break
434+ exec_command = task_cwd = None
435+ if interactive :
436+ # Remove the payload section of the task's command. This way run-task will
437+ # set up the task (clone repos, download fetches, etc) but won't actually
438+ # start the core of the task. Instead we'll drop the user into an interactive
439+ # shell and provide the ability to resume the task command.
440+ if index := _index (task_command , "--" ):
441+ exec_command = shlex .join (task_command [index + 1 :])
442+ # I attempted to run the interactive bash shell here, but for some
443+ # reason when executed through `run-task`, the interactive shell
444+ # doesn't work well. There's no shell prompt on newlines and tab
445+ # completion doesn't work. That's why it is executed outside of
446+ # `run-task` below, and why we need to parse `--task-cwd`.
447+ task_command [index + 1 :] = [
448+ "echo" ,
449+ "Task setup complete!\n Run `exec-task` to execute the task's command." ,
450+ ]
451+
452+ # Parse `--task-cwd` so we know where to execute the task's command later.
453+ if index := _index (task_command , "--task-cwd" ):
454+ task_cwd = task_command [index + 1 ]
459455 else :
460- task_cwd = "$TASK_WORKDIR"
456+ for arg in task_command :
457+ if arg .startswith ("--task-cwd=" ):
458+ task_cwd = arg .split ("=" , 1 )[1 ]
459+ break
460+ else :
461+ task_cwd = "$TASK_WORKDIR"
462+
463+ task_command = [
464+ "bash" ,
465+ "-c" ,
466+ f"{ shlex .join (task_command )} && cd $TASK_WORKDIR && su -p { user } " ,
467+ ]
461468
462469 # Set some env vars the worker would normally set.
463470 env = {
@@ -476,17 +483,21 @@ def load_task(
476483
477484 envfile = None
478485 initfile = None
486+ isatty = os .isatty (sys .stdin .fileno ())
479487 try :
480488 command = [
481489 "docker" ,
482490 "run" ,
483- "-it" ,
484- image_tag ,
485- "bash" ,
486- "-c" ,
487- f"{ shlex .join (command )} && cd $TASK_WORKDIR && su -p { user } " ,
491+ "-i" ,
488492 ]
489493
494+ if isatty :
495+ command .append ("-t" )
496+
497+ command .append (image_tag )
498+
499+ if task_command :
500+ command .extend (task_command )
490501 if remove :
491502 command .insert (2 , "--rm" )
492503
@@ -497,16 +508,16 @@ def load_task(
497508
498509 command .insert (2 , f"--env-file={ envfile .name } " )
499510
500- if task_command :
511+ if exec_command :
501512 initfile = tempfile .NamedTemporaryFile ("w+" , delete = False )
502513 os .fchmod (initfile .fileno (), 0o644 )
503514 initfile .write (
504515 dedent (
505516 f"""
506517 function exec-task() {{
507- echo Starting task: { shlex .quote (task_command )}
518+ echo Starting task: { shlex .quote (exec_command )}
508519 pushd { task_cwd }
509- { task_command }
520+ { exec_command }
510521 popd
511522 }}
512523 """
@@ -516,6 +527,7 @@ def load_task(
516527
517528 command [2 :2 ] = ["-v" , f"{ initfile .name } :/builds/worker/.bashrc" ]
518529
530+ logger .info (f"Running: { ' ' .join (command )} " )
519531 proc = subprocess .run (command )
520532 finally :
521533 if envfile :
0 commit comments