File size: 5,240 Bytes
d1ceb73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""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()