Skip to content
This repository was archived by the owner on Sep 22, 2023. It is now read-only.

Commit e155f30

Browse files
authored
Fix Windows compatibility and interrupted exits on all platforms (#93)
* repo: Update gitignore * fix: Let asyncio_run_forever() rely on intrinsic interrupt mechanisms - To fix either hang-up or not-implemented-error for signal handlers, we need to use Python 3.8 for proper Windows + IOCP support. * fix: "backend.ai --version" has been broken... * fix(setup): Update trove classifiers and the CLI entrypoint. * fix: Properly set the exit code for interruptions of all commands - We need to handle this correctly because our Windows customer is going to use "backend.ai" CLI inside *batch scripts*. See https://bugs.python.org/issue1054041 for details!
1 parent 2a2f173 commit e155f30

File tree

6 files changed

+79
-21
lines changed

6 files changed

+79
-21
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ env_*.sh
3636
.ipynb_checkpoints
3737

3838
.vscode/
39+
.mypy_cache/

CHANGELOG.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Changes
22
=======
33

4+
19.09.7 (2020-03-31)
5+
--------------------
6+
7+
* FIX: Not-implemented-error in ``backend.ai app`` command on Windows, due
8+
to manually set event loop UNIX signal handlers. (#93)
9+
10+
* FIX: Now *all* CLI commands set exit codes correctly for interrupts
11+
(Ctrl+C on Windows or SIGINT on POSIX systems) so that batch/shell
12+
scripts that use ``backend.ai`` commands get interrupted properly.
13+
(#93)
14+
415
19.09.6 (2020-03-16)
516
--------------------
617

setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ def read_src_version():
7777
'Programming Language :: Python :: 3',
7878
'Programming Language :: Python :: 3.6',
7979
'Programming Language :: Python :: 3.7',
80+
'Programming Language :: Python :: 3.8',
8081
'Operating System :: POSIX',
8182
'Operating System :: MacOS :: MacOS X',
83+
'Operating System :: Windows',
8284
'Environment :: No Input/Output (Daemon)',
8385
'Topic :: Scientific/Engineering',
8486
'Topic :: Software Development',
@@ -97,7 +99,7 @@ def read_src_version():
9799
data_files=[],
98100
entry_points={
99101
'console_scripts': [
100-
'backend.ai = ai.backend.client.cli:main',
102+
'backend.ai = ai.backend.client.cli:run_main',
101103
'lcc = ai.backend.client.cli:run_alias',
102104
'lpython = ai.backend.client.cli:run_alias',
103105
],

src/ai/backend/client/cli/__init__.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
from pathlib import Path
2+
import os
3+
import signal
24
import sys
35

46
import click
7+
from click.exceptions import ClickException, Abort
58

9+
from .. import __version__
610
from ..config import APIConfig, set_config
711

812

@@ -85,7 +89,7 @@ def format_commands(self, ctx, formatter):
8589
@click.option('--skip-sslcert-validation',
8690
help='Skip SSL certificate validation for all API requests.',
8791
is_flag=True)
88-
@click.version_option()
92+
@click.version_option(version=__version__)
8993
def main(skip_sslcert_validation):
9094
"""
9195
Backend.AI command line interface.
@@ -109,7 +113,7 @@ def run_alias():
109113
sys.argv.insert(1, 'run')
110114
if help:
111115
sys.argv.append('--help')
112-
main.main(prog_name='backend.ai')
116+
run_main()
113117

114118

115119
def _attach_command():
@@ -118,3 +122,48 @@ def _attach_command():
118122

119123

120124
_attach_command()
125+
126+
127+
def run_main():
128+
try:
129+
_interrupted = False
130+
main.main(
131+
standalone_mode=False,
132+
prog_name='backend.ai',
133+
)
134+
except KeyboardInterrupt:
135+
# For interruptions outside the Click's exception handling block.
136+
print("Interrupted!", end="", file=sys.stderr)
137+
sys.stderr.flush()
138+
_interrupted = True
139+
except Abort as e:
140+
# Click wraps unhandled KeyboardInterrupt with a plain
141+
# sys.exit(1) call and prints "Aborted!" message
142+
# (which would look non-sense to users).
143+
# This is *NOT* what we want.
144+
# Instead of relying on Click, mark the _interrupted
145+
# flag to perform our own exit routines.
146+
if isinstance(e.__context__, KeyboardInterrupt):
147+
print("Interrupted!", end="", file=sys.stderr)
148+
sys.stderr.flush()
149+
_interrupted = True
150+
else:
151+
print("Aborted!", end="", file=sys.stderr)
152+
sys.stderr.flush()
153+
sys.exit(1)
154+
except ClickException as e:
155+
e.show()
156+
sys.exit(e.exit_code)
157+
finally:
158+
if _interrupted:
159+
# Override the exit code when it's interrupted,
160+
# referring https://github.com/python/cpython/pull/11862
161+
if sys.platform.startswith('win'):
162+
# Use STATUS_CONTROL_C_EXIT to notify cmd.exe
163+
# for interrupted exit
164+
sys.exit(-1073741510)
165+
else:
166+
# Use the default signal handler to set the exit
167+
# code properly for interruption.
168+
signal.signal(signal.SIGINT, signal.SIG_DFL)
169+
os.kill(0, signal.SIGINT)

src/ai/backend/client/cli/__main__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
from . import main
1+
from . import run_main
22

3-
main()
3+
4+
run_main()

src/ai/backend/client/compat.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ def _cancel_all_tasks(loop):
5555
})
5656

5757

58-
def _asyncio_run(coro, *, debug=False):
58+
def asyncio_run(coro, *, debug=False):
5959
loop = asyncio.new_event_loop()
60+
asyncio.set_event_loop(loop)
61+
loop.set_debug(debug)
6062
try:
61-
asyncio.set_event_loop(loop)
62-
loop.set_debug(debug)
6363
return loop.run_until_complete(coro)
6464
finally:
6565
try:
@@ -68,15 +68,10 @@ def _asyncio_run(coro, *, debug=False):
6868
loop.run_until_complete(loop.shutdown_asyncgens())
6969
finally:
7070
loop.stop()
71+
loop.close()
7172
asyncio.set_event_loop(None)
7273

7374

74-
if hasattr(asyncio, 'run'): # Python 3.7+
75-
asyncio_run = asyncio.run
76-
else:
77-
asyncio_run = _asyncio_run
78-
79-
8075
def asyncio_run_forever(setup_coro, shutdown_coro, *,
8176
stop_signals={signal.SIGINT}, debug=False):
8277
'''
@@ -87,17 +82,15 @@ def asyncio_run_forever(setup_coro, shutdown_coro, *,
8782
async def wait_for_stop():
8883
loop = current_loop()
8984
future = loop.create_future()
90-
for stop_sig in stop_signals:
91-
loop.add_signal_handler(stop_sig, future.set_result, stop_sig)
9285
try:
93-
recv_sig = await future
94-
finally:
95-
loop.remove_signal_handler(recv_sig)
86+
await future
87+
except asyncio.CancelledError:
88+
pass
9689

9790
loop = asyncio.new_event_loop()
91+
asyncio.set_event_loop(loop)
92+
loop.set_debug(debug)
9893
try:
99-
asyncio.set_event_loop(loop)
100-
loop.set_debug(debug)
10194
loop.run_until_complete(setup_coro)
10295
loop.run_until_complete(wait_for_stop())
10396
finally:
@@ -108,4 +101,5 @@ async def wait_for_stop():
108101
loop.run_until_complete(loop.shutdown_asyncgens())
109102
finally:
110103
loop.stop()
104+
loop.close()
111105
asyncio.set_event_loop(None)

0 commit comments

Comments
 (0)