Skip to content

Commit 462211d

Browse files
authored
[vscode-extension] reopen vscode in devbox env using process communication (#1075)
## Summary Added ability to reload a vscode window and re-open it in a devbox environment. This is done in 4 steps: 1. Devbox extension invokes devbox CLI (devbox integrate vscode) as a child process. 2. CLI then prepares the devbox environment (computeNixEnv) and sends a message to parent process that preparing new environment is finished and ready. 3. Parent process (vscode window) upon receiving the finish message, closes itself. 4. CLI will open a new vscode window (cmd.execute) with env variables it has received from setting up devbox environment. NOTE: This required changes to both CLI and the extension, so the release of extension has to happen after CLI. Addresses #1029 ## How was it tested? 1. compile CLI and put the binary in an directory with devbox.json (let's call it `vstest/`) 2. specify a package (e.g., hello@latest) and an env variable in devbox.json (e.g., "foo": "bar123") 3. in devbox.ts update `const devbox = 'devbox';` to `const devbox = <absolute path to compiled CLI binary>` 4. `cd devbox/vscode-extension` then package an installable extensinon file: `vsce package`.(this will create a devbox-version.vsix file) 5. install this extension on vscode by `code --install-extension path-to-vsix-file` 6. open `vstest/` in vscode. 7. In vscode's terminal run `echo $foo` it should be empty. Then type `which hello` and it should point to devbox environment's hello, or say command not found 8. open command palette in vscode (cmd + shift + p) and type "reopen in devbox" and choose the suggested action. 9. once vscode is re-opened, try step 7 and confirm that vscode now has devbox environment setup.
1 parent f19dbfc commit 462211d

File tree

10 files changed

+258
-3
lines changed

10 files changed

+258
-3
lines changed

devbox.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Devbox interface {
3434
IsEnvEnabled() bool
3535
ListScripts() []string
3636
PrintEnv(opts *devopt.PrintEnv) (string, error)
37+
PrintEnvVars(ctx context.Context) ([]string, error)
3738
PrintGlobalList() error
3839
Pull(ctx context.Context, overwrite bool, path string) error
3940
Push(url string) error

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ require (
4141

4242
require github.com/lib/pq v1.10.7 // indirect
4343

44+
require github.com/zealic/go2node v0.1.0 // indirect
45+
4446
require (
4547
github.com/InVisionApp/go-health/v2 v2.1.3 // indirect
4648
github.com/InVisionApp/go-logger v1.0.1 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
163163
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
164164
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
165165
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
166+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
166167
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
167168
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
168169
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -179,6 +180,8 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB
179180
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
180181
github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
181182
github.com/zaffka/mongodb-boltdb-mock v0.0.0-20221014194232-b4bb03fbe3a0/go.mod h1:GsDD1qsG+86MeeCG7ndi6Ei3iGthKL3wQ7PTFigDfNY=
183+
github.com/zealic/go2node v0.1.0 h1:ofxpve08cmLJBwFdI0lPCk9jfwGWOSD+s6216x0oAaA=
184+
github.com/zealic/go2node v0.1.0/go.mod h1:GrkFr+HctXwP7vzcU9RsgtAeJjTQ6Ud0IPCQAqpTfBg=
182185
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
183186
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
184187
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=

internal/boxcli/integrate.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package boxcli
5+
6+
import (
7+
"bytes"
8+
"encoding/json"
9+
"os/exec"
10+
11+
"github.com/spf13/cobra"
12+
"github.com/zealic/go2node"
13+
"go.jetpack.io/devbox"
14+
"go.jetpack.io/devbox/internal/debug"
15+
"go.jetpack.io/devbox/internal/impl/devopt"
16+
)
17+
18+
type integrateCmdFlags struct {
19+
config configFlags
20+
}
21+
22+
func integrateCmd() *cobra.Command {
23+
command := &cobra.Command{
24+
Use: "integrate",
25+
Short: "integrate with an IDE",
26+
Args: cobra.MaximumNArgs(1),
27+
Hidden: true,
28+
PreRunE: ensureNixInstalled,
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
return cmd.Help()
31+
},
32+
}
33+
command.AddCommand(integrateVSCodeCmd())
34+
return command
35+
}
36+
37+
func integrateVSCodeCmd() *cobra.Command {
38+
flags := integrateCmdFlags{}
39+
command := &cobra.Command{
40+
Use: "vscode",
41+
Hidden: true,
42+
Short: "Integrate devbox environment with VSCode.",
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
return runIntegrateVSCodeCmd(cmd)
45+
},
46+
}
47+
flags.config.register(command)
48+
49+
return command
50+
}
51+
52+
type parentMessage struct {
53+
ConfigDir string `json:"configDir"`
54+
}
55+
56+
func runIntegrateVSCodeCmd(cmd *cobra.Command) error {
57+
58+
// Setup process communication with node as parent
59+
channel, err := go2node.RunAsNodeChild()
60+
if err != nil {
61+
return err
62+
}
63+
64+
// Get config dir as a message from parent process
65+
msg, err := channel.Read()
66+
if err != nil {
67+
return err
68+
}
69+
// Parse node process' message
70+
var message parentMessage
71+
if err = json.Unmarshal(msg.Message, &message); err != nil {
72+
return err
73+
}
74+
75+
// todo: add error handling - consider sending error message to parent process
76+
box, err := devbox.Open(&devopt.Opts{
77+
Dir: message.ConfigDir,
78+
Writer: cmd.OutOrStdout(),
79+
})
80+
if err != nil {
81+
return err
82+
}
83+
// Get env variables of a devbox shell
84+
envVars, err := box.PrintEnvVars(cmd.Context())
85+
if err != nil {
86+
return err
87+
}
88+
89+
// Send message to parent process to terminate
90+
err = channel.Write(&go2node.NodeMessage{
91+
Message: []byte(`{"status": "finished"}`),
92+
})
93+
if err != nil {
94+
return err
95+
}
96+
// Open vscode with devbox shell environment
97+
cmnd := exec.Command("code", message.ConfigDir)
98+
cmnd.Env = append(cmnd.Env, envVars...)
99+
var outb, errb bytes.Buffer
100+
cmnd.Stdout = &outb
101+
cmnd.Stderr = &errb
102+
err = cmnd.Run()
103+
if err != nil {
104+
debug.Log("out: %s \n err: %s", outb.String(), errb.String())
105+
return err
106+
}
107+
return nil
108+
}

internal/boxcli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func RootCmd() *cobra.Command {
5757
command.AddCommand(infoCmd())
5858
command.AddCommand(initCmd())
5959
command.AddCommand(installCmd())
60+
command.AddCommand(integrateCmd())
6061
command.AddCommand(logCmd())
6162
command.AddCommand(planCmd())
6263
command.AddCommand(removeCmd())

internal/impl/devbox.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,22 @@ func (d *Devbox) PrintEnv(opts *devopt.PrintEnv) (string, error) {
346346
return envStr, nil
347347
}
348348

349+
func (d *Devbox) PrintEnvVars(ctx context.Context) ([]string, error) {
350+
ctx, task := trace.NewTask(ctx, "devboxPrintEnvVars")
351+
defer task.End()
352+
// this only returns env variables for the shell environment excluding hooks
353+
// and excluding "export " prefix in "export key=value" format
354+
if err := d.ensurePackagesAreInstalled(ctx, ensure); err != nil {
355+
return nil, err
356+
}
357+
358+
envs, err := d.nixEnv(ctx)
359+
if err != nil {
360+
return nil, err
361+
}
362+
return keyEqualsValue(envs), nil
363+
}
364+
349365
func (d *Devbox) ShellEnvHash(ctx context.Context) (string, error) {
350366
envs, err := d.nixEnv(ctx)
351367
if err != nil {

internal/impl/envvars.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ func pairsToMap(pairs []string) map[string]string {
3232
return vars
3333
}
3434

35-
// exportify takes an array of strings of the form VAR=VAL and returns a bash script
36-
// that exports all the vars after properly escaping them.
35+
// exportify takes a map of [string]string and returns a single string
36+
// of the form export KEY="VAL"; and escapes all the vals from special characters.
3737
func exportify(vars map[string]string) string {
3838
keys := make([]string, 0, len(vars))
3939
for k := range vars {
@@ -60,6 +60,56 @@ func exportify(vars map[string]string) string {
6060
return strings.TrimSpace(strb.String())
6161
}
6262

63+
// exportify takes a map of [string]string and returns an array of string
64+
// of the form KEY="VAL" and escapes all the vals from special characters.
65+
func keyEqualsValue(vars map[string]string) []string {
66+
keys := make([]string, 0, len(vars))
67+
for k := range vars {
68+
keys = append(keys, k)
69+
}
70+
keyValues := make([]string, 0, len(vars))
71+
sort.Strings(keys)
72+
73+
for _, k := range keys {
74+
if isApproved(k) {
75+
strb := strings.Builder{}
76+
strb.WriteString(k)
77+
strb.WriteString(`=`)
78+
for _, r := range vars[k] {
79+
switch r {
80+
// Special characters inside double quotes:
81+
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
82+
case '$', '`', '"', '\\', '\n':
83+
strb.WriteRune('\\')
84+
}
85+
strb.WriteRune(r)
86+
}
87+
keyValues = append(keyValues, strb.String())
88+
}
89+
}
90+
return keyValues
91+
}
92+
93+
func isApproved(key string) bool {
94+
// list to keys
95+
// should find the corrupt key
96+
troublingEnvKeys := []string{
97+
"HOME",
98+
"NODE_CHANNEL_FD",
99+
}
100+
approved := true
101+
for _, ak := range troublingEnvKeys {
102+
// DEVBOX_OG_PATH_<hash> being set causes devbox global shellenv or overwrite
103+
// the PATH after vscode opens and resets it to global shellenv
104+
// This causes vscode terminal to not be able to find devbox packages
105+
// after reopen in devbox environment action is called
106+
if key == ak || strings.HasPrefix(key, "DEVBOX_OG_PATH") {
107+
approved = false
108+
}
109+
}
110+
return approved
111+
}
112+
63113
// addEnvIfNotPreviouslySetByDevbox adds the key-value pairs from new to existing,
64114
// but only if the key was not previously set by devbox
65115
// Caveat, this won't mark the values as set by devbox automatically. Instead,

vscode-extension/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
"command": "devbox.setupDevContainer",
3131
"title": "Devbox: Generate Dev Containers config files"
3232
},
33+
{
34+
"command": "devbox.reopen",
35+
"title": "Devbox: Reopen in Devbox shell environment"
36+
},
3337
{
3438
"command": "devbox.generateDockerfile",
3539
"title": "Devbox: Generate a Dockerfile from devbox.json"
@@ -61,6 +65,9 @@
6165
"command": "devbox.setupDevContainer",
6266
"when": "devbox.configFileExists == true"
6367
},
68+
{
69+
"command": "devbox.reopen"
70+
},
6471
{
6572
"command": "devbox.add",
6673
"when": "devbox.configFileExists == true"
@@ -123,4 +130,4 @@
123130
"node-fetch": "^2",
124131
"which": "^3.0.0"
125132
}
126-
}
133+
}

vscode-extension/src/devbox.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { window, workspace, commands, ProgressLocation, Uri } from 'vscode';
2+
import { spawn, spawnSync } from 'node:child_process';
3+
4+
5+
interface Message {
6+
status: string
7+
}
8+
export async function devboxReopen() {
9+
await window.withProgress({
10+
location: ProgressLocation.Notification,
11+
title: "Setting up your Devbox environment. Please don't close vscode.",
12+
cancellable: true
13+
},
14+
async (progress, token) => {
15+
token.onCancellationRequested(() => {
16+
console.log("User canceled the long running operation");
17+
});
18+
19+
const p = new Promise<void>(async (resolve, reject) => {
20+
21+
if (workspace.workspaceFolders) {
22+
const workingDir = workspace.workspaceFolders[0].uri;
23+
const dotdevbox = Uri.joinPath(workingDir, '/.devbox');
24+
try {
25+
// check if .devbox exists
26+
await workspace.fs.stat(dotdevbox);
27+
} catch (error) {
28+
//.devbox doesn't exist
29+
// running devbox shellenv to create it
30+
spawnSync('devbox', ['shellenv'], {
31+
cwd: workingDir.path
32+
});
33+
}
34+
// To use a custom compiled devbox when testing, change this to an absolute path.
35+
const devbox = 'devbox';
36+
// run devbox integrate and then close this window
37+
let child = spawn(devbox, ['integrate', 'vscode'], {
38+
cwd: workingDir.path,
39+
stdio: [0, 1, 2, 'ipc']
40+
});
41+
// if CLI closes before sending "finished" message
42+
child.on('close', (code: number) => {
43+
console.log("child process closed with exit code:", code);
44+
window.showErrorMessage("Failed to setup devbox environment.");
45+
reject();
46+
});
47+
// send config path to CLI
48+
child.send({ configDir: workingDir.path });
49+
// handle CLI finishing the env and sending "finished"
50+
child.on('message', function (msg: Message, handle) {
51+
if (msg.status === "finished") {
52+
resolve();
53+
commands.executeCommand("workbench.action.closeWindow");
54+
}
55+
});
56+
}
57+
});
58+
return p;
59+
}
60+
);
61+
}

vscode-extension/src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { workspace, window, commands, Uri, ExtensionContext } from 'vscode';
33
import { posix } from 'path';
44

55
import { handleOpenInVSCode } from './openinvscode';
6+
import { devboxReopen } from './devbox';
67

78
// This method is called when your extension is activated
89
// Your extension is activated the very first time the command is executed
@@ -83,6 +84,11 @@ export function activate(context: ExtensionContext) {
8384
await runInTerminal('devbox generate dockerfile', true);
8485
});
8586

87+
const reopen = commands.registerCommand('devbox.reopen', async () => {
88+
await devboxReopen();
89+
});
90+
91+
context.subscriptions.push(reopen);
8692
context.subscriptions.push(devboxAdd);
8793
context.subscriptions.push(devboxRun);
8894
context.subscriptions.push(devboxInit);

0 commit comments

Comments
 (0)