diff --git a/docs/source/server-process.rst b/docs/source/server-process.rst index 5fde7532..ce71a7aa 100644 --- a/docs/source/server-process.rst +++ b/docs/source/server-process.rst @@ -36,7 +36,8 @@ pairs. * A callable that takes any :ref:`callable arguments `, and returns a list of strings that are used & treated same as above. - This key is required. + If the command is not specified or is an empty list, the server process is + assumed to be started ahead of time and already available to be proxied to. ``timeout`` ^^^^^^^^^^^ diff --git a/jupyter_server_proxy/config.py b/jupyter_server_proxy/config.py index cb231fc1..333c30a6 100644 --- a/jupyter_server_proxy/config.py +++ b/jupyter_server_proxy/config.py @@ -26,6 +26,7 @@ class _Proxy(SuperviseAndProxyHandler): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.name = name + self.command = command self.proxy_base = name self.absolute_url = absolute_url self.requested_port = port @@ -62,7 +63,7 @@ def _realize_rendered_template(self, attribute): return self._render_template(attribute) def get_cmd(self): - return self._realize_rendered_template(command) + return self._realize_rendered_template(self.command) def get_env(self): return self._realize_rendered_template(environment) @@ -121,7 +122,7 @@ def make_server_process(name, server_process_config, serverproxy_config): le = server_process_config.get('launcher_entry', {}) return ServerProcess( name=name, - command=server_process_config['command'], + command=server_process_config.get('command', list()), environment=server_process_config.get('environment', {}), timeout=server_process_config.get('timeout', 5), absolute_url=server_process_config.get('absolute_url', False), @@ -152,12 +153,16 @@ class ServerProxy(Configurable): Value should be a dictionary with the following keys: command - A list of strings that should be the full command to be executed. + An optional list of strings that should be the full command to be executed. The optional template arguments {{port}} and {{base_url}} will be substituted with the port the process should listen on and the base-url of the notebook. Could also be a callable. It should return a list. + If the command is not specified or is an empty list, the server + process is assumed to be started ahead of time and already available + to be proxied to. + environment A dictionary of environment variable mappings. As with the command traitlet, {{port}} and {{base_url}} will be substituted. diff --git a/jupyter_server_proxy/handlers.py b/jupyter_server_proxy/handlers.py index 88d448de..a1dc2c0a 100644 --- a/jupyter_server_proxy/handlers.py +++ b/jupyter_server_proxy/handlers.py @@ -573,6 +573,7 @@ class SuperviseAndProxyHandler(LocalProxyHandler): def __init__(self, *args, **kwargs): self.requested_port = 0 self.mappath = {} + self.command = list() super().__init__(*args, **kwargs) def initialize(self, state): @@ -588,11 +589,14 @@ def port(self): Allocate either the requested port or a random empty port for use by application """ - if 'port' not in self.state: + if 'port' not in self.state and self.command: sock = socket.socket() sock.bind(('', self.requested_port)) self.state['port'] = sock.getsockname()[1] sock.close() + elif 'port' not in self.state: + self.state['port'] = self.requested_port + return self.state['port'] def get_cwd(self): @@ -639,7 +643,14 @@ async def ensure_process(self): if 'proc' not in self.state: # FIXME: Prevent races here # FIXME: Handle graceful exits of spawned processes here + + # When command option isn't truthy, it means its a process not + # to be managed/started by jupyter-server-proxy. This means we + # won't await its readiness or similar either. cmd = self.get_cmd() + if not cmd: + self.state['proc'] = "process not managed by jupyter-server-proxy" + return # Set up extra environment variables for process server_env = os.environ.copy() diff --git a/tests/resources/jupyter_server_config.py b/tests/resources/jupyter_server_config.py index 2f72df89..4eb795e0 100644 --- a/tests/resources/jupyter_server_config.py +++ b/tests/resources/jupyter_server_config.py @@ -82,6 +82,9 @@ def cats_only(response, path): 'command': ['python3', './tests/resources/httpinfo.py', '{port}'], 'rewrite_response': [cats_only, dog_to_cat], }, + 'python-proxyto54321-no-command': { + 'port': 54321 + } } c.ServerProxy.non_service_rewrite_response = hello_to_foo diff --git a/tests/test_proxies.py b/tests/test_proxies.py index 7d780551..ac2b61a8 100644 --- a/tests/test_proxies.py +++ b/tests/test_proxies.py @@ -150,6 +150,18 @@ def test_server_proxy_requested_port(): assert direct.code == 200 +def test_server_proxy_on_requested_port_no_command(): + r = request_get(PORT, '/python-proxyto54321-no-command/ghi', TOKEN) + assert r.code == 200 + s = r.read().decode('ascii') + assert s.startswith('GET /ghi?token=') + assert 'X-Forwarded-Context: /python-proxyto54321-no-command\n' in s + assert 'X-Proxycontextpath: /python-proxyto54321-no-command\n' in s + + direct = request_get(54321, '/ghi', TOKEN) + assert direct.code == 200 + + def test_server_proxy_port_non_absolute(): r = request_get(PORT, '/proxy/54321/jkl', TOKEN) assert r.code == 200