View on 📖 Notion

    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:


    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:

    1. 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.
    2. 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?

    3. Lifecycle management: invokes the remote-dev-server.sh to bootstrap the IDE server, and handles signal for graceful shutdown.
    4. Serve HTTP Endpoints: exposes /status for supervisor health check, and /joinLink for gateway plugin fetching a one-time join token for connection. /joinLink requests are proxied to http://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:

    1. download build stage:
      1. fetch the IDE backend tar file and extract them into the workdir ($IDE_HOME). The download URL (and other arguments) are defined at BUILD.yaml which is Gitpod’s custom build script.
      2. 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.
    2. final build stage:
      1. copy IntelliJ platform’s components (some properties files are later overwritten by status)
      2. copy Gitpod supervisor config file & entrypoint scripts, etc.

    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.

    1. Setup initial variables and validate environment state
    2. Configure fonts and fontconfig
    3. Run help command after setting up libraries and fonts
    4. Run PROJECT_PATH checks after commands that could run without the project path
    5. Set default config and system directories
    6. Patch JBR (JetBrains JRE) to make self-contained JVM (requires nothing from host system except glibc)
    7. Display project trust warning
    8. Set Remote Development properties
    9. Set Remote Development vmoptions
    10. Set password (Code-With-Me)
    11. Run the IDE (invoke IDE script, i.e. idea.sh)

    5. idea.sh

    or goland.shphpstorm.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 \
      "$@"