Skip to content

Commit

Permalink
feat: add rstudio subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
remlapmot committed Sep 27, 2024
1 parent c0469e3 commit 841962d
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 0 deletions.
2 changes: 2 additions & 0 deletions opensafely/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
info,
jupyter,
pull,
rstudio,
unzip,
upgrade,
)
Expand Down Expand Up @@ -136,6 +137,7 @@ def add_subcommand(cmd, module):
add_subcommand("info", info)
add_subcommand("exec", execute)
add_subcommand("clean", clean)
add_subcommand("rstudio", rstudio)

warn_if_updates_needed(sys.argv)
args = parser.parse_args()
Expand Down
137 changes: 137 additions & 0 deletions opensafely/rstudio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import os
import socket
import sys
import threading
import time
import webbrowser
from pathlib import Path
from sys import platform
from urllib import request

from opensafely import utils


DESCRIPTION = "Run an RStudio Server session using the OpenSAFELY environment"


# poor mans debugging because debugging threads on windows is hard
if os.environ.get("DEBUG", False):

def debug(msg):
# threaded output for some reason needs the carriage return or else
# it doesn't reset the cursor.
sys.stderr.write("DEBUG: " + msg.replace("\n", "\r\n") + "\r\n")
sys.stderr.flush()

else:

def debug(msg):
pass


def add_arguments(parser):
parser.add_argument(
"--directory",
"-d",
default=os.getcwd(),
type=Path,
help="Directory to run the RStudio Server session in (default is current dir)",
)
parser.add_argument(
"--name",
help="Name of docker image (defaults to use directory name)",
default="rstudio",
)

parser.add_argument(
"--port",
"-p",
default=8787,
help="Port to run on",
)


def open_browser(name, port):
try:
url = f"http://localhost:{port}"
debug(f"open_browser: url={url}")

# wait for port to be open
debug("open_browser: waiting for port")
start = time.time()
while time.time() - start < 60.0:
try:
response = request.urlopen(url, timeout=1)
except (request.URLError, OSError):
pass
else:
break

if not response:
debug("open_browser: open_browser: could not get response")
return

# open a webbrowser pointing to the docker container
debug("open_browser: open_browser: opening browser window")
webbrowser.open(url, new=2)

except Exception:
# reformat exception printing to work from thread
import traceback

sys.stderr.write("Error in open browser thread:\r\n")
tb = traceback.format_exc().replace("\n", "\r\n")
sys.stderr.write(tb)
sys.stderr.flush()


def get_free_port():
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
return port


def main(directory, name, port):
if name is None:
name = f"os-rstudio-{directory.name}"

if port is None:
# this is a race condition, as something else could consume the socket
# before docker binds to it, but the chance of that on a user's
# personal machine is very small.
port = str(get_free_port())

# if not no_browser:
# start thread to open web browser
thread = threading.Thread(target=open_browser, args=(name, port), daemon=True)
thread.name = "browser thread"
debug("starting open_browser thread")
thread.start()

# Determine if on Linux, if so obtain user id
# And need to know in Windows win32 for text file line endings setting
if platform == "linux":
uid = os.getuid()
else:
uid = None

docker_args = [
"--platform=linux/amd64",
f"-p={port}:8787",
f"--name={name}",
f"--hostname={name}",
"--volume="
+ os.path.join(os.path.expanduser("~"), ".gitconfig")
+ ":/home/rstudio/local-gitconfig",
f"--env=HOSTPLATFORM={platform}",
f"--env=HOSTUID={uid}",
]

debug("docker: " + " ".join(docker_args))
ps = utils.run_docker(
docker_args, "rstudio", "", interactive=True, user="0:0", directory=directory
)
# we want to exit with the same code that rstudio-server did
return ps.returncode
38 changes: 38 additions & 0 deletions tests/test_rstudio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os
import pathlib
from sys import platform

from opensafely import rstudio
from tests.conftest import run_main


def test_rstudio(run):
if platform == "linux":
uid = os.getuid()
else:
uid = None

run.expect(
[
"docker",
"run",
"--rm",
"--init",
"--label=opensafely",
"--interactive",
"--user=0:0",
f"--volume={pathlib.Path.cwd()}://workspace",
"--platform=linux/amd64",
"-p=8787:8787",
"--name=test_rstudio",
"--hostname=test_rstudio",
"--volume="
+ os.path.join(os.path.expanduser("~"), ".gitconfig")
+ ":/home/rstudio/local-gitconfig",
"--env=HOSTPLATFORM=" + platform,
f"--env=HOSTUID={uid}",
"ghcr.io/opensafely-core/rstudio",
]
)

assert run_main(rstudio, "--port 8787 --name test_rstudio") == 0

0 comments on commit 841962d

Please sign in to comment.