This post digs into the bootstrap process of an IntelliJ-based IDE server inside a Gitpod workspace. It’s also a development note during the PullRequest #10175 and helps me understand Gitpod workspace & IntelliJ Platform better.
Some background information:
- Gitpod OSS project: https://github.com/gitpod-io/gitpod
- Gitpod x JetBrains Gateway remote development
Let’s start a Gitpod workspace from JetBrains Gateway plugin and open up a terminal and check what’s running inside the remote workspace:
gitpod /workspace/spring-petclinic (master) $ ps -afx -o pid,ppid,command
PID PPID COMMAND
1 0 supervisor init
36 1 supervisor run
50 36 \_ /ide-desktop/status 24000 intellij Open in IntelliJ IDEA
1109 50 | \_ /bin/sh /ide-desktop/backend/bin/remote-dev-server.sh run /workspace/spring-petclinic
1117 1109 | \_ /bin/sh /ide-desktop/backend/plugins/remote-dev-server/bin/launcher.sh idea.sh IU IDEA idea -Xmx750m run /workspace/spring-petclinic
1236 1117 | \_ /bin/sh /ide-desktop/backend/bin/idea.sh cwmHostNoLobby /workspace/spring-petclinic
1251 1236 | \_ /lib64/ld-linux-x86-64.so.2 --library-path /ide-desktop/backend/plugins/remote-dev-server/selfcontained/lib /workspace/.cache (...truncated)
1353 1251 | \_ /ide-desktop/backend/bin/fsnotifier
1389 1251 | \_ /bin/bash --rcfile /ide-desktop/backend/plugins/terminal/jediterm-bash.in -i
2144 1251 | \_ /bin/bash --rcfile /ide-desktop/backend/plugins/terminal/jediterm-bash.in -i
2404 2144 | \_ gp tasks attach
...
The overall component stacks:
This post covers the Remote part only, the Local part will be covered later
1. Supervisor
supervisor
is the init process and service manager for Gitpod workspace (similar to systemd
or tini
) and it runs as root
with PID1. supervisor
spawns and runs the ide process defined in a JSON config file (src file):
{
"entrypoint": "/ide-desktop/startup.sh",
"entrypointArgs": [ "{DESKTOPIDEPORT}", "intellij", "Open in IntelliJ IDEA" ],
"readinessProbe": {
"type": "http",
"http": {
"path": "/status"
}
}
}
The notable part is entrypoint
and readinessProbe
, which gives hints on how the IDE process is started and managed.
startup.sh
script (src file):
# kill background jobs when the script exits
trap "jobs -p | xargs -r kill" SIGINT SIGTERM EXIT
unset JAVA_TOOL_OPTIONS
exec /ide-desktop/status "$@"
A child process (/ide-desktop/status
) is spawned by exec
command, and it replaces the current shell. That’s the reason why we didn’t see start.sh
in the process tree. So let’s move forward to /ide-desktop/status
.
2. /ide-desktop/status
status
(src file) is a Gitpod binary started by supervisor
, which serves several HTTP endpoints for operational information (maybe that’s the reason it got the name status in the first place), and invokes IntelliJ IDE server script /backend/bin/remote-dev-server.sh
.
The most important duty of status
is IDE startup management, which includes:
- Parse configurations from workspace’s
.gitpod.yml
: the .gitpod.yml spec allows Gitpod users to set preferences such as pre-installed IDE plugins,status
then passes these plugin installation instructions downwards. -
IDE runtime configuration: configures JVM options for the IDE process, also sets necessary environment variables for IDE server / backend IntelliJ plugin. One example is declaring the
IJ_HOST_SYSTEM_BASE_DIR
envvar, which specifies the system directories where IDE preferences are preserved, and Gitpod users won’t lose their personal configs after the workspace hibernated.The IntelliJ platform is a huge system which supports different IDE product lines (such as IntelliJ IDEA / GoLand / Android Studio) by numerous feature flags and extension points, so tuning the ide performance by specifying different properties relies heavily on experiences with the IntelliJ platform (issue #8704). Since JetBrains & Gitpod join forces to provide a seamless remote development experience, maybe we can expect a ”remote-first” IntelliJ platform distribution?
- Lifecycle management: invokes the
remote-dev-server.sh
to bootstrap the IDE server, and handles signal for graceful shutdown. - Serve HTTP Endpoints: exposes
/status
forsupervisor
health check, and/joinLink
for gateway plugin fetching a one-time join token for connection./joinLink
requests are proxied tohttp://localhost:63342/codeWithMe/unattendedHostStatus?token=<CWM_HOST_STATUS_OVER_HTTP_TOKEN>
which might be provided by the pre-installed Code With Me plugin. (The IntelliJ platform embeds a built-in web server and allows plugins to provide REST APIs)
IntelliJ IDE Server inside a Gitpod workspace
Before we dive deeper, let’s take a look at how Gitpod assembles such a containerized environment for the IDE backend server.
The code below is simplified. Full src file
FROM alpine:3.15 as download
RUN curl -sSLo backend.tar.gz "$JETBRAINS_BACKEND_URL" && tar -xf ...
COPY jetbrains-backend-plugin/build/gitpod-remote.zip /workdir/plugins
FROM scratch
COPY ${SUPERVISOR_IDE_CONFIG} /ide-desktop/supervisor-ide-config.json
COPY startup.sh /ide-desktop/
COPY --from=download /workdir/ /ide-desktop/backend/
COPY status /ide-desktop
Some explanations of the Dockerfile:
download
build stage:- fetch the IDE backend tar file and extract them into the workdir (
$IDE_HOME
). The download URL (and other arguments) are defined atBUILD.yaml
which is Gitpod’s custom build script. - copy the
gitpod-remote
plugin artifact into the “plugin” directory under$IDE_HOME
and unpack the plugin zip file. The plugin will then be bundled into IDE when started.
- fetch the IDE backend tar file and extract them into the workdir (
final
build stage:- copy IntelliJ platform’s components (some properties files are later overwritten by
status
) - copy Gitpod supervisor config file & entrypoint scripts, etc.
- copy IntelliJ platform’s components (some properties files are later overwritten by
Now we finish the Gitpod’s part and move into the wrapper scripts of IDE server: remote-dev-server.sh, launcher.sh, idea.sh
3. /backend/bin/remote-dev-server.sh
Each JetBrains IDE (IntelliJ / PyCharm / GoLand) backend server directory contains a remote-dev-server.sh
, which passes IDE product identifier (such as idea
/ goland
) to $IDE_HOME/plugins/remote-dev-server/bin/launcher.sh
and that’s all.
while true; do
set +e
"$REMOTE_DEV_SERVER_LAUNCHER_PATH" "goland.sh" "GO" "GOLAND" "goland" "-Xmx750m" "$@"
# "$REMOTE_DEV_SERVER_LAUNCHER_PATH" "idea.sh" "IU" "IDEA" "idea" "-Xmx750m" "$@"
host_exit_code=$?
set -e
# restart on special exit code, otherwise forward the exit code to caller
if [ $host_exit_code -ne $IDEA_RESTART_VIA_EXIT_CODE ]; then
exit $host_exit_code
fi
done
4. launcher.sh
launcher.sh
takes care of all the IDE agnostic logic for a backend server startup, and is reused by IntelliJ / PyCharm / GoLand, etc.
- Setup initial variables and validate environment state
- Configure fonts and fontconfig
- Run help command after setting up libraries and fonts
- Run PROJECT_PATH checks after commands that could run without the project path
- Set default config and system directories
- Patch JBR (JetBrains JRE) to make self-contained JVM (requires nothing from host system except glibc)
- Display project trust warning
- Set Remote Development properties
- Set Remote Development vmoptions
- Set password (Code-With-Me)
- Run the IDE (invoke IDE script, i.e.
idea.sh
)
5. idea.sh
or
goland.sh
,phpstorm.sh
, etc.
At last, the final script which calls the main method of com.intellij.idea.Main
class. It collects the effective JVM options and IDE properties. Classpath varies between different IDE products, since some functionalities are implemented as plugins and libraries outside the IntelliJ platform.
"$JAVA_BIN" \
-classpath "$CLASS_PATH" \
${VM_OPTIONS} \
"-Djb.vmOptionsFile=${USER_VM_OPTIONS_FILE:-${VM_OPTIONS_FILE}}" \
${IDE_PROPERTIES_PROPERTY} \
com.intellij.idea.Main \
"$@"
Thank you so much for reading. This post is also a part of a remote development series:
1. Gitpod workspace with JetBrains Gateway
2. Gitpod Self-hosted installation on Tencent Cloud
3. How JetBrains Gateway works - beyond the black box