From 85312f76bbf8f9804ad6613837d018649058c367 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Wed, 20 Oct 2021 16:07:30 +0800 Subject: [PATCH 01/28] Fix missing await when call 'async_replace_file' --- jupyter_server/services/contents/fileio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyter_server/services/contents/fileio.py b/jupyter_server/services/contents/fileio.py index 5025c4570a..8421339c90 100644 --- a/jupyter_server/services/contents/fileio.py +++ b/jupyter_server/services/contents/fileio.py @@ -373,8 +373,8 @@ async def _read_notebook(self, os_path, as_version=4): # Move the bad file aside, restore the intermediate, and try again. invalid_file = path_to_invalid(os_path) - async_replace_file(os_path, invalid_file) - async_replace_file(tmp_path, os_path) + await async_replace_file(os_path, invalid_file) + await async_replace_file(tmp_path, os_path) return await self._read_notebook(os_path, as_version) async def _save_notebook(self, os_path, nb): From 00c92c2a2626a28a45044d46bbba303e03fc0b10 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Thu, 2 Dec 2021 16:04:19 +0800 Subject: [PATCH 02/28] patch execution_state filed into terminal --- jupyter_server/terminal/handlers.py | 15 ++++++++++++++- jupyter_server/terminal/terminalmanager.py | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/jupyter_server/terminal/handlers.py b/jupyter_server/terminal/handlers.py index e56c780dcb..7e7362f2bb 100644 --- a/jupyter_server/terminal/handlers.py +++ b/jupyter_server/terminal/handlers.py @@ -5,9 +5,9 @@ import terminado from tornado import web +from jupyter_server._tz import utcnow from ..base.handlers import JupyterHandler from ..base.zmqhandlers import WebSocketMixin -from jupyter_server._tz import utcnow class TermSocket(WebSocketMixin, JupyterHandler, terminado.TermSocket): @@ -27,13 +27,26 @@ def get(self, *args, **kwargs): def on_message(self, message): super(TermSocket, self).on_message(message) self._update_activity() + self._set_state_busy() def write_message(self, message, binary=False): super(TermSocket, self).write_message(message, binary=binary) self._update_activity() + if message != '["stdout", "\\r\\n"]': + self._set_state_idle() def _update_activity(self): self.application.settings["terminal_last_activity"] = utcnow() # terminal may not be around on deletion/cull if self.term_name in self.terminal_manager.terminals: self.terminal_manager.terminals[self.term_name].last_activity = utcnow() + + def _set_state_busy(self): + self.log.info('set terminal execution_state as busy') + if self.term_name in self.terminal_manager.terminals: + self.terminal_manager.terminals[self.term_name].execution_state = 'busy' + + def _set_state_idle(self): + self.log.info('set terminal execution_state as idle') + if self.term_name in self.terminal_manager.terminals: + self.terminal_manager.terminals[self.term_name].execution_state = 'idle' diff --git a/jupyter_server/terminal/terminalmanager.py b/jupyter_server/terminal/terminalmanager.py index cfbfea8e4c..0148e0865d 100644 --- a/jupyter_server/terminal/terminalmanager.py +++ b/jupyter_server/terminal/terminalmanager.py @@ -13,9 +13,9 @@ from traitlets import Integer from traitlets.config import LoggingConfigurable -from ..prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL from jupyter_server._tz import isoformat from jupyter_server._tz import utcnow +from ..prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL class TerminalManager(LoggingConfigurable, terminado.NamedTermManager): @@ -96,6 +96,7 @@ def get_terminal_model(self, name): model = { "name": name, "last_activity": isoformat(term.last_activity), + 'execution_state': getattr(term, 'execution_state', 'not connected yet') } return model From 418cb1851597e7886048649be01b0d0f5eeea4c9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 2 Dec 2021 06:44:37 -0600 Subject: [PATCH 03/28] Update jupyter_server/terminal/handlers.py --- jupyter_server/terminal/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/terminal/handlers.py b/jupyter_server/terminal/handlers.py index 7e7362f2bb..c438e62d71 100644 --- a/jupyter_server/terminal/handlers.py +++ b/jupyter_server/terminal/handlers.py @@ -42,7 +42,7 @@ def _update_activity(self): self.terminal_manager.terminals[self.term_name].last_activity = utcnow() def _set_state_busy(self): - self.log.info('set terminal execution_state as busy') + self.log.debug('set terminal execution_state as busy') if self.term_name in self.terminal_manager.terminals: self.terminal_manager.terminals[self.term_name].execution_state = 'busy' From e03952d91fb58a58631b30f631f7ec447af01c97 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 2 Dec 2021 06:44:42 -0600 Subject: [PATCH 04/28] Update jupyter_server/terminal/handlers.py --- jupyter_server/terminal/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/terminal/handlers.py b/jupyter_server/terminal/handlers.py index c438e62d71..93ac3ebd9e 100644 --- a/jupyter_server/terminal/handlers.py +++ b/jupyter_server/terminal/handlers.py @@ -47,6 +47,6 @@ def _set_state_busy(self): self.terminal_manager.terminals[self.term_name].execution_state = 'busy' def _set_state_idle(self): - self.log.info('set terminal execution_state as idle') + self.log.debug('set terminal execution_state as idle') if self.term_name in self.terminal_manager.terminals: self.terminal_manager.terminals[self.term_name].execution_state = 'idle' From 4f4502a84b46dcf43349adbfa78f9cf4fc440de5 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Thu, 2 Dec 2021 22:21:54 +0800 Subject: [PATCH 05/28] Record the first output to identify the terminal return --- jupyter_server/terminal/handlers.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/jupyter_server/terminal/handlers.py b/jupyter_server/terminal/handlers.py index 93ac3ebd9e..e758cf446d 100644 --- a/jupyter_server/terminal/handlers.py +++ b/jupyter_server/terminal/handlers.py @@ -2,6 +2,8 @@ """Tornado handlers for the terminal emulator.""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +import json + import terminado from tornado import web @@ -11,6 +13,10 @@ class TermSocket(WebSocketMixin, JupyterHandler, terminado.TermSocket): + def initialize(self, *args, **kwargs): + super(TermSocket, self).initialize(*args, **kwargs) + self._first_stdout = '' + def origin_check(self): """Terminado adds redundant origin_check Tornado already calls check_origin, so don't do anything here. @@ -31,8 +37,15 @@ def on_message(self, message): def write_message(self, message, binary=False): super(TermSocket, self).write_message(message, binary=binary) - self._update_activity() - if message != '["stdout", "\\r\\n"]': + message_seg = json.loads(message) + if not self._first_stdout and message_seg[0] == 'stdout': + # Record the first output to identify the terminal return + # It works well for jupyterhub-singleuser and should also work for other debian-based mirrors + # fixme: May fail if terminal is not properly separated with ':' or change user after connect + # (Any change to the user, hostname or environment may render it invalid) + self._first_stdout = message_seg[1].split(':')[0].lstrip() + self.log.debug(f'take "{self._first_stdout}" as terminal returned') + if isinstance(message_seg[1], str) and message_seg[1].lstrip().startswith(self._first_stdout): self._set_state_idle() def _update_activity(self): @@ -42,11 +55,10 @@ def _update_activity(self): self.terminal_manager.terminals[self.term_name].last_activity = utcnow() def _set_state_busy(self): - self.log.debug('set terminal execution_state as busy') if self.term_name in self.terminal_manager.terminals: self.terminal_manager.terminals[self.term_name].execution_state = 'busy' def _set_state_idle(self): - self.log.debug('set terminal execution_state as idle') if self.term_name in self.terminal_manager.terminals: + self.log.debug('set terminal execution_state as idle') self.terminal_manager.terminals[self.term_name].execution_state = 'idle' From 728319a6a45756e0939ec84d8036780f21ff3eed Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Thu, 2 Dec 2021 22:50:32 +0800 Subject: [PATCH 06/28] make set state idle if terminal return as a function --- jupyter_server/terminal/handlers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jupyter_server/terminal/handlers.py b/jupyter_server/terminal/handlers.py index e758cf446d..5fe6ad0224 100644 --- a/jupyter_server/terminal/handlers.py +++ b/jupyter_server/terminal/handlers.py @@ -37,6 +37,10 @@ def on_message(self, message): def write_message(self, message, binary=False): super(TermSocket, self).write_message(message, binary=binary) + self._update_activity() + self._set_state_idle_if_return(message) + + def _set_state_idle_if_return(self, message): message_seg = json.loads(message) if not self._first_stdout and message_seg[0] == 'stdout': # Record the first output to identify the terminal return From 12a2cad5e8df897db321f2db1008180fe2121352 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 10:10:27 +0800 Subject: [PATCH 07/28] patch first_stdout into terminals --- jupyter_server/terminal/handlers.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/jupyter_server/terminal/handlers.py b/jupyter_server/terminal/handlers.py index 5fe6ad0224..bf77949dc1 100644 --- a/jupyter_server/terminal/handlers.py +++ b/jupyter_server/terminal/handlers.py @@ -13,9 +13,6 @@ class TermSocket(WebSocketMixin, JupyterHandler, terminado.TermSocket): - def initialize(self, *args, **kwargs): - super(TermSocket, self).initialize(*args, **kwargs) - self._first_stdout = '' def origin_check(self): """Terminado adds redundant origin_check @@ -41,15 +38,20 @@ def write_message(self, message, binary=False): self._set_state_idle_if_return(message) def _set_state_idle_if_return(self, message): + if not self.term_name in self.terminal_manager.terminals: + return message_seg = json.loads(message) - if not self._first_stdout and message_seg[0] == 'stdout': + first_stdout = getattr(self.terminal_manager.terminals[self.term_name], 'first_stdout', '') + + if not first_stdout and message_seg[0] == 'stdout': # Record the first output to identify the terminal return # It works well for jupyterhub-singleuser and should also work for other debian-based mirrors # fixme: May fail if terminal is not properly separated with ':' or change user after connect # (Any change to the user, hostname or environment may render it invalid) - self._first_stdout = message_seg[1].split(':')[0].lstrip() - self.log.debug(f'take "{self._first_stdout}" as terminal returned') - if isinstance(message_seg[1], str) and message_seg[1].lstrip().startswith(self._first_stdout): + first_stdout = message_seg[1].split(':')[0].lstrip() + self.terminal_manager.terminals[self.term_name].first_stdout = first_stdout + self.log.debug(f'take "{first_stdout}" as terminal returned') + if isinstance(message_seg[1], str) and message_seg[1].lstrip().startswith(first_stdout): self._set_state_idle() def _update_activity(self): From fdd7dffcbf522466c52951954202f2c935f69752 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 11:59:09 +0800 Subject: [PATCH 08/28] set busy when receive message, set idled when ptyproc read --- jupyter_server/terminal/handlers.py | 24 ------------ jupyter_server/terminal/terminalmanager.py | 43 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/jupyter_server/terminal/handlers.py b/jupyter_server/terminal/handlers.py index bf77949dc1..b2267a6c5e 100644 --- a/jupyter_server/terminal/handlers.py +++ b/jupyter_server/terminal/handlers.py @@ -2,7 +2,6 @@ """Tornado handlers for the terminal emulator.""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import json import terminado from tornado import web @@ -35,24 +34,6 @@ def on_message(self, message): def write_message(self, message, binary=False): super(TermSocket, self).write_message(message, binary=binary) self._update_activity() - self._set_state_idle_if_return(message) - - def _set_state_idle_if_return(self, message): - if not self.term_name in self.terminal_manager.terminals: - return - message_seg = json.loads(message) - first_stdout = getattr(self.terminal_manager.terminals[self.term_name], 'first_stdout', '') - - if not first_stdout and message_seg[0] == 'stdout': - # Record the first output to identify the terminal return - # It works well for jupyterhub-singleuser and should also work for other debian-based mirrors - # fixme: May fail if terminal is not properly separated with ':' or change user after connect - # (Any change to the user, hostname or environment may render it invalid) - first_stdout = message_seg[1].split(':')[0].lstrip() - self.terminal_manager.terminals[self.term_name].first_stdout = first_stdout - self.log.debug(f'take "{first_stdout}" as terminal returned') - if isinstance(message_seg[1], str) and message_seg[1].lstrip().startswith(first_stdout): - self._set_state_idle() def _update_activity(self): self.application.settings["terminal_last_activity"] = utcnow() @@ -63,8 +44,3 @@ def _update_activity(self): def _set_state_busy(self): if self.term_name in self.terminal_manager.terminals: self.terminal_manager.terminals[self.term_name].execution_state = 'busy' - - def _set_state_idle(self): - if self.term_name in self.terminal_manager.terminals: - self.log.debug('set terminal execution_state as idle') - self.terminal_manager.terminals[self.term_name].execution_state = 'idle' diff --git a/jupyter_server/terminal/terminalmanager.py b/jupyter_server/terminal/terminalmanager.py index 0148e0865d..98d73bacc5 100644 --- a/jupyter_server/terminal/terminalmanager.py +++ b/jupyter_server/terminal/terminalmanager.py @@ -7,6 +7,7 @@ from datetime import timedelta import terminado +from terminado.management import _poll from tornado import web from tornado.ioloop import IOLoop from tornado.ioloop import PeriodicCallback @@ -81,6 +82,48 @@ async def terminate(self, name, force=False): # because a terminal has been shutdown TERMINAL_CURRENTLY_RUNNING_TOTAL.dec() + def pty_read(self, fd, events=None): + """Called by the event loop when there is pty data ready to read.""" + # prevent blocking on fd + if not _poll(fd, timeout=0.1): # 100ms + self.log.debug(f"Spurious pty_read() on fd {fd}") + return + ptywclients = self.ptys_by_fd[fd] + try: + s = ptywclients.ptyproc.read(65536) + client_list = ptywclients.clients + ptywclients.read_buffer.append(s) + # hook for set terminal status + self._set_state_idle_if_return(ptywclients, s) + if not client_list: + # No one to consume our output: buffer it. + ptywclients.preopen_buffer.append(s) + return + for client in ptywclients.clients: + client.on_pty_read(s) + except EOFError: + self.on_eof(ptywclients) + for client in ptywclients.clients: + client.on_pty_died() + + def _set_state_idle_if_return(self, ptywclients, s): + first_stdout = getattr(ptywclients, 'first_stdout', '') + + if not first_stdout: + # Record the first output to identify the terminal return + # It works well for jupyterhub-singleuser and should also work for other debian-based mirrors + # fixme: May fail if terminal is not properly separated with ':' or change user after connect + # (Any change to the user, hostname or environment may render it invalid) + first_stdout = s.split(':')[0].lstrip() + ptywclients.first_stdout = first_stdout + self.log.debug(f'take "{first_stdout}" as terminal returned') + if s.lstrip().startswith(first_stdout): + self._set_state_idle(ptywclients) + + def _set_state_idle(self, ptywclients): + self.log.debug('set terminal execution_state as idle') + ptywclients.execution_state = 'idle' + async def terminate_all(self): """Terminate all terminals.""" terms = [name for name in self.terminals] From 8e26e88daa1da8ec94bfe395f31aa51f840c288c Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 13:39:27 +0800 Subject: [PATCH 09/28] Make StatefulTerminalManager a class and configuable --- jupyter_server/serverapp.py | 13 ++++ jupyter_server/terminal/__init__.py | 5 +- jupyter_server/terminal/handlers.py | 6 +- jupyter_server/terminal/terminalmanager.py | 87 +++++++++++----------- 4 files changed, 65 insertions(+), 46 deletions(-) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index ff193dba7e..d1d79fd62d 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -351,6 +351,7 @@ def init_settings( server_root_dir=root_dir, jinja2_env=env, terminals_available=terminado_available and jupyter_app.terminals_enabled, + stateful_terminals_enabled=jupyter_app.stateful_terminals_enabled, serverapp=jupyter_app, ) @@ -1671,6 +1672,18 @@ def _update_server_extensions(self, change): ), ) + stateful_terminals_enabled = Bool( + False, + config=True, + help=_i18n( + """Set to True to enable stateful terminals. + + Terminals may also be automatically disabled if the terminado package + is not available. + """ + ), + ) + # Since use of terminals is also a function of whether the terminado package is # available, this variable holds the "final indication" of whether terminal functionality # should be considered (particularly during shutdown/cleanup). It is enabled only diff --git a/jupyter_server/terminal/__init__.py b/jupyter_server/terminal/__init__.py index 8bac278403..a49766a6d2 100644 --- a/jupyter_server/terminal/__init__.py +++ b/jupyter_server/terminal/__init__.py @@ -12,7 +12,7 @@ from jupyter_server.utils import url_path_join as ujoin from . import api_handlers from .handlers import TermSocket -from .terminalmanager import TerminalManager +from .terminalmanager import TerminalManager, StatefulTerminalManager def initialize(webapp, root_dir, connection_url, settings): @@ -29,7 +29,8 @@ def initialize(webapp, root_dir, connection_url, settings): # the user has specifically set a preferred shell command. if os.name != "nt" and shell_override is None and not sys.stdout.isatty(): shell.append("-l") - terminal_manager = webapp.settings["terminal_manager"] = TerminalManager( + terminal_class = StatefulTerminalManager if webapp.settings['stateful_terminals_enabled'] else TerminalManager + terminal_manager = webapp.settings['terminal_manager'] = terminal_class( shell_command=shell, extra_env={ "JUPYTER_SERVER_ROOT": root_dir, diff --git a/jupyter_server/terminal/handlers.py b/jupyter_server/terminal/handlers.py index b2267a6c5e..090a1c9b53 100644 --- a/jupyter_server/terminal/handlers.py +++ b/jupyter_server/terminal/handlers.py @@ -29,7 +29,7 @@ def get(self, *args, **kwargs): def on_message(self, message): super(TermSocket, self).on_message(message) self._update_activity() - self._set_state_busy() + self._set_state_busy_if_stateful() def write_message(self, message, binary=False): super(TermSocket, self).write_message(message, binary=binary) @@ -41,6 +41,8 @@ def _update_activity(self): if self.term_name in self.terminal_manager.terminals: self.terminal_manager.terminals[self.term_name].last_activity = utcnow() - def _set_state_busy(self): + def _set_state_busy_if_stateful(self): + if not hasattr(self.terminal_manager, 'set_state_idle_if_return'): + return if self.term_name in self.terminal_manager.terminals: self.terminal_manager.terminals[self.term_name].execution_state = 'busy' diff --git a/jupyter_server/terminal/terminalmanager.py b/jupyter_server/terminal/terminalmanager.py index 98d73bacc5..aafbdb6789 100644 --- a/jupyter_server/terminal/terminalmanager.py +++ b/jupyter_server/terminal/terminalmanager.py @@ -82,48 +82,6 @@ async def terminate(self, name, force=False): # because a terminal has been shutdown TERMINAL_CURRENTLY_RUNNING_TOTAL.dec() - def pty_read(self, fd, events=None): - """Called by the event loop when there is pty data ready to read.""" - # prevent blocking on fd - if not _poll(fd, timeout=0.1): # 100ms - self.log.debug(f"Spurious pty_read() on fd {fd}") - return - ptywclients = self.ptys_by_fd[fd] - try: - s = ptywclients.ptyproc.read(65536) - client_list = ptywclients.clients - ptywclients.read_buffer.append(s) - # hook for set terminal status - self._set_state_idle_if_return(ptywclients, s) - if not client_list: - # No one to consume our output: buffer it. - ptywclients.preopen_buffer.append(s) - return - for client in ptywclients.clients: - client.on_pty_read(s) - except EOFError: - self.on_eof(ptywclients) - for client in ptywclients.clients: - client.on_pty_died() - - def _set_state_idle_if_return(self, ptywclients, s): - first_stdout = getattr(ptywclients, 'first_stdout', '') - - if not first_stdout: - # Record the first output to identify the terminal return - # It works well for jupyterhub-singleuser and should also work for other debian-based mirrors - # fixme: May fail if terminal is not properly separated with ':' or change user after connect - # (Any change to the user, hostname or environment may render it invalid) - first_stdout = s.split(':')[0].lstrip() - ptywclients.first_stdout = first_stdout - self.log.debug(f'take "{first_stdout}" as terminal returned') - if s.lstrip().startswith(first_stdout): - self._set_state_idle(ptywclients) - - def _set_state_idle(self, ptywclients): - self.log.debug('set terminal execution_state as idle') - ptywclients.execution_state = 'idle' - async def terminate_all(self): """Terminate all terminals.""" terms = [name for name in self.terminals] @@ -209,3 +167,48 @@ async def _cull_inactive_terminal(self, name): "Culling terminal '%s' due to %s seconds of inactivity.", name, inactivity ) await self.terminate(name, force=True) + + +class StatefulTerminalManager(TerminalManager): + # patch execution_state into terminal + def pty_read(self, fd, events=None): + """Called by the event loop when there is pty data ready to read.""" + # prevent blocking on fd + if not _poll(fd, timeout=0.1): # 100ms + self.log.debug(f"Spurious pty_read() on fd {fd}") + return + ptywclients = self.ptys_by_fd[fd] + try: + s = ptywclients.ptyproc.read(65536) + client_list = ptywclients.clients + ptywclients.read_buffer.append(s) + # hook for set terminal status + self.set_state_idle_if_return(ptywclients, s) + if not client_list: + # No one to consume our output: buffer it. + ptywclients.preopen_buffer.append(s) + return + for client in ptywclients.clients: + client.on_pty_read(s) + except EOFError: + self.on_eof(ptywclients) + for client in ptywclients.clients: + client.on_pty_died() + + def set_state_idle_if_return(self, ptywclients, s): + first_stdout = getattr(ptywclients, 'first_stdout', '') + + if not first_stdout: + # Record the first output to identify the terminal return + # It works well for jupyterhub-singleuser and should also work for other debian-based mirrors + # fixme: May fail if terminal is not properly separated with ':' or change user after connect + # (Any change to the user, hostname or environment may render it invalid) + first_stdout = s.split(':')[0].lstrip() + ptywclients.first_stdout = first_stdout + self.log.debug(f'take "{first_stdout}" as terminal returned') + if s.lstrip().startswith(first_stdout): + self._set_state_idle(ptywclients) + + def _set_state_idle(self, ptywclients): + self.log.debug('set terminal execution_state as idle') + ptywclients.execution_state = 'idle' From 695cf276261e96c4567d540940854a506fa3a643 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 13:47:46 +0800 Subject: [PATCH 10/28] revert TerminalManager and make all state operation into StatefulTerminalManager --- jupyter_server/terminal/terminalmanager.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/jupyter_server/terminal/terminalmanager.py b/jupyter_server/terminal/terminalmanager.py index aafbdb6789..179369d412 100644 --- a/jupyter_server/terminal/terminalmanager.py +++ b/jupyter_server/terminal/terminalmanager.py @@ -97,7 +97,6 @@ def get_terminal_model(self, name): model = { "name": name, "last_activity": isoformat(term.last_activity), - 'execution_state': getattr(term, 'execution_state', 'not connected yet') } return model @@ -212,3 +211,12 @@ def set_state_idle_if_return(self, ptywclients, s): def _set_state_idle(self, ptywclients): self.log.debug('set terminal execution_state as idle') ptywclients.execution_state = 'idle' + + def get_terminal_model(self, name): + """Return a JSON-safe dict representing a terminal. + For use in representing terminals in the JSON APIs. + """ + model = super(StatefulTerminalManager, self).get_terminal_model(name) + term = self.terminals[name] + model.setdefault('execution_state', getattr(term, 'execution_state', 'not connected yet')) + return model From f4a07e3e85fb462a669ac9f45baf5b134273df4c Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 16:06:37 +0800 Subject: [PATCH 11/28] add some test on StatefulTerminalManager --- .../tests/test_stateful_terminal.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 jupyter_server/tests/test_stateful_terminal.py diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py new file mode 100644 index 0000000000..675943b4af --- /dev/null +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -0,0 +1,87 @@ +import asyncio +import json +import os +import shutil + +import pytest +from traitlets.config import Config + + +@pytest.fixture +def terminal_path(tmp_path): + subdir = tmp_path.joinpath("terminal_path") + subdir.mkdir() + + yield subdir + + shutil.rmtree(str(subdir), ignore_errors=True) + + +CULL_TIMEOUT = 10 +CULL_INTERVAL = 3 + + +@pytest.fixture +def jp_server_config(): + return Config( + { + "ServerApp": { + "stateful_terminals_enabled": True, + } + } + ) + + +async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): + # disable man sudo_root + os.system(f"touch {os.path.expanduser('~/.sudo_as_admin_successful')}") + + resp = await jp_fetch( + "api", + "terminals", + method="POST", + allow_nonstandard_methods=True, + ) + term = json.loads(resp.body.decode()) + term_1 = term["name"] + ws = await jp_ws_fetch( + 'terminals', 'websocket', term_1 + ) + setup = ["set_size", 0, 0, 80, 32] + await ws.write_message(json.dumps(setup)) + await ws.read_message() + sleep_1_msg = ['stdin', "python -c 'import time;time.sleep(1)'\r\n"] + await ws.write_message(json.dumps(sleep_1_msg)) + assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'busy' + await asyncio.sleep(2) + assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'idle' + await jp_cleanup_subprocesses() + + +async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): + # disable man sudo_root + os.system(f"touch {os.path.expanduser('~/.sudo_as_admin_successful')}") + + resp = await jp_fetch( + "api", + "terminals", + method="POST", + allow_nonstandard_methods=True, + ) + term = json.loads(resp.body.decode()) + term_1 = term["name"] + ws = await jp_ws_fetch( + 'terminals', 'websocket', term_1 + ) + setup = ["set_size", 0, 0, 80, 32] + await ws.write_message(json.dumps(setup)) + await ws.read_message() + sleep_3_msg = ['stdin', "python -c 'import time;time.sleep(3)'\r\n"] + await ws.write_message(json.dumps(sleep_3_msg)) + ws.close() + await asyncio.sleep(1) + assert not jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].clients + assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'busy' + await asyncio.sleep(3) + assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'idle' + await jp_cleanup_subprocesses() From 3c0688a273555d04633d123213d5ecb453132472 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 16:26:55 +0800 Subject: [PATCH 12/28] improve stateful terminal test --- jupyter_server/tests/test_stateful_terminal.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index 675943b4af..5eca536608 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -76,12 +76,11 @@ async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesse setup = ["set_size", 0, 0, 80, 32] await ws.write_message(json.dumps(setup)) await ws.read_message() - sleep_3_msg = ['stdin', "python -c 'import time;time.sleep(3)'\r\n"] - await ws.write_message(json.dumps(sleep_3_msg)) + sleep_1_msg = ['stdin', "python -c 'import time;time.sleep(1)'\r\n"] + await ws.write_message(json.dumps(sleep_1_msg)) ws.close() - await asyncio.sleep(1) - assert not jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].clients assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'busy' - await asyncio.sleep(3) + await asyncio.sleep(2) + assert not jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].clients assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'idle' await jp_cleanup_subprocesses() From 3e3b703a65c95514a7ee57e2df6582b646880d49 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 16:35:16 +0800 Subject: [PATCH 13/28] change test wait durations --- jupyter_server/tests/test_stateful_terminal.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index 5eca536608..07356fff07 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -50,8 +50,9 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve setup = ["set_size", 0, 0, 80, 32] await ws.write_message(json.dumps(setup)) await ws.read_message() - sleep_1_msg = ['stdin', "python -c 'import time;time.sleep(1)'\r\n"] - await ws.write_message(json.dumps(sleep_1_msg)) + sleep_2_msg = ['stdin', "python -c 'import time;time.sleep(2)'\r\n"] + await ws.write_message(json.dumps(sleep_2_msg)) + await asyncio.sleep(1) assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'busy' await asyncio.sleep(2) assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'idle' @@ -76,11 +77,12 @@ async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesse setup = ["set_size", 0, 0, 80, 32] await ws.write_message(json.dumps(setup)) await ws.read_message() - sleep_1_msg = ['stdin', "python -c 'import time;time.sleep(1)'\r\n"] - await ws.write_message(json.dumps(sleep_1_msg)) + sleep_2_msg = ['stdin', "python -c 'import time;time.sleep(2)'\r\n"] + await ws.write_message(json.dumps(sleep_2_msg)) ws.close() + await asyncio.sleep(1) assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'busy' await asyncio.sleep(2) assert not jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].clients assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'idle' - await jp_cleanup_subprocesses() + await jp_cleanup_subprocesses() \ No newline at end of file From 36fc36470586c9f7590931bbbd23c776c24a0fa6 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 16:59:50 +0800 Subject: [PATCH 14/28] add block to wait init terminal --- .../tests/test_stateful_terminal.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index 07356fff07..e93cdb805f 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -49,12 +49,28 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve ) setup = ["set_size", 0, 0, 80, 32] await ws.write_message(json.dumps(setup)) - await ws.read_message() + while True: + try: + await asyncio.wait_for(ws.read_message(), timeout=1) + except asyncio.TimeoutError: + break sleep_2_msg = ['stdin', "python -c 'import time;time.sleep(2)'\r\n"] await ws.write_message(json.dumps(sleep_2_msg)) await asyncio.sleep(1) assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'busy' await asyncio.sleep(2) + message_stdout = "" + while True: + try: + message = await asyncio.wait_for(ws.read_message(), timeout=5.0) + except asyncio.TimeoutError: + break + + message = json.loads(message) + + if message[0] == "stdout": + message_stdout += message[1] + print(message_stdout) assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'idle' await jp_cleanup_subprocesses() @@ -76,7 +92,11 @@ async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesse ) setup = ["set_size", 0, 0, 80, 32] await ws.write_message(json.dumps(setup)) - await ws.read_message() + while True: + try: + await asyncio.wait_for(ws.read_message(), timeout=1) + except asyncio.TimeoutError: + break sleep_2_msg = ['stdin', "python -c 'import time;time.sleep(2)'\r\n"] await ws.write_message(json.dumps(sleep_2_msg)) ws.close() @@ -85,4 +105,4 @@ async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesse await asyncio.sleep(2) assert not jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].clients assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'idle' - await jp_cleanup_subprocesses() \ No newline at end of file + await jp_cleanup_subprocesses() From b6998c1efabc51cec971a9590a9dfe5b5a963999 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 17:14:19 +0800 Subject: [PATCH 15/28] only test linux system --- jupyter_server/tests/test_stateful_terminal.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index e93cdb805f..323d048971 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -1,6 +1,7 @@ import asyncio import json import os +import platform import shutil import pytest @@ -33,6 +34,9 @@ def jp_server_config(): async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): + if platform.system().lower() != 'linux': + return + # disable man sudo_root os.system(f"touch {os.path.expanduser('~/.sudo_as_admin_successful')}") @@ -76,6 +80,9 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): + if platform.system().lower() != 'linux': + return + # disable man sudo_root os.system(f"touch {os.path.expanduser('~/.sudo_as_admin_successful')}") From 998efff0ccff6eb708d0d5ac1cd039dcfae6a3a2 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Fri, 3 Dec 2021 17:39:07 +0800 Subject: [PATCH 16/28] pre-commit run --- jupyter_server/terminal/__init__.py | 8 +++-- jupyter_server/terminal/handlers.py | 8 ++--- jupyter_server/terminal/terminalmanager.py | 14 ++++---- .../tests/test_stateful_terminal.py | 36 +++++++++++-------- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/jupyter_server/terminal/__init__.py b/jupyter_server/terminal/__init__.py index a49766a6d2..295dd01128 100644 --- a/jupyter_server/terminal/__init__.py +++ b/jupyter_server/terminal/__init__.py @@ -29,8 +29,12 @@ def initialize(webapp, root_dir, connection_url, settings): # the user has specifically set a preferred shell command. if os.name != "nt" and shell_override is None and not sys.stdout.isatty(): shell.append("-l") - terminal_class = StatefulTerminalManager if webapp.settings['stateful_terminals_enabled'] else TerminalManager - terminal_manager = webapp.settings['terminal_manager'] = terminal_class( + terminal_class = ( + StatefulTerminalManager + if webapp.settings["stateful_terminals_enabled"] + else TerminalManager + ) + terminal_manager = webapp.settings["terminal_manager"] = terminal_class( shell_command=shell, extra_env={ "JUPYTER_SERVER_ROOT": root_dir, diff --git a/jupyter_server/terminal/handlers.py b/jupyter_server/terminal/handlers.py index 090a1c9b53..fcfc76d030 100644 --- a/jupyter_server/terminal/handlers.py +++ b/jupyter_server/terminal/handlers.py @@ -2,17 +2,15 @@ """Tornado handlers for the terminal emulator.""" # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. - import terminado from tornado import web -from jupyter_server._tz import utcnow from ..base.handlers import JupyterHandler from ..base.zmqhandlers import WebSocketMixin +from jupyter_server._tz import utcnow class TermSocket(WebSocketMixin, JupyterHandler, terminado.TermSocket): - def origin_check(self): """Terminado adds redundant origin_check Tornado already calls check_origin, so don't do anything here. @@ -42,7 +40,7 @@ def _update_activity(self): self.terminal_manager.terminals[self.term_name].last_activity = utcnow() def _set_state_busy_if_stateful(self): - if not hasattr(self.terminal_manager, 'set_state_idle_if_return'): + if not hasattr(self.terminal_manager, "set_state_idle_if_return"): return if self.term_name in self.terminal_manager.terminals: - self.terminal_manager.terminals[self.term_name].execution_state = 'busy' + self.terminal_manager.terminals[self.term_name].execution_state = "busy" diff --git a/jupyter_server/terminal/terminalmanager.py b/jupyter_server/terminal/terminalmanager.py index 179369d412..cd38123c1d 100644 --- a/jupyter_server/terminal/terminalmanager.py +++ b/jupyter_server/terminal/terminalmanager.py @@ -14,9 +14,9 @@ from traitlets import Integer from traitlets.config import LoggingConfigurable +from ..prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL from jupyter_server._tz import isoformat from jupyter_server._tz import utcnow -from ..prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL class TerminalManager(LoggingConfigurable, terminado.NamedTermManager): @@ -103,7 +103,7 @@ def get_terminal_model(self, name): def _check_terminal(self, name): """Check a that terminal 'name' exists and raise 404 if not.""" if name not in self.terminals: - raise web.HTTPError(404, u"Terminal not found: %s" % name) + raise web.HTTPError(404, "Terminal not found: %s" % name) def _initialize_culler(self): """Start culler if 'cull_inactive_timeout' is greater than zero. @@ -195,22 +195,22 @@ def pty_read(self, fd, events=None): client.on_pty_died() def set_state_idle_if_return(self, ptywclients, s): - first_stdout = getattr(ptywclients, 'first_stdout', '') + first_stdout = getattr(ptywclients, "first_stdout", "") if not first_stdout: # Record the first output to identify the terminal return # It works well for jupyterhub-singleuser and should also work for other debian-based mirrors # fixme: May fail if terminal is not properly separated with ':' or change user after connect # (Any change to the user, hostname or environment may render it invalid) - first_stdout = s.split(':')[0].lstrip() + first_stdout = s.split(":")[0].lstrip() ptywclients.first_stdout = first_stdout self.log.debug(f'take "{first_stdout}" as terminal returned') if s.lstrip().startswith(first_stdout): self._set_state_idle(ptywclients) def _set_state_idle(self, ptywclients): - self.log.debug('set terminal execution_state as idle') - ptywclients.execution_state = 'idle' + self.log.debug("set terminal execution_state as idle") + ptywclients.execution_state = "idle" def get_terminal_model(self, name): """Return a JSON-safe dict representing a terminal. @@ -218,5 +218,5 @@ def get_terminal_model(self, name): """ model = super(StatefulTerminalManager, self).get_terminal_model(name) term = self.terminals[name] - model.setdefault('execution_state', getattr(term, 'execution_state', 'not connected yet')) + model.setdefault("execution_state", getattr(term, "execution_state", "not connected yet")) return model diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index 323d048971..cd6d444c27 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -34,7 +34,7 @@ def jp_server_config(): async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): - if platform.system().lower() != 'linux': + if platform.system().lower() != "linux": return # disable man sudo_root @@ -48,9 +48,7 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve ) term = json.loads(resp.body.decode()) term_1 = term["name"] - ws = await jp_ws_fetch( - 'terminals', 'websocket', term_1 - ) + ws = await jp_ws_fetch("terminals", "websocket", term_1) setup = ["set_size", 0, 0, 80, 32] await ws.write_message(json.dumps(setup)) while True: @@ -58,10 +56,13 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve await asyncio.wait_for(ws.read_message(), timeout=1) except asyncio.TimeoutError: break - sleep_2_msg = ['stdin', "python -c 'import time;time.sleep(2)'\r\n"] + sleep_2_msg = ["stdin", "python -c 'import time;time.sleep(2)'\r\n"] await ws.write_message(json.dumps(sleep_2_msg)) await asyncio.sleep(1) - assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'busy' + assert ( + jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state + == "busy" + ) await asyncio.sleep(2) message_stdout = "" while True: @@ -75,12 +76,15 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve if message[0] == "stdout": message_stdout += message[1] print(message_stdout) - assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'idle' + assert ( + jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state + == "idle" + ) await jp_cleanup_subprocesses() async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): - if platform.system().lower() != 'linux': + if platform.system().lower() != "linux": return # disable man sudo_root @@ -94,9 +98,7 @@ async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesse ) term = json.loads(resp.body.decode()) term_1 = term["name"] - ws = await jp_ws_fetch( - 'terminals', 'websocket', term_1 - ) + ws = await jp_ws_fetch("terminals", "websocket", term_1) setup = ["set_size", 0, 0, 80, 32] await ws.write_message(json.dumps(setup)) while True: @@ -104,12 +106,18 @@ async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesse await asyncio.wait_for(ws.read_message(), timeout=1) except asyncio.TimeoutError: break - sleep_2_msg = ['stdin', "python -c 'import time;time.sleep(2)'\r\n"] + sleep_2_msg = ["stdin", "python -c 'import time;time.sleep(2)'\r\n"] await ws.write_message(json.dumps(sleep_2_msg)) ws.close() await asyncio.sleep(1) - assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'busy' + assert ( + jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state + == "busy" + ) await asyncio.sleep(2) assert not jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].clients - assert jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state == 'idle' + assert ( + jp_serverapp.web_app.settings["terminal_manager"].terminals[term_1].execution_state + == "idle" + ) await jp_cleanup_subprocesses() From d125c38e09b749169784e611788d9c96603f5adc Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 3 Dec 2021 13:27:26 -0600 Subject: [PATCH 17/28] Update jupyter_server/tests/test_stateful_terminal.py --- jupyter_server/tests/test_stateful_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index cd6d444c27..bbe7af372c 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -84,7 +84,7 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): - if platform.system().lower() != "linux": + if os.name != "nt": return # disable man sudo_root From 1af3064f7095fee66d0f2fb81caa143b5096ba97 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 3 Dec 2021 13:28:07 -0600 Subject: [PATCH 18/28] Update jupyter_server/tests/test_stateful_terminal.py --- jupyter_server/tests/test_stateful_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index bbe7af372c..8400133da3 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -85,7 +85,7 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): if os.name != "nt": - return + pytest.skip('Feature not supported on Windows') # disable man sudo_root os.system(f"touch {os.path.expanduser('~/.sudo_as_admin_successful')}") From 1b6c158cb91799fb3a24fc3acb1ba9c2db7b528e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 3 Dec 2021 17:32:29 -0600 Subject: [PATCH 19/28] Update jupyter_server/tests/test_stateful_terminal.py --- jupyter_server/tests/test_stateful_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index 8400133da3..8612db0242 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -84,7 +84,7 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): - if os.name != "nt": + if os.name == "nt": pytest.skip('Feature not supported on Windows') # disable man sudo_root From 02f7fb841dc63a19b89e7d48d0014b9557e33d23 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 3 Dec 2021 17:33:08 -0600 Subject: [PATCH 20/28] Update jupyter_server/tests/test_stateful_terminal.py --- jupyter_server/tests/test_stateful_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index 8612db0242..24e95e70a2 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -85,7 +85,7 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): if os.name == "nt": - pytest.skip('Feature not supported on Windows') + pytest.skip("Feature not supported on Windows") # disable man sudo_root os.system(f"touch {os.path.expanduser('~/.sudo_as_admin_successful')}") From 8c0186b2b739ca75bf50d8ac086070c50a88a5b9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 3 Dec 2021 17:33:33 -0600 Subject: [PATCH 21/28] Update jupyter_server/tests/test_stateful_terminal.py --- jupyter_server/tests/test_stateful_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index 24e95e70a2..8e94b70b2c 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -34,7 +34,7 @@ def jp_server_config(): async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): - if platform.system().lower() != "linux": + if os.name == "nt": return # disable man sudo_root From d627bb1bb89a190359515922f44e105772f94d05 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 3 Dec 2021 17:34:02 -0600 Subject: [PATCH 22/28] Update jupyter_server/tests/test_stateful_terminal.py --- jupyter_server/tests/test_stateful_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index 8e94b70b2c..af34775982 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -35,7 +35,7 @@ def jp_server_config(): async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): if os.name == "nt": - return + pytest.skip("Feature not supported on Windows") # disable man sudo_root os.system(f"touch {os.path.expanduser('~/.sudo_as_admin_successful')}") From 4111d90315dab0537f479255c59f7e3b49a4237c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 3 Dec 2021 20:34:09 -0600 Subject: [PATCH 23/28] Only run on Linux --- jupyter_server/tests/test_stateful_terminal.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index af34775982..bbfa6a6746 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -33,10 +33,8 @@ def jp_server_config(): ) +@pytest.mark.skipif(platform.system().lower() != "linux", reason="Only available on Linux") async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): - if os.name == "nt": - pytest.skip("Feature not supported on Windows") - # disable man sudo_root os.system(f"touch {os.path.expanduser('~/.sudo_as_admin_successful')}") @@ -83,10 +81,8 @@ async def test_set_idle(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serve await jp_cleanup_subprocesses() +@pytest.mark.skipif(platform.system().lower() != "linux", reason="Only available on Linux") async def test_set_idle_disconnect(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses, jp_serverapp): - if os.name == "nt": - pytest.skip("Feature not supported on Windows") - # disable man sudo_root os.system(f"touch {os.path.expanduser('~/.sudo_as_admin_successful')}") From e242e560caf9e80c03bbb33651682dafda82cc0d Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Sun, 5 Dec 2021 13:31:39 +0800 Subject: [PATCH 24/28] add terminal_manager_class to support third-party terminal_manager --- jupyter_server/serverapp.py | 20 ++++++++++---------- jupyter_server/terminal/__init__.py | 9 +++------ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index d1d79fd62d..b0a9290d8b 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -178,6 +178,7 @@ # Added for backwards compatibility from classic notebook server. DEFAULT_SERVER_PORT = DEFAULT_JUPYTER_SERVER_PORT + # ----------------------------------------------------------------------------- # Helper functions # ----------------------------------------------------------------------------- @@ -351,7 +352,7 @@ def init_settings( server_root_dir=root_dir, jinja2_env=env, terminals_available=terminado_available and jupyter_app.terminals_enabled, - stateful_terminals_enabled=jupyter_app.stateful_terminals_enabled, + terminal_manager_class=jupyter_app.terminal_manager_class, serverapp=jupyter_app, ) @@ -526,7 +527,6 @@ def shutdown_server(server_info, timeout=5, log=None): class JupyterServerStopApp(JupyterApp): - version = __version__ description = "Stop currently running Jupyter server for a given port" @@ -666,7 +666,6 @@ def start(self): """, ) - # Add notebook manager flags flags.update( boolean_flag( @@ -695,13 +694,13 @@ def start(self): } ) + # ----------------------------------------------------------------------------- # ServerApp # ----------------------------------------------------------------------------- class ServerApp(JupyterApp): - name = "jupyter-server" version = __version__ description = _i18n( @@ -1672,15 +1671,16 @@ def _update_server_extensions(self, change): ), ) - stateful_terminals_enabled = Bool( - False, + terminal_manager_class = Type( + default_value=TerminalManager, + klass=TerminalManager, config=True, help=_i18n( - """Set to True to enable stateful terminals. + """The terminal manager class to use. - Terminals may also be automatically disabled if the terminado package - is not available. - """ + Only when terminals_enabled is instantiated, + the call to init_terminals function will get self.terminal_manager + """ ), ) diff --git a/jupyter_server/terminal/__init__.py b/jupyter_server/terminal/__init__.py index 295dd01128..e4e4561d70 100644 --- a/jupyter_server/terminal/__init__.py +++ b/jupyter_server/terminal/__init__.py @@ -29,12 +29,9 @@ def initialize(webapp, root_dir, connection_url, settings): # the user has specifically set a preferred shell command. if os.name != "nt" and shell_override is None and not sys.stdout.isatty(): shell.append("-l") - terminal_class = ( - StatefulTerminalManager - if webapp.settings["stateful_terminals_enabled"] - else TerminalManager - ) - terminal_manager = webapp.settings["terminal_manager"] = terminal_class( + terminal_manager = webapp.settings["terminal_manager"] = webapp.settings[ + "terminal_manager_class" + ]( shell_command=shell, extra_env={ "JUPYTER_SERVER_ROOT": root_dir, From ea00345dac99ecaed8900754a33b2aa5c26ca9e4 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Sun, 5 Dec 2021 13:38:06 +0800 Subject: [PATCH 25/28] change stateful terminal test config to terminal_manager_class --- jupyter_server/tests/test_stateful_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/tests/test_stateful_terminal.py b/jupyter_server/tests/test_stateful_terminal.py index bbfa6a6746..72549f504d 100644 --- a/jupyter_server/tests/test_stateful_terminal.py +++ b/jupyter_server/tests/test_stateful_terminal.py @@ -27,7 +27,7 @@ def jp_server_config(): return Config( { "ServerApp": { - "stateful_terminals_enabled": True, + "terminal_manager_class": "jupyter_server.terminal.StatefulTerminalManager", } } ) From 82847b12adcf3c788f49aad5317e60a0f2766d18 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Sun, 5 Dec 2021 14:29:46 +0800 Subject: [PATCH 26/28] add comments on StatefulTerminalManager --- jupyter_server/terminal/terminalmanager.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jupyter_server/terminal/terminalmanager.py b/jupyter_server/terminal/terminalmanager.py index cd38123c1d..cee732c457 100644 --- a/jupyter_server/terminal/terminalmanager.py +++ b/jupyter_server/terminal/terminalmanager.py @@ -169,7 +169,12 @@ async def _cull_inactive_terminal(self, name): class StatefulTerminalManager(TerminalManager): - # patch execution_state into terminal + """ + ***Experimental*** + Patch execution_state into terminal + The method of setting the state is not necessarily reliable, the terminal of bus y state will still be culled + """ + def pty_read(self, fd, events=None): """Called by the event loop when there is pty data ready to read.""" # prevent blocking on fd From 12f3af0d71501960837e7a5aa57c454875fccf62 Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Tue, 7 Dec 2021 09:21:54 +0800 Subject: [PATCH 27/28] Compatible with terminado_available is False --- jupyter_server/serverapp.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index b0a9290d8b..fc8cec6d7e 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -147,6 +147,11 @@ except ImportError: terminado_available = False + + class TerminalManager: + # Compatible with terminal_manager_class configuration + ... + # ----------------------------------------------------------------------------- # Module globals # ----------------------------------------------------------------------------- @@ -1673,7 +1678,7 @@ def _update_server_extensions(self, change): terminal_manager_class = Type( default_value=TerminalManager, - klass=TerminalManager, + klass=TerminalManager if terminado_available else object, config=True, help=_i18n( """The terminal manager class to use. From 5d4c9f9242ff235246eec6afca2feee90373ab0e Mon Sep 17 00:00:00 2001 From: jizhongsheng Date: Tue, 7 Dec 2021 09:25:56 +0800 Subject: [PATCH 28/28] pre-commit run --- jupyter_server/serverapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index fc8cec6d7e..1feafaee6d 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -147,11 +147,11 @@ except ImportError: terminado_available = False - class TerminalManager: # Compatible with terminal_manager_class configuration ... + # ----------------------------------------------------------------------------- # Module globals # -----------------------------------------------------------------------------