You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			129 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			129 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Python
		
	
"""A terminals extension app."""
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import os
 | 
						|
import shlex
 | 
						|
import sys
 | 
						|
import typing as t
 | 
						|
from shutil import which
 | 
						|
 | 
						|
from jupyter_core.utils import ensure_async
 | 
						|
from jupyter_server.extension.application import ExtensionApp
 | 
						|
from jupyter_server.transutils import trans
 | 
						|
from traitlets import Type
 | 
						|
 | 
						|
from . import api_handlers, handlers
 | 
						|
from .terminalmanager import TerminalManager
 | 
						|
 | 
						|
 | 
						|
class TerminalsExtensionApp(ExtensionApp):
 | 
						|
    """A terminals extension app."""
 | 
						|
 | 
						|
    name = "jupyter_server_terminals"
 | 
						|
 | 
						|
    terminal_manager_class: type[TerminalManager] = Type(  # type:ignore[assignment]
 | 
						|
        default_value=TerminalManager, help="The terminal manager class to use."
 | 
						|
    ).tag(config=True)
 | 
						|
 | 
						|
    # 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
 | 
						|
    # once both the terminals "service" can be initialized and terminals_enabled is True.
 | 
						|
    # Note: this variable is slightly different from 'terminals_available' in the web settings
 | 
						|
    # in that this variable *could* remain false if terminado is available, yet the terminal
 | 
						|
    # service's initialization still fails.  As a result, this variable holds the truth.
 | 
						|
    terminals_available = False
 | 
						|
 | 
						|
    def initialize_settings(self) -> None:
 | 
						|
        """Initialize settings."""
 | 
						|
        if not self.serverapp or not self.serverapp.terminals_enabled:
 | 
						|
            self.settings.update({"terminals_available": False})
 | 
						|
            return
 | 
						|
        self.initialize_configurables()
 | 
						|
        self.settings.update(
 | 
						|
            {"terminals_available": True, "terminal_manager": self.terminal_manager}
 | 
						|
        )
 | 
						|
 | 
						|
    def initialize_configurables(self) -> None:
 | 
						|
        """Initialize configurables."""
 | 
						|
        default_shell = "powershell.exe" if os.name == "nt" else which("sh")
 | 
						|
        assert self.serverapp is not None
 | 
						|
        shell_override = self.serverapp.terminado_settings.get("shell_command")
 | 
						|
        if isinstance(shell_override, str):
 | 
						|
            shell_override = shlex.split(shell_override)
 | 
						|
        shell = (
 | 
						|
            [os.environ.get("SHELL") or default_shell] if shell_override is None else shell_override
 | 
						|
        )
 | 
						|
        # When the notebook server is not running in a terminal (e.g. when
 | 
						|
        # it's launched by a JupyterHub spawner), it's likely that the user
 | 
						|
        # environment hasn't been fully set up. In that case, run a login
 | 
						|
        # shell to automatically source /etc/profile and the like, unless
 | 
						|
        # 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")
 | 
						|
 | 
						|
        self.terminal_manager = self.terminal_manager_class(
 | 
						|
            shell_command=shell,
 | 
						|
            extra_env={
 | 
						|
                "JUPYTER_SERVER_ROOT": self.serverapp.root_dir,
 | 
						|
                "JUPYTER_SERVER_URL": self.serverapp.connection_url,
 | 
						|
            },
 | 
						|
            parent=self.serverapp,
 | 
						|
        )
 | 
						|
        self.terminal_manager.log = self.serverapp.log
 | 
						|
 | 
						|
    def initialize_handlers(self) -> None:
 | 
						|
        """Initialize handlers."""
 | 
						|
        if not self.serverapp:
 | 
						|
            # Already set `terminals_available` as `False` in `initialize_settings`
 | 
						|
            return
 | 
						|
 | 
						|
        if not self.serverapp.terminals_enabled:
 | 
						|
            # webapp settings for backwards compat (used by nbclassic), #12
 | 
						|
            self.serverapp.web_app.settings["terminals_available"] = self.settings[
 | 
						|
                "terminals_available"
 | 
						|
            ]
 | 
						|
            return
 | 
						|
        self.handlers.append(
 | 
						|
            (
 | 
						|
                r"/terminals/websocket/(\w+)",
 | 
						|
                handlers.TermSocket,
 | 
						|
                {"term_manager": self.terminal_manager},
 | 
						|
            )
 | 
						|
        )
 | 
						|
        self.handlers.extend(api_handlers.default_handlers)
 | 
						|
        assert self.serverapp is not None
 | 
						|
        self.serverapp.web_app.settings["terminal_manager"] = self.terminal_manager
 | 
						|
        self.serverapp.web_app.settings["terminals_available"] = self.settings[
 | 
						|
            "terminals_available"
 | 
						|
        ]
 | 
						|
 | 
						|
    def current_activity(self) -> dict[str, t.Any] | None:
 | 
						|
        """Get current activity info."""
 | 
						|
        if self.terminals_available:
 | 
						|
            terminals = self.terminal_manager.terminals
 | 
						|
            if terminals:
 | 
						|
                return terminals
 | 
						|
        return None
 | 
						|
 | 
						|
    async def cleanup_terminals(self) -> None:
 | 
						|
        """Shutdown all terminals.
 | 
						|
 | 
						|
        The terminals will shutdown themselves when this process no longer exists,
 | 
						|
        but explicit shutdown allows the TerminalManager to cleanup.
 | 
						|
        """
 | 
						|
        if not self.terminals_available:
 | 
						|
            return
 | 
						|
 | 
						|
        terminal_manager = self.terminal_manager
 | 
						|
        n_terminals = len(terminal_manager.list())
 | 
						|
        terminal_msg = trans.ngettext(
 | 
						|
            "Shutting down %d terminal", "Shutting down %d terminals", n_terminals
 | 
						|
        )
 | 
						|
        self.log.info("%s %% %s", terminal_msg, n_terminals)
 | 
						|
        await ensure_async(terminal_manager.terminate_all())  # type:ignore[arg-type]
 | 
						|
 | 
						|
    async def stop_extension(self) -> None:
 | 
						|
        """Stop the extension."""
 | 
						|
        await self.cleanup_terminals()
 |