Skip to content

Commit 64bfcea

Browse files
Zsailerblink1073
andauthored
Remove terminals in favor of jupyter_server_terminals extension (#651)
Co-authored-by: Steven Silvester <[email protected]>
1 parent 64555ff commit 64bfcea

File tree

13 files changed

+91
-419
lines changed

13 files changed

+91
-419
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ repos:
4444
hooks:
4545
- id: doc8
4646
args: [--max-line-length=200]
47+
exclude: docs/source/other/full-config.rst
4748
stages: [manual]
4849

4950
- repo: https://github.com/pycqa/flake8

jupyter_server/extension/application.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,10 @@ def start(self):
429429
# Start the server.
430430
self.serverapp.start()
431431

432+
def current_activity(self):
433+
"""Return a list of activity happening in this extension."""
434+
return
435+
432436
async def stop_extension(self):
433437
"""Cleanup any resources managed by this extension."""
434438

jupyter_server/extension/handler.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ class ExtensionHandlerMixin:
2828
other extensions.
2929
"""
3030

31-
def initialize(self, name):
31+
def initialize(self, name, *args, **kwargs):
3232
self.name = name
33+
try:
34+
super().initialize(*args, **kwargs)
35+
except TypeError:
36+
pass
3337

3438
@property
3539
def extensionapp(self):

jupyter_server/extension/manager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,9 @@ async def stop_all_extensions(self):
394394
for name, apps in sorted(dict(self.extension_apps).items())
395395
]
396396
)
397+
398+
def any_activity(self):
399+
"""Check for any activity currently happening across all extension applications."""
400+
for _, app in sorted(dict(self.extension_apps).items()):
401+
if app.current_activity():
402+
return True

jupyter_server/pytest_plugin.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ def jp_environ(
127127
@pytest.fixture
128128
def jp_server_config():
129129
"""Allows tests to setup their specific configuration values."""
130-
return {}
130+
return Config(
131+
{
132+
"jpserver_extensions": {"jupyter_server_terminals": True},
133+
}
134+
)
131135

132136

133137
@pytest.fixture
@@ -225,6 +229,13 @@ def my_test(jp_configurable_serverapp):
225229
"""
226230
ServerApp.clear_instance()
227231

232+
# Inject jupyter_server_terminals into config unless it was
233+
# explicitly put in config.
234+
serverapp_config = jp_server_config.setdefault("ServerApp", {})
235+
exts = serverapp_config.setdefault("jpserver_extensions", {})
236+
if "jupyter_server_terminals" not in exts:
237+
exts["jupyter_server_terminals"] = True
238+
228239
def _configurable_serverapp(
229240
config=jp_server_config,
230241
base_url=jp_base_url,
@@ -473,7 +484,12 @@ def jp_cleanup_subprocesses(jp_serverapp):
473484
"""Clean up subprocesses started by a Jupyter Server, i.e. kernels and terminal."""
474485

475486
async def _():
476-
terminal_cleanup = jp_serverapp.web_app.settings["terminal_manager"].terminate_all
487+
term_manager = jp_serverapp.web_app.settings.get("terminal_manager")
488+
if term_manager:
489+
terminal_cleanup = term_manager.terminate_all
490+
else:
491+
terminal_cleanup = lambda: None # noqa
492+
477493
kernel_cleanup = jp_serverapp.kernel_manager.shutdown_all
478494

479495
async def kernel_cleanup_steps():
@@ -496,7 +512,7 @@ async def kernel_cleanup_steps():
496512
print(e)
497513
else:
498514
try:
499-
await terminal_cleanup()
515+
terminal_cleanup()
500516
except Exception as e:
501517
print(e)
502518
if asyncio.iscoroutinefunction(kernel_cleanup):

jupyter_server/serverapp.py

Lines changed: 17 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,6 @@
134134
urlencode_unix_socket_path,
135135
)
136136

137-
# Tolerate missing terminado package.
138-
try:
139-
from jupyter_server.terminal import TerminalManager
140-
141-
terminado_available = True
142-
except ImportError:
143-
terminado_available = False
144-
145137
# -----------------------------------------------------------------------------
146138
# Module globals
147139
# -----------------------------------------------------------------------------
@@ -292,7 +284,7 @@ def init_settings(
292284
env.install_gettext_translations(nbui, newstyle=False)
293285

294286
if sys_info["commit_source"] == "repository":
295-
# don't cache (rely on 304) when working from default branch
287+
# don't cache (rely on 304) when working from master
296288
version_hash = ""
297289
else:
298290
# reset the cache on server restart
@@ -361,7 +353,6 @@ def init_settings(
361353
allow_password_change=jupyter_app.allow_password_change,
362354
server_root_dir=root_dir,
363355
jinja2_env=env,
364-
terminals_available=terminado_available and jupyter_app.terminals_enabled,
365356
serverapp=jupyter_app,
366357
)
367358

@@ -454,14 +445,12 @@ def last_activity(self):
454445
self.settings["started"],
455446
self.settings["kernel_manager"].last_kernel_activity,
456447
]
457-
try:
458-
sources.append(self.settings["api_last_activity"])
459-
except KeyError:
460-
pass
461-
try:
462-
sources.append(self.settings["terminal_last_activity"])
463-
except KeyError:
464-
pass
448+
# Any setting that ends with a key that ends with `_last_activity` is
449+
# counted here. This provides a hook for extensions to add a last activity
450+
# setting to the server.
451+
sources.extend(
452+
[key for key, val in self.settings.items() if key.endswith("_last_activity")]
453+
)
465454
sources.extend(self.settings["last_activity_times"].values())
466455
return max(sources)
467456

@@ -744,8 +733,6 @@ class ServerApp(JupyterApp):
744733
GatewayClient,
745734
Authorizer,
746735
]
747-
if terminado_available: # Only necessary when terminado is available
748-
classes.append(TerminalManager)
749736

750737
subcommands = dict(
751738
list=(JupyterServerListApp, JupyterServerListApp.description.splitlines()[0]),
@@ -1713,8 +1700,8 @@ def _update_server_extensions(self, change):
17131700
0,
17141701
config=True,
17151702
help=(
1716-
"Shut down the server after N seconds with no kernels or "
1717-
"terminals running and no activity. "
1703+
"Shut down the server after N seconds with no kernels"
1704+
"running and no activity. "
17181705
"This can be used together with culling idle kernels "
17191706
"(MappingKernelManager.cull_idle_timeout) to "
17201707
"shutdown the Jupyter server when it's not in use. This is not "
@@ -1724,7 +1711,6 @@ def _update_server_extensions(self, change):
17241711
)
17251712

17261713
terminals_enabled = Bool(
1727-
True,
17281714
config=True,
17291715
help=_i18n(
17301716
"""Set to False to disable terminals.
@@ -1738,14 +1724,10 @@ def _update_server_extensions(self, change):
17381724
),
17391725
)
17401726

1741-
# Since use of terminals is also a function of whether the terminado package is
1742-
# available, this variable holds the "final indication" of whether terminal functionality
1743-
# should be considered (particularly during shutdown/cleanup). It is enabled only
1744-
# once both the terminals "service" can be initialized and terminals_enabled is True.
1745-
# Note: this variable is slightly different from 'terminals_available' in the web settings
1746-
# in that this variable *could* remain false if terminado is available, yet the terminal
1747-
# service's initialization still fails. As a result, this variable holds the truth.
1748-
terminals_available = False
1727+
@default("terminals_enabled")
1728+
def _default_terminals_enabled(self):
1729+
1730+
return True
17491731

17501732
authenticate_prometheus = Bool(
17511733
True,
@@ -2032,23 +2014,6 @@ def connection_url(self):
20322014
urlparts = self._get_urlparts(path=self.base_url)
20332015
return urlparts.geturl()
20342016

2035-
def init_terminals(self):
2036-
if not self.terminals_enabled:
2037-
return
2038-
2039-
try:
2040-
from jupyter_server.terminal import initialize
2041-
2042-
initialize(
2043-
self.web_app,
2044-
self.root_dir,
2045-
self.connection_url,
2046-
self.terminado_settings,
2047-
)
2048-
self.terminals_available = True
2049-
except ImportError as e:
2050-
self.log.warning(_i18n("Terminals not available (error was %s)"), e)
2051-
20522017
def init_signal(self):
20532018
if not sys.platform.startswith("win") and sys.stdin and sys.stdin.isatty():
20542019
signal.signal(signal.SIGINT, self._handle_sigint)
@@ -2194,24 +2159,22 @@ def shutdown_no_activity(self):
21942159
if len(km) != 0:
21952160
return # Kernels still running
21962161

2197-
if self.terminals_available:
2198-
term_mgr = self.web_app.settings["terminal_manager"]
2199-
if term_mgr.terminals:
2200-
return # Terminals still running
2162+
if self.extension_manager.any_activity:
2163+
return
22012164

22022165
seconds_since_active = (utcnow() - self.web_app.last_activity()).total_seconds()
22032166
self.log.debug("No activity for %d seconds.", seconds_since_active)
22042167
if seconds_since_active > self.shutdown_no_activity_timeout:
22052168
self.log.info(
2206-
"No kernels or terminals for %d seconds; shutting down.",
2169+
"No kernels for %d seconds; shutting down.",
22072170
seconds_since_active,
22082171
)
22092172
self.stop()
22102173

22112174
def init_shutdown_no_activity(self):
22122175
if self.shutdown_no_activity_timeout > 0:
22132176
self.log.info(
2214-
"Will shut down after %d seconds with no kernels or terminals.",
2177+
"Will shut down after %d seconds with no kernels.",
22152178
self.shutdown_no_activity_timeout,
22162179
)
22172180
pc = ioloop.PeriodicCallback(self.shutdown_no_activity, 60000)
@@ -2409,7 +2372,6 @@ def initialize(
24092372
self.init_configurables()
24102373
self.init_components()
24112374
self.init_webapp()
2412-
self.init_terminals()
24132375
self.init_signal()
24142376
self.init_ioloop()
24152377
self.load_server_extensions()
@@ -2431,23 +2393,6 @@ async def cleanup_kernels(self):
24312393
self.log.info(kernel_msg % n_kernels)
24322394
await run_sync_in_loop(self.kernel_manager.shutdown_all())
24332395

2434-
async def cleanup_terminals(self):
2435-
"""Shutdown all terminals.
2436-
2437-
The terminals will shutdown themselves when this process no longer exists,
2438-
but explicit shutdown allows the TerminalManager to cleanup.
2439-
"""
2440-
if not self.terminals_available:
2441-
return
2442-
2443-
terminal_manager = self.web_app.settings["terminal_manager"]
2444-
n_terminals = len(terminal_manager.list())
2445-
terminal_msg = trans.ngettext(
2446-
"Shutting down %d terminal", "Shutting down %d terminals", n_terminals
2447-
)
2448-
self.log.info(terminal_msg % n_terminals)
2449-
await run_sync_in_loop(terminal_manager.terminate_all())
2450-
24512396
async def cleanup_extensions(self):
24522397
"""Call shutdown hooks in all extensions."""
24532398
n_extensions = len(self.extension_manager.extension_apps)
@@ -2728,7 +2673,6 @@ async def _cleanup(self):
27282673
self.remove_browser_open_files()
27292674
await self.cleanup_extensions()
27302675
await self.cleanup_kernels()
2731-
await self.cleanup_terminals()
27322676

27332677
def start_ioloop(self):
27342678
"""Start the IO Loop."""

jupyter_server/terminal/__init__.py

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,12 @@
1-
import os
2-
import sys
3-
from shutil import which
4-
5-
import terminado
6-
7-
from ..utils import check_version
8-
9-
if not check_version(terminado.__version__, "0.8.3"):
10-
raise ImportError("terminado >= 0.8.3 required, found %s" % terminado.__version__)
11-
12-
from jupyter_server.utils import url_path_join as ujoin
13-
14-
from . import api_handlers
15-
from .handlers import TermSocket
16-
from .terminalmanager import TerminalManager
17-
18-
19-
def initialize(webapp, root_dir, connection_url, settings):
20-
if os.name == "nt":
21-
default_shell = "powershell.exe"
22-
else:
23-
default_shell = which("sh")
24-
shell_override = settings.get("shell_command")
25-
shell = [os.environ.get("SHELL") or default_shell] if shell_override is None else shell_override
26-
# When the notebook server is not running in a terminal (e.g. when
27-
# it's launched by a JupyterHub spawner), it's likely that the user
28-
# environment hasn't been fully set up. In that case, run a login
29-
# shell to automatically source /etc/profile and the like, unless
30-
# the user has specifically set a preferred shell command.
31-
if os.name != "nt" and shell_override is None and not sys.stdout.isatty():
32-
shell.append("-l")
33-
terminal_manager = webapp.settings["terminal_manager"] = TerminalManager(
34-
shell_command=shell,
35-
extra_env={
36-
"JUPYTER_SERVER_ROOT": root_dir,
37-
"JUPYTER_SERVER_URL": connection_url,
38-
},
39-
parent=webapp.settings["serverapp"],
40-
)
41-
terminal_manager.log = webapp.settings["serverapp"].log
42-
base_url = webapp.settings["base_url"]
43-
handlers = [
44-
(
45-
ujoin(base_url, r"/terminals/websocket/(\w+)"),
46-
TermSocket,
47-
{"term_manager": terminal_manager},
48-
),
49-
(ujoin(base_url, r"/api/terminals"), api_handlers.TerminalRootHandler),
50-
(ujoin(base_url, r"/api/terminals/(\w+)"), api_handlers.TerminalHandler),
51-
]
52-
webapp.add_handlers(".*$", handlers)
1+
import warnings
2+
3+
# Shims
4+
from jupyter_server_terminals import api_handlers, initialize # noqa
5+
from jupyter_server_terminals.handlers import TermSocket # noqa
6+
from jupyter_server_terminals.terminalmanager import TerminalManager # noqa
7+
8+
warnings.warn(
9+
"Terminals support has moved to `jupyter_server_terminals`",
10+
DeprecationWarning,
11+
stacklevel=2,
12+
)

0 commit comments

Comments
 (0)