Skip to content

Accept an unset command to proxy to an already started process (unmanaged process) #339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jan 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/source/server-process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ pairs.
* A callable that takes any :ref:`callable arguments <server-process/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``
^^^^^^^^^^^
Expand Down
11 changes: 8 additions & 3 deletions jupyter_server_proxy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 12 additions & 1 deletion jupyter_server_proxy/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions tests/resources/jupyter_server_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions tests/test_proxies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down