Skip to content

Commit b0ffcf2

Browse files
authored
Merge pull request #438 from kevin-bates/port-terminal-culling
Port terminal culling from Notebook
2 parents 1b7d11d + 4f5f691 commit b0ffcf2

File tree

8 files changed

+294
-51
lines changed

8 files changed

+294
-51
lines changed

docs/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ dependencies:
1313
- sphinxcontrib_github_alt
1414
- sphinxcontrib-openapi
1515
- sphinxemoji
16+
- terminado
1617
- git+https://github.com/pandas-dev/pydata-sphinx-theme.git@master

jupyter_server/serverapp.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@
116116
from jupyter_server.extension.config import ExtensionConfigManager
117117
from jupyter_server.traittypes import TypeFromClasses
118118

119+
# Tolerate missing terminado package.
120+
try:
121+
from .terminal import TerminalManager
122+
terminado_available = True
123+
except ImportError:
124+
terminado_available = False
125+
119126
#-----------------------------------------------------------------------------
120127
# Module globals
121128
#-----------------------------------------------------------------------------
@@ -284,7 +291,7 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager,
284291
allow_password_change=jupyter_app.allow_password_change,
285292
server_root_dir=root_dir,
286293
jinja2_env=env,
287-
terminals_available=False, # Set later if terminals are available
294+
terminals_available=terminado_available and jupyter_app.terminals_enabled,
288295
serverapp=jupyter_app
289296
)
290297

@@ -589,6 +596,8 @@ class ServerApp(JupyterApp):
589596
ContentsManager, FileContentsManager, AsyncContentsManager, AsyncFileContentsManager, NotebookNotary,
590597
GatewayKernelManager, GatewayKernelSpecManager, GatewaySessionManager, GatewayClient
591598
]
599+
if terminado_available: # Only necessary when terminado is available
600+
classes.append(TerminalManager)
592601

593602
subcommands = dict(
594603
list=(JupyterServerListApp, JupyterServerListApp.description.splitlines()[0]),
@@ -1329,6 +1338,15 @@ def _update_server_extensions(self, change):
13291338
is not available.
13301339
"""))
13311340

1341+
# Since use of terminals is also a function of whether the terminado package is
1342+
# available, this variable holds the "final indication" of whether terminal functionality
1343+
# should be considered (particularly during shutdown/cleanup). It is enabled only
1344+
# once both the terminals "service" can be initialized and terminals_enabled is True.
1345+
# Note: this variable is slightly different from 'terminals_available' in the web settings
1346+
# in that this variable *could* remain false if terminado is available, yet the terminal
1347+
# service's initialization still fails. As a result, this variable holds the truth.
1348+
terminals_available = False
1349+
13321350
authenticate_prometheus = Bool(
13331351
True,
13341352
help=""""
@@ -1547,7 +1565,7 @@ def init_terminals(self):
15471565
try:
15481566
from .terminal import initialize
15491567
initialize(self.web_app, self.root_dir, self.connection_url, self.terminado_settings)
1550-
self.web_app.settings['terminals_available'] = True
1568+
self.terminals_available = True
15511569
except ImportError as e:
15521570
self.log.warning(_i18n("Terminals not available (error was %s)"), e)
15531571

@@ -1693,11 +1711,8 @@ def shutdown_no_activity(self):
16931711
if len(km) != 0:
16941712
return # Kernels still running
16951713

1696-
try:
1714+
if self.terminals_available:
16971715
term_mgr = self.web_app.settings['terminal_manager']
1698-
except KeyError:
1699-
pass # Terminals not enabled
1700-
else:
17011716
if term_mgr.terminals:
17021717
return # Terminals still running
17031718

@@ -1846,6 +1861,21 @@ def cleanup_kernels(self):
18461861
self.log.info(kernel_msg % n_kernels)
18471862
run_sync(self.kernel_manager.shutdown_all())
18481863

1864+
def cleanup_terminals(self):
1865+
"""Shutdown all terminals.
1866+
1867+
The terminals will shutdown themselves when this process no longer exists,
1868+
but explicit shutdown allows the TerminalManager to cleanup.
1869+
"""
1870+
if not self.terminals_available:
1871+
return
1872+
1873+
terminal_manager = self.web_app.settings['terminal_manager']
1874+
n_terminals = len(terminal_manager.list())
1875+
terminal_msg = trans.ngettext('Shutting down %d terminal', 'Shutting down %d terminals', n_terminals)
1876+
self.log.info(terminal_msg % n_terminals)
1877+
run_sync(terminal_manager.terminate_all())
1878+
18491879
def running_server_info(self, kernel_count=True):
18501880
"Return the current working directory and the server url information"
18511881
info = self.contents_manager.info_string() + "\n"
@@ -2076,6 +2106,7 @@ def _cleanup(self):
20762106
self.remove_server_info_file()
20772107
self.remove_browser_open_files()
20782108
self.cleanup_kernels()
2109+
self.cleanup_terminals()
20792110

20802111
def start_ioloop(self):
20812112
"""Start the IO Loop."""

jupyter_server/services/api/api.yaml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ paths:
568568
schema:
569569
type: array
570570
items:
571-
$ref: '#/definitions/Terminal_ID'
571+
$ref: '#/definitions/Terminal'
572572
403:
573573
description: Forbidden to access
574574
404:
@@ -582,7 +582,7 @@ paths:
582582
200:
583583
description: Succesfully created a new terminal
584584
schema:
585-
$ref: '#/definitions/Terminal_ID'
585+
$ref: '#/definitions/Terminal'
586586
403:
587587
description: Forbidden to access
588588
404:
@@ -599,7 +599,7 @@ paths:
599599
200:
600600
description: Terminal session with given id
601601
schema:
602-
$ref: '#/definitions/Terminal_ID'
602+
$ref: '#/definitions/Terminal'
603603
403:
604604
description: Forbidden to access
605605
404:
@@ -845,12 +845,18 @@ definitions:
845845
type: string
846846
description: Last modified timestamp
847847
format: dateTime
848-
Terminal_ID:
849-
description: A Terminal_ID object
848+
Terminal:
849+
description: A Terminal object
850850
type: object
851851
required:
852852
- name
853853
properties:
854854
name:
855855
type: string
856-
description: name of terminal ID
856+
description: name of terminal
857+
last_activity:
858+
type: string
859+
description: |
860+
ISO 8601 timestamp for the last-seen activity on this terminal. Use
861+
this to identify which terminals have been inactive since a given time.
862+
Timestamps will be UTC, indicated 'Z' suffix.

jupyter_server/terminal/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88
raise ImportError("terminado >= 0.8.3 required, found %s" % terminado.__version__)
99

1010
from ipython_genutils.py3compat import which
11-
from terminado import NamedTermManager
12-
from tornado.log import app_log
1311
from jupyter_server.utils import url_path_join as ujoin
1412
from . import api_handlers
1513
from .handlers import TermSocket
14+
from .terminalmanager import TerminalManager
1615

1716

1817
def initialize(webapp, root_dir, connection_url, settings):
@@ -33,13 +32,14 @@ def initialize(webapp, root_dir, connection_url, settings):
3332
# the user has specifically set a preferred shell command.
3433
if os.name != 'nt' and shell_override is None and not sys.stdout.isatty():
3534
shell.append('-l')
36-
terminal_manager = webapp.settings['terminal_manager'] = NamedTermManager(
35+
terminal_manager = webapp.settings['terminal_manager'] = TerminalManager(
3736
shell_command=shell,
3837
extra_env={'JUPYTER_SERVER_ROOT': root_dir,
3938
'JUPYTER_SERVER_URL': connection_url,
4039
},
40+
parent=webapp.settings['serverapp'],
4141
)
42-
terminal_manager.log = app_log
42+
terminal_manager.log = webapp.settings['serverapp'].log
4343
base_url = webapp.settings['base_url']
4444
handlers = [
4545
(ujoin(base_url, r"/terminals/websocket/(\w+)"), TermSocket,
Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,34 @@
11
import json
22
from tornado import web
33
from ..base.handlers import APIHandler
4-
from ..prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL
5-
64

75

86
class TerminalRootHandler(APIHandler):
97

108
@web.authenticated
119
def get(self):
12-
tm = self.terminal_manager
13-
terms = [{'name': name} for name in tm.terminals]
14-
self.finish(json.dumps(terms))
15-
16-
# Update the metric below to the length of the list 'terms'
17-
TERMINAL_CURRENTLY_RUNNING_TOTAL.set(
18-
len(terms)
19-
)
10+
models = self.terminal_manager.list()
11+
self.finish(json.dumps(models))
2012

2113
@web.authenticated
2214
def post(self):
2315
"""POST /terminals creates a new terminal and redirects to it"""
2416
data = self.get_json_body() or {}
2517

26-
name, _ = self.terminal_manager.new_named_terminal(**data)
27-
self.finish(json.dumps({'name': name}))
28-
29-
# Increase the metric by one because a new terminal was created
30-
TERMINAL_CURRENTLY_RUNNING_TOTAL.inc()
18+
model = self.terminal_manager.create(**data)
19+
self.finish(json.dumps(model))
3120

3221

3322
class TerminalHandler(APIHandler):
3423
SUPPORTED_METHODS = ('GET', 'DELETE')
3524

3625
@web.authenticated
3726
def get(self, name):
38-
tm = self.terminal_manager
39-
if name in tm.terminals:
40-
self.finish(json.dumps({'name': name}))
41-
else:
42-
raise web.HTTPError(404, "Terminal not found: %r" % name)
27+
model = self.terminal_manager.get(name)
28+
self.finish(json.dumps(model))
4329

4430
@web.authenticated
4531
async def delete(self, name):
46-
tm = self.terminal_manager
47-
if name in tm.terminals:
48-
await tm.terminate(name, force=True)
49-
self.set_status(204)
50-
self.finish()
51-
52-
# Decrease the metric below by one
53-
# because a terminal has been shutdown
54-
TERMINAL_CURRENTLY_RUNNING_TOTAL.dec()
55-
56-
else:
57-
raise web.HTTPError(404, "Terminal not found: %r" % name)
32+
await self.terminal_manager.terminate(name, force=True)
33+
self.set_status(204)
34+
self.finish()

jupyter_server/terminal/handlers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,14 @@ def get(self, *args, **kwargs):
2626

2727
def on_message(self, message):
2828
super(TermSocket, self).on_message(message)
29-
self.application.settings['terminal_last_activity'] = utcnow()
29+
self._update_activity()
3030

3131
def write_message(self, message, binary=False):
3232
super(TermSocket, self).write_message(message, binary=binary)
33-
self.application.settings['terminal_last_activity'] = utcnow()
33+
self._update_activity()
34+
35+
def _update_activity(self):
36+
self.application.settings['terminal_last_activity'] = utcnow()
37+
# terminal may not be around on deletion/cull
38+
if self.term_name in self.terminal_manager.terminals:
39+
self.terminal_manager.terminals[self.term_name].last_activity = utcnow()

0 commit comments

Comments
 (0)