Skip to content

Commit 96d6190

Browse files
committed
Kill all spawned processes on exit
Fixes #832, fixes #833.
1 parent aac8e0b commit 96d6190

File tree

7 files changed

+58
-11
lines changed

7 files changed

+58
-11
lines changed

cwltool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111
from cwltool import main
1212

1313
if __name__ == "__main__":
14-
sys.exit(main.main(sys.argv[1:]))
14+
main.run(sys.argv[1:])

cwltool/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
from . import main
66

7-
sys.exit(main.main())
7+
main.run()

cwltool/job.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from .utils import bytes2str_in_dicts # pylint: disable=unused-import
3232
from .utils import ( # pylint: disable=unused-import
3333
DEFAULT_TMP_PREFIX, Directory, copytree_with_merge, json_dump, json_dumps,
34-
onWindows, subprocess)
34+
onWindows, subprocess, processes_to_kill)
3535
from .context import (RuntimeContext, # pylint: disable=unused-import
3636
getdefault)
3737
if TYPE_CHECKING:
@@ -521,6 +521,7 @@ def _job_popen(
521521
stderr=stderr,
522522
env=env,
523523
cwd=cwd)
524+
processes_to_kill.append(sproc)
524525

525526
if sproc.stdin:
526527
sproc.stdin.close()
@@ -589,6 +590,7 @@ def terminate():
589590
stderr=sys.stderr,
590591
stdin=subprocess.PIPE,
591592
)
593+
processes_to_kill.append(sproc)
592594
if sproc.stdin:
593595
sproc.stdin.close()
594596

cwltool/main.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
import io
1111
import logging
1212
import os
13+
import signal
1314
import sys
1415

1516
from typing import (IO, Any, Callable, Dict, # pylint: disable=unused-import
16-
Iterable, List, Mapping, MutableMapping, Optional, Text,
17-
TextIO, Tuple, Union, cast)
17+
Deque, Iterable, List, Mapping, MutableMapping, Optional,
18+
Text, TextIO, Tuple, Union, cast)
1819
import pkg_resources # part of setuptools
1920
import ruamel.yaml as yaml
2021
import schema_salad.validate as validate
@@ -47,10 +48,41 @@
4748
from .stdfsaccess import StdFsAccess
4849
from .update import ALLUPDATES, UPDATES
4950
from .utils import (DEFAULT_TMP_PREFIX, add_sizes, json_dumps, onWindows,
50-
versionstring, windows_default_container_id)
51+
versionstring, windows_default_container_id,
52+
processes_to_kill)
5153
from .context import LoadingContext, RuntimeContext, getdefault
5254
from .builder import HasReqsHints
5355

56+
57+
def _terminate_processes():
58+
"""Kill all spawned processes.
59+
60+
Processes to be killed must be appended to `utils.processes_to_kill`
61+
as they are spawned.
62+
63+
An important caveat: since there's no supported way to kill another
64+
thread in Python, this function cannot stop other threads from
65+
continuing to execute while it kills the processes that they've
66+
spawned. This may occasionally lead to unexpected behaviour.
67+
"""
68+
# It's possible that another thread will spawn a new task while
69+
# we're executing, so it's not safe to use a for loop here.
70+
while processes_to_kill:
71+
processes_to_kill.popleft().kill()
72+
73+
74+
def _signal_handler(signum, frame):
75+
"""Kill all spawned processes and exit.
76+
77+
Note that it's possible for another thread to spawn a process after
78+
all processes have been killed, but before Python exits.
79+
80+
Refer to the docstring for _terminate_processes() for other caveats.
81+
"""
82+
_terminate_processes()
83+
sys.exit(signum)
84+
85+
5486
def generate_example_input(inptype):
5587
# type: (Union[Text, Dict[Text, Any]]) -> Any
5688
defaults = {u'null': 'null',
@@ -680,5 +712,14 @@ def find_default_container(builder, # type: HasReqsHints
680712
return default_container
681713

682714

715+
def run(*args, **kwargs):
716+
"""Run cwltool."""
717+
signal.signal(signal.SIGTERM, _signal_handler)
718+
try:
719+
sys.exit(main(*args, **kwargs))
720+
finally:
721+
_terminate_processes()
722+
723+
683724
if __name__ == "__main__":
684-
sys.exit(main(sys.argv[1:]))
725+
run(sys.argv[1:])

cwltool/sandboxjs.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import six
1515
from pkg_resources import resource_stream
1616

17-
from .utils import json_dumps, onWindows, subprocess
17+
from .utils import json_dumps, onWindows, subprocess, processes_to_kill
1818

1919
try:
2020
import queue # type: ignore
@@ -71,7 +71,7 @@ def new_js_proc(js_text, force_docker_pull=False):
7171
stdin=subprocess.PIPE,
7272
stdout=subprocess.PIPE,
7373
stderr=subprocess.PIPE)
74-
74+
processes_to_kill.append(nodejs)
7575
required_node_version = check_js_threshold_version(n)
7676
break
7777
except (subprocess.CalledProcessError, OSError):
@@ -95,6 +95,7 @@ def new_js_proc(js_text, force_docker_pull=False):
9595
"--sig-proxy=true", "--interactive",
9696
"--rm", nodeimg, "node", "--eval", js_text],
9797
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
98+
processes_to_kill.append(nodejs)
9899
docker = True
99100
except OSError as e:
100101
if e.errno == errno.ENOENT:

cwltool/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import absolute_import
22

3+
import collections
34
import json
45
import os
56
import sys
@@ -9,7 +10,7 @@
910
import pkg_resources
1011
from functools import partial # pylint: disable=unused-import
1112
from typing import (IO, Any, AnyStr, Callable, # pylint: disable=unused-import
12-
Dict, Iterable, List, Optional, Text, Tuple, TypeVar,
13+
Deque, Dict, Iterable, List, Optional, Text, Tuple, TypeVar,
1314
Union)
1415

1516
import six
@@ -32,6 +33,8 @@
3233

3334
DEFAULT_TMP_PREFIX = "tmp"
3435

36+
processes_to_kill = collections.deque() # type: Deque[subprocess.Popen]
37+
3538
def versionstring():
3639
# type: () -> Text
3740
'''

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
test_suite='tests',
7373
tests_require=['pytest', 'mock >= 2.0.0', 'arcp >= 0.2.0', 'rdflib-jsonld >= 0.4.0'],
7474
entry_points={
75-
'console_scripts': ["cwltool=cwltool.main:main"]
75+
'console_scripts': ["cwltool=cwltool.main:run"]
7676
},
7777
zip_safe=True,
7878
cmdclass={'egg_info': tagger},

0 commit comments

Comments
 (0)