Skip to content

Commit f1ce2c5

Browse files
committed
Fix JetBrains persistence and toolbox backend reuse
1 parent 48c218a commit f1ce2c5

4 files changed

Lines changed: 85 additions & 75 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ the workspace in Coder.
3030
| `rtk` | [rtk](https://github.com/rtk-ai/rtk), token-reducing Claude proxy | Auto-patches Claude Code via a post-create hook at workspace start |
3131
| `nvm` | [nvm](https://github.com/nvm-sh/nvm) at `/usr/local/share/nvm` | Default Node = LTS, `node`/`npm`/`npx` in `/usr/local/bin` |
3232
| `web-shell` | [web-shell](https://github.com/SoureCode/web-shell), persistent browser terminal | systemd unit, registered as a Coder app |
33-
| `jetbrains` | JetBrains Gateway remote backend persistence | Headless-only: Toolbox/Gateway runs on the user's local machine, opens a `jetbrains-gateway://` URL that SSHes in and runs `remote-dev-server.sh` here. Declares `~/.cache/JetBrains/`, `~/.config/JetBrains/`, `~/.local/share/JetBrains/`, `~/.java/.userPrefs/jetbrains/` to `home-persist` so the downloaded IDE backend, per-IDE settings, plugins, project indexes and JetProfile login survive workspace restarts. |
33+
| `jetbrains` | JetBrains Toolbox workspace integration | Uses Coder's JetBrains Toolbox module (`registry.coder.com/coder/jetbrains/coder`). Persists `~/.config/JetBrains/`, `~/.local/share/JetBrains/`, and `~/.java/.userPrefs/jetbrains/` for settings/plugins and JetProfile/license state. On startup, writes Toolbox `environment.json` (`allowUpdate=false`) and pins backend install location to `/mnt/home-persist/.jetbrains-dist` to avoid per-restart re-downloads, enforces `idea.properties` path split (`config/plugins` persisted, `system/log` in `/tmp`), prunes persisted `Daemon`, Toolbox `download/backup`, and per-IDE `caches/logs`, and does not persist `~/.cache/JetBrains/`. |
3434
| `home-persist` | Manifest-driven `$HOME` persistence | Reads `/etc/home-persist.d/*.json`, symlinks declared paths under `/mnt/home-persist` (per-owner volume). Add extra per-workspace paths via the `home_persist_paths` Coder parameter. See [`docs/persistence.md`](docs/persistence.md). |
3535
| `llvm` (cpp) | Clang toolchain via [apt.llvm.org](https://apt.llvm.org/) | `CC=clang`, `CXX=clang++` via `/etc/profile.d/llvm-env.sh` |
3636
| `cmake` (cpp) | CMake from [Kitware's GitHub releases](https://cmake.org/) | latest by default |

docs/persistence.md

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,6 @@ opt in.
6666
single-writer semantics (lock files, unix sockets, per-project
6767
indexes) — otherwise two concurrent workspaces race and one fails.
6868

69-
```json
70-
{
71-
"source": "jetbrains-local",
72-
"scope": "workspace",
73-
"paths": [".cache/JetBrains/"]
74-
}
75-
```
76-
7769
**Sibling rule**: owner-scoped and workspace-scoped paths must not nest
7870
under each other. A path symlinked at the parent already points into the
7971
shared volume; a child symlink would land inside that target and leak
@@ -108,14 +100,13 @@ opt in.
108100
│ │ .config/… .local/share/… .claude/ … │
109101
│ │ │
110102
│ └─ .workspaces/<CODER_WORKSPACE_ID>/ workspace-scoped │
111-
│ ├─ <id-A>/ .cache/JetBrains/ … (private to ws A)
112-
│ ├─ <id-B>/ .cache/JetBrains/ … (private to ws B)
113-
│ └─ <id-C>/ .cache/JetBrains/ … (private to ws C)
103+
│ ├─ <id-A>/ <workspace-only paths>
104+
│ ├─ <id-B>/ <workspace-only paths>
105+
│ └─ <id-C>/ <workspace-only paths>
114106
│ │
115107
│ workspace A mounts /mnt/home-persist │
116108
│ └─► $HOME/.config/JetBrains → /mnt/home-persist/.config/... │
117-
│ └─► $HOME/.cache/JetBrains → /mnt/home-persist/.workspaces │
118-
│ /<ws-A-id>/.cache/JetBrains │
109+
│ └─► $HOME/.local/share/JetBrains → /mnt/home-persist/... │
119110
└─────────────────────────────────────────────────────────────────┘
120111
```
121112

@@ -136,8 +127,7 @@ Properties that fall out:
136127
| Source | Scope | Paths | Why |
137128
| ----------------- | ----------- | ----------------------------- | ------------------------------------ |
138129
| `claude-code` | owner | `.claude/`, `.claude.json` | Login credentials, sessions, plugins |
139-
| `jetbrains` | owner | `.config/JetBrains/`, `.local/share/JetBrains/`, `.java/.userPrefs/jetbrains/` | Settings, plugins, and JetProfile state that should follow the user across workspaces. Keymaps, color schemes, installed plugins, license acceptance. |
140-
| `jetbrains-local` | workspace | `.cache/JetBrains/` | Per-workspace runtime: the SSH-deployed Toolbox Agent (`Toolbox-CLI-dist/`), its IPC lock and unix socket under `Toolbox/ports/`, the downloaded IDE backend (`RemoteDev/dist/`), and per-IDE system caches and project indexes. Must be per-workspace — concurrent workspaces that share `.cache/JetBrains/` race on the Toolbox Agent's `UnixApplicationStartLock` and fail to connect ("main instance is alive, cannot bind twice"). |
130+
| `jetbrains` | owner | `.config/JetBrains/`, `.local/share/JetBrains/`, `.java/.userPrefs/jetbrains/` | Settings, plugins, and JetProfile/license state that should follow the user across workspaces. |
141131

142132
Anything not declared is image-owned (or per-workspace-home-volume-owned)
143133
and resets on image rebuild — git config, SSH keys, bash history, caches.
@@ -244,22 +234,44 @@ ls /mnt/home-persist/.workspaces/
244234
rm -rf /mnt/home-persist/.workspaces/<stale-id>
245235
```
246236

247-
## Migrating an owner-scoped path to workspace-scoped
237+
## Migrating or Dropping a Persisted Path
248238

249-
Flipping a path from `scope: "owner"` to `scope: "workspace"` leaves the old
250-
`/mnt/home-persist/<path>` dir behind — the resolver retargets the symlink
251-
but doesn't touch the previous target. Tens to hundreds of MB can accumulate
252-
(JetBrains caches, Docker-ish state, etc.).
239+
Changing persistence scope or dropping persistence for a path leaves the old
240+
`/mnt/home-persist/<path>` dir behind — the resolver retargets or removes
241+
symlink ownership, but doesn't clean the previous target automatically.
253242

254243
Add a `migration_sweep` line to `coder_script.lifecycle_init` in `main.tf`,
255244
keyed by a unique sentinel name:
256245

257246
```bash
258247
migration_sweep <sentinel-name> <path-relative-to-/mnt/home-persist>
259248
# e.g.
260-
migration_sweep jetbrains-cache-owner-to-workspace .cache/JetBrains
249+
migration_sweep jetbrains-cache-owner-to-ephemeral .cache/JetBrains
250+
migration_sweep jetbrains-toolbox-download-owner-to-ephemeral .local/share/JetBrains/Toolbox/download
251+
migration_sweep jetbrains-toolbox-backup-owner-to-ephemeral .local/share/JetBrains/Toolbox/backup
261252
```
262253

254+
For JetBrains backend reuse between restarts, `main.tf` creates
255+
`/mnt/home-persist/.jetbrains-dist` and writes Toolbox `environment.json`
256+
with a fixed `tools.location` pointing to that path.
257+
258+
Because `.local/share/JetBrains/` is persisted broadly, `main.tf` startup also
259+
prunes subpaths that should remain ephemeral:
260+
261+
`~/.local/share/JetBrains/Daemon/`
262+
`~/.local/share/JetBrains/Toolbox/download/`
263+
`~/.local/share/JetBrains/Toolbox/backup/`
264+
`~/.local/share/JetBrains/*/{caches,logs}/`
265+
266+
`main.tf` startup also writes `~/.local/share/JetBrains/Toolbox/environment.json`
267+
with `tools.allowUpdate=false` and a pinned tool location, and writes `idea.properties` in each
268+
`~/.config/JetBrains/<IDE>/` directory with:
269+
270+
`idea.config.path=~/.config/JetBrains/<IDE>`
271+
`idea.plugins.path=~/.local/share/JetBrains/<IDE>/plugins`
272+
`idea.system.path=/tmp/jetbrains/system/<IDE>`
273+
`idea.log.path=/tmp/jetbrains/log/<IDE>`
274+
263275
The sweep runs once per owner volume (the sentinel
264276
`/mnt/home-persist/.workspaces/.migrated/<sentinel-name>` blocks reruns) and
265277
is a no-op if the orphan is already gone. Delete the line from `main.tf`

main.tf

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,60 @@ resource "coder_script" "lifecycle_init" {
370370
touch "$sentinel"
371371
}
372372
if [ -w /mnt/home-persist ]; then
373-
migration_sweep jetbrains-cache-owner-to-workspace .cache/JetBrains
373+
migration_sweep jetbrains-cache-owner-to-ephemeral .cache/JetBrains
374+
mkdir -p /mnt/home-persist/.jetbrains-dist
375+
rm -rf /mnt/home-persist/.local/share/JetBrains/Daemon
376+
rm -rf /mnt/home-persist/.local/share/JetBrains/Toolbox/download
377+
rm -rf /mnt/home-persist/.local/share/JetBrains/Toolbox/backup
378+
if [ -d /mnt/home-persist/.local/share/JetBrains ]; then
379+
find /mnt/home-persist/.local/share/JetBrains -mindepth 2 -maxdepth 2 -type d \
380+
\( -name caches -o -name logs \) -exec rm -rf {} +
381+
fi
374382
fi
375383
376384
[ -x /usr/local/bin/home-persist-resolve ] && /usr/local/bin/home-persist-resolve
385+
386+
mkdir -p "$HOME/.local/share/JetBrains/Toolbox"
387+
printf '%s\n' \
388+
'{' \
389+
' "tools": {' \
390+
' "allowUpdate": false,' \
391+
' "location": [' \
392+
' {' \
393+
' "path": "/mnt/home-persist/.jetbrains-dist",' \
394+
' "levels": 1' \
395+
' }' \
396+
' ]' \
397+
' }' \
398+
'}' \
399+
> "$HOME/.local/share/JetBrains/Toolbox/environment.json"
400+
401+
mkdir -p "$HOME/.config/JetBrains" "$HOME/.local/share/JetBrains"
402+
if [ -d "$HOME/.local/share/JetBrains" ]; then
403+
for share_dir in "$HOME"/.local/share/JetBrains/*; do
404+
[ -d "$share_dir" ] || continue
405+
share_name="$(basename "$share_dir")"
406+
case "$share_name" in
407+
Toolbox|Daemon|consentOptions)
408+
continue
409+
;;
410+
esac
411+
mkdir -p "$HOME/.config/JetBrains/$share_name"
412+
done
413+
fi
414+
if [ -d "$HOME/.config/JetBrains" ]; then
415+
for ide_dir in "$HOME"/.config/JetBrains/*; do
416+
[ -d "$ide_dir" ] || continue
417+
ide_name="$(basename "$ide_dir")"
418+
printf '%s\n' \
419+
"idea.config.path=$ide_dir" \
420+
"idea.plugins.path=$HOME/.local/share/JetBrains/$ide_name/plugins" \
421+
"idea.system.path=/tmp/jetbrains/system/$ide_name" \
422+
"idea.log.path=/tmp/jetbrains/log/$ide_name" \
423+
> "$ide_dir/idea.properties"
424+
done
425+
fi
426+
377427
[ -x "$HOME/.local/share/rtk/post-create.sh" ] && "$HOME/.local/share/rtk/post-create.sh"
378428
exit 0
379429
EOT

scripts/jetbrains/install.sh

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,8 @@
11
#!/usr/bin/env bash
2-
# jetbrains installer.
3-
#
4-
# How the Coder JetBrains flow works: the workspace is headless. Clicking the
5-
# IDE button in the Coder UI opens a `jetbrains-gateway://` URL that the
6-
# user's *local* Toolbox/Gateway handles; Gateway then SSHes into the
7-
# workspace and runs remote-dev-server.sh, which downloads the IDE backend
8-
# (IntelliJ IDEA, PyCharm, CLion, WebStorm, GoLand, RubyMine, PhpStorm,
9-
# Rider, DataGrip) on-demand into $HOME. Toolbox itself never runs on the
10-
# workspace. Nothing to install in the image — this script only declares the
11-
# HOME paths the remote-dev backend writes, so Gateway doesn't redownload
12-
# hundreds of MB and users don't lose settings, plugins, and project indexes
13-
# on every restart.
14-
#
15-
# JetBrains' remote-dev backend follows the XDG scheme on Linux with a
16-
# RemoteDev-<Code> subdir per IDE flavor (PS = PhpStorm, PY = PyCharm, IU =
17-
# IDEA Ultimate, CL = CLion, ...) under these three roots:
18-
#
19-
# ~/.cache/JetBrains/ — RemoteDev/dist/<build>/ (downloaded backend),
20-
# RemoteDev-<Code>/ system caches, indexes,
21-
# LocalHistory, log, Toolbox-CLI-dist/ (the
22-
# SSH-deployed Toolbox Agent), Toolbox/ports/
23-
# (agent IPC / lock)
24-
# ~/.config/JetBrains/ — RemoteDev-<Code>/ IDE settings, keymaps,
25-
# schemes, options, workspace, .lock
26-
# ~/.local/share/JetBrains/ — RemoteDev-<Code>/ installed plugins,
27-
# Daemon, consentOptions
28-
#
29-
# Plus the Java Preferences store the IDEs use for JetBrains Account
30-
# (JetProfile) login, license activation, and non-commercial-license
31-
# acceptance — skipping this forces re-login + re-accept every restart:
32-
#
33-
# ~/.java/.userPrefs/jetbrains/
34-
#
35-
# .cache/JetBrains/ is scoped per-workspace because the Toolbox Agent's
36-
# UnixApplicationStartLock + IPC socket live under it and would collide
37-
# across concurrent workspaces ("main instance is alive, cannot bind twice").
38-
# Settings, plugins, and JetProfile state stay owner-scoped so they follow
39-
# the user across workspaces.
402
set -eo pipefail
413

424
mkdir -p /etc/home-persist.d
435

44-
# Owner-scoped: things we want shared across all of the user's workspaces.
456
tee /etc/home-persist.d/jetbrains.json >/dev/null <<'EOF'
467
{
478
"source": "jetbrains",
@@ -53,16 +14,3 @@ tee /etc/home-persist.d/jetbrains.json >/dev/null <<'EOF'
5314
]
5415
}
5516
EOF
56-
57-
# Workspace-scoped: runtime IPC and per-project indexes. Must be private
58-
# per workspace, otherwise concurrent workspaces race on the Toolbox Agent
59-
# lock. Survives restarts of the same workspace.
60-
tee /etc/home-persist.d/jetbrains-local.json >/dev/null <<'EOF'
61-
{
62-
"source": "jetbrains-local",
63-
"scope": "workspace",
64-
"paths": [
65-
".cache/JetBrains/"
66-
]
67-
}
68-
EOF

0 commit comments

Comments
 (0)