Skip to content

Commit e0c126f

Browse files
authored
Ensure terminal cwd exists (#755)
1 parent acc8ffc commit e0c126f

File tree

2 files changed

+101
-6
lines changed

2 files changed

+101
-6
lines changed

jupyter_server/terminal/api_handlers.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import json
2-
import os
2+
from pathlib import Path
33

44
from tornado import web
55

@@ -30,11 +30,23 @@ def post(self):
3030
# if cwd is a relative path, it should be relative to the root_dir,
3131
# but if we pass it as relative, it will we be considered as relative to
3232
# the path jupyter_server was started in
33-
if "cwd" in data.keys():
34-
if not os.path.isabs(data["cwd"]):
35-
cwd = data["cwd"]
36-
cwd = os.path.join(self.settings["server_root_dir"], cwd)
37-
data["cwd"] = cwd
33+
if "cwd" in data:
34+
cwd = Path(data["cwd"])
35+
if not cwd.resolve().exists():
36+
cwd = Path(self.settings["server_root_dir"]).expanduser() / cwd
37+
if not cwd.resolve().exists():
38+
cwd = None
39+
40+
if cwd is None:
41+
server_root_dir = self.settings["server_root_dir"]
42+
self.log.debug(
43+
f"Failed to find requested terminal cwd: {data.get('cwd')}\n"
44+
f" It was not found within the server root neither: {server_root_dir}."
45+
)
46+
del data["cwd"]
47+
else:
48+
self.log.debug(f"Opening terminal in: {cwd.resolve()!s}")
49+
data["cwd"] = str(cwd.resolve())
3850

3951
model = self.terminal_manager.create(**data)
4052
self.finish(json.dumps(model))

tests/test_terminal.py

+83
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import os
44
import shutil
5+
import sys
56

67
import pytest
78
from tornado.httpclient import HTTPClientError
@@ -18,6 +19,16 @@ def terminal_path(tmp_path):
1819
shutil.rmtree(str(subdir), ignore_errors=True)
1920

2021

22+
@pytest.fixture
23+
def terminal_root_dir(jp_root_dir):
24+
subdir = jp_root_dir.joinpath("terminal_path")
25+
subdir.mkdir()
26+
27+
yield subdir
28+
29+
shutil.rmtree(str(subdir), ignore_errors=True)
30+
31+
2132
CULL_TIMEOUT = 10
2233
CULL_INTERVAL = 3
2334

@@ -137,6 +148,78 @@ async def test_terminal_create_with_cwd(
137148
await jp_cleanup_subprocesses()
138149

139150

151+
async def test_terminal_create_with_relative_cwd(
152+
jp_fetch, jp_ws_fetch, jp_root_dir, terminal_root_dir, jp_cleanup_subprocesses
153+
):
154+
resp = await jp_fetch(
155+
"api",
156+
"terminals",
157+
method="POST",
158+
body=json.dumps({"cwd": str(terminal_root_dir.relative_to(jp_root_dir))}),
159+
allow_nonstandard_methods=True,
160+
)
161+
162+
data = json.loads(resp.body.decode())
163+
term_name = data["name"]
164+
165+
ws = await jp_ws_fetch("terminals", "websocket", term_name)
166+
167+
ws.write_message(json.dumps(["stdin", "pwd\r\n"]))
168+
169+
message_stdout = ""
170+
while True:
171+
try:
172+
message = await asyncio.wait_for(ws.read_message(), timeout=5.0)
173+
except asyncio.TimeoutError:
174+
break
175+
176+
message = json.loads(message)
177+
178+
if message[0] == "stdout":
179+
message_stdout += message[1]
180+
181+
ws.close()
182+
183+
expected = terminal_root_dir.name if sys.platform == "win32" else str(terminal_root_dir)
184+
assert expected in message_stdout
185+
await jp_cleanup_subprocesses()
186+
187+
188+
async def test_terminal_create_with_bad_cwd(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses):
189+
non_existing_path = "/tmp/path/to/nowhere"
190+
resp = await jp_fetch(
191+
"api",
192+
"terminals",
193+
method="POST",
194+
body=json.dumps({"cwd": non_existing_path}),
195+
allow_nonstandard_methods=True,
196+
)
197+
198+
data = json.loads(resp.body.decode())
199+
term_name = data["name"]
200+
201+
ws = await jp_ws_fetch("terminals", "websocket", term_name)
202+
203+
ws.write_message(json.dumps(["stdin", "pwd\r\n"]))
204+
205+
message_stdout = ""
206+
while True:
207+
try:
208+
message = await asyncio.wait_for(ws.read_message(), timeout=5.0)
209+
except asyncio.TimeoutError:
210+
break
211+
212+
message = json.loads(message)
213+
214+
if message[0] == "stdout":
215+
message_stdout += message[1]
216+
217+
ws.close()
218+
219+
assert non_existing_path not in message_stdout
220+
await jp_cleanup_subprocesses()
221+
222+
140223
async def test_culling_config(jp_server_config, jp_configurable_serverapp):
141224
terminal_mgr_config = jp_configurable_serverapp().config.ServerApp.TerminalManager
142225
assert terminal_mgr_config.cull_inactive_timeout == CULL_TIMEOUT

0 commit comments

Comments
 (0)