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 16 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, it is assumed by the
proxy that the process is already running and available on ``{port}``.

``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, it is assumed the process is already
running, therefore the port checking is skipped and the proxy is set up on the specified
port.

environment
A dictionary of environment variable mappings. As with the command
traitlet, {{port}} and {{base_url}} will be substituted.
Expand Down
9 changes: 8 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 @@ -650,6 +654,9 @@ async def ensure_process(self):
proc = SupervisedProcess(self.name, *cmd, env=server_env, ready_func=self._http_ready_func, ready_timeout=timeout, log=self.log)
self.state['proc'] = proc

if not cmd:
return

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this called to late? Should self.state['proc'] be set to a new SupervisedProcess object? The idea was that if the command is unset, it shouldn't be supervised - right?

If it is correct, a comment is needed to clarify why it is correct in my mind as this is quite unexpected for me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides this, the changes looks good to me!

Copy link
Member

@consideRatio consideRatio Dec 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like it is now, it won't be awaited readiness either. I'll push a commit to move this logic upwards: d6fbbd8

try:
await proc.start()

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