Skip to content

[vscode-extension] reopen vscode in devbox env using process communication #1075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Devbox interface {
IsEnvEnabled() bool
ListScripts() []string
PrintEnv(opts *devopt.PrintEnv) (string, error)
PrintEnvVars(ctx context.Context) ([]string, error)
PrintGlobalList() error
Pull(ctx context.Context, overwrite bool, path string) error
Push(url string) error
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ require (

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

require github.com/zealic/go2node v0.1.0 // indirect

require (
github.com/InVisionApp/go-health/v2 v2.1.3 // indirect
github.com/InVisionApp/go-logger v1.0.1 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand All @@ -179,6 +180,8 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/zaffka/mongodb-boltdb-mock v0.0.0-20221014194232-b4bb03fbe3a0/go.mod h1:GsDD1qsG+86MeeCG7ndi6Ei3iGthKL3wQ7PTFigDfNY=
github.com/zealic/go2node v0.1.0 h1:ofxpve08cmLJBwFdI0lPCk9jfwGWOSD+s6216x0oAaA=
github.com/zealic/go2node v0.1.0/go.mod h1:GrkFr+HctXwP7vzcU9RsgtAeJjTQ6Ud0IPCQAqpTfBg=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
Expand Down
108 changes: 108 additions & 0 deletions internal/boxcli/integrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
// Use of this source code is governed by the license in the LICENSE file.

package boxcli

import (
"bytes"
"encoding/json"
"os/exec"

"github.com/spf13/cobra"
"github.com/zealic/go2node"
"go.jetpack.io/devbox"
"go.jetpack.io/devbox/internal/debug"
"go.jetpack.io/devbox/internal/impl/devopt"
)

type integrateCmdFlags struct {
config configFlags
}

func integrateCmd() *cobra.Command {
command := &cobra.Command{
Use: "integrate",
Short: "integrate with an IDE",
Args: cobra.MaximumNArgs(1),
Hidden: true,
PreRunE: ensureNixInstalled,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
command.AddCommand(integrateVSCodeCmd())
return command
}

func integrateVSCodeCmd() *cobra.Command {
flags := integrateCmdFlags{}
command := &cobra.Command{
Use: "vscode",
Hidden: true,
Short: "Integrate devbox environment with VSCode.",
RunE: func(cmd *cobra.Command, args []string) error {
return runIntegrateVSCodeCmd(cmd)
},
}
flags.config.register(command)

return command
}

type parentMessage struct {
ConfigDir string `json:"configDir"`
}

func runIntegrateVSCodeCmd(cmd *cobra.Command) error {

// Setup process communication with node as parent
channel, err := go2node.RunAsNodeChild()
if err != nil {
return err
}

// Get config dir as a message from parent process
msg, err := channel.Read()
if err != nil {
return err
}
// Parse node process' message
var message parentMessage
if err = json.Unmarshal(msg.Message, &message); err != nil {
return err
}

// todo: add error handling - consider sending error message to parent process
box, err := devbox.Open(&devopt.Opts{
Dir: message.ConfigDir,
Writer: cmd.OutOrStdout(),
})
if err != nil {
return err
}
// Get env variables of a devbox shell
envVars, err := box.PrintEnvVars(cmd.Context())
if err != nil {
return err
}

// Send message to parent process to terminate
err = channel.Write(&go2node.NodeMessage{
Message: []byte(`{"status": "finished"}`),
})
if err != nil {
return err
}
// Open vscode with devbox shell environment
cmnd := exec.Command("code", message.ConfigDir)
cmnd.Env = append(cmnd.Env, envVars...)
var outb, errb bytes.Buffer
cmnd.Stdout = &outb
cmnd.Stderr = &errb
err = cmnd.Run()
if err != nil {
debug.Log("out: %s \n err: %s", outb.String(), errb.String())
return err
}
return nil
}
1 change: 1 addition & 0 deletions internal/boxcli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func RootCmd() *cobra.Command {
command.AddCommand(infoCmd())
command.AddCommand(initCmd())
command.AddCommand(installCmd())
command.AddCommand(integrateCmd())
command.AddCommand(logCmd())
command.AddCommand(planCmd())
command.AddCommand(removeCmd())
Expand Down
16 changes: 16 additions & 0 deletions internal/impl/devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,22 @@ func (d *Devbox) PrintEnv(opts *devopt.PrintEnv) (string, error) {
return envStr, nil
}

func (d *Devbox) PrintEnvVars(ctx context.Context) ([]string, error) {
ctx, task := trace.NewTask(ctx, "devboxPrintEnvVars")
defer task.End()
// this only returns env variables for the shell environment excluding hooks
// and excluding "export " prefix in "export key=value" format
if err := d.ensurePackagesAreInstalled(ctx, ensure); err != nil {
return nil, err
}

envs, err := d.nixEnv(ctx)
if err != nil {
return nil, err
}
return keyEqualsValue(envs), nil
}

func (d *Devbox) ShellEnvHash(ctx context.Context) (string, error) {
envs, err := d.nixEnv(ctx)
if err != nil {
Expand Down
54 changes: 52 additions & 2 deletions internal/impl/envvars.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ func pairsToMap(pairs []string) map[string]string {
return vars
}

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

// exportify takes a map of [string]string and returns an array of string
// of the form KEY="VAL" and escapes all the vals from special characters.
func keyEqualsValue(vars map[string]string) []string {
keys := make([]string, 0, len(vars))
for k := range vars {
keys = append(keys, k)
}
keyValues := make([]string, 0, len(vars))
sort.Strings(keys)

for _, k := range keys {
if isApproved(k) {
strb := strings.Builder{}
strb.WriteString(k)
strb.WriteString(`=`)
for _, r := range vars[k] {
switch r {
// Special characters inside double quotes:
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
case '$', '`', '"', '\\', '\n':
strb.WriteRune('\\')
}
strb.WriteRune(r)
}
keyValues = append(keyValues, strb.String())
}
}
return keyValues
}

func isApproved(key string) bool {
// list to keys
// should find the corrupt key
troublingEnvKeys := []string{
"HOME",
"NODE_CHANNEL_FD",
}
approved := true
for _, ak := range troublingEnvKeys {
// DEVBOX_OG_PATH_<hash> being set causes devbox global shellenv or overwrite
// the PATH after vscode opens and resets it to global shellenv
// This causes vscode terminal to not be able to find devbox packages
// after reopen in devbox environment action is called
if key == ak || strings.HasPrefix(key, "DEVBOX_OG_PATH") {
approved = false
}
}
return approved
}

// addEnvIfNotPreviouslySetByDevbox adds the key-value pairs from new to existing,
// but only if the key was not previously set by devbox
// Caveat, this won't mark the values as set by devbox automatically. Instead,
Expand Down
9 changes: 8 additions & 1 deletion vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"command": "devbox.setupDevContainer",
"title": "Devbox: Generate Dev Containers config files"
},
{
"command": "devbox.reopen",
"title": "Devbox: Reopen in Devbox shell environment"
},
{
"command": "devbox.generateDockerfile",
"title": "Devbox: Generate a Dockerfile from devbox.json"
Expand Down Expand Up @@ -61,6 +65,9 @@
"command": "devbox.setupDevContainer",
"when": "devbox.configFileExists == true"
},
{
"command": "devbox.reopen"
},
{
"command": "devbox.add",
"when": "devbox.configFileExists == true"
Expand Down Expand Up @@ -123,4 +130,4 @@
"node-fetch": "^2",
"which": "^3.0.0"
}
}
}
61 changes: 61 additions & 0 deletions vscode-extension/src/devbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { window, workspace, commands, ProgressLocation, Uri } from 'vscode';
import { spawn, spawnSync } from 'node:child_process';


interface Message {
status: string
}
export async function devboxReopen() {
await window.withProgress({
location: ProgressLocation.Notification,
title: "Setting up your Devbox environment. Please don't close vscode.",
cancellable: true
},
async (progress, token) => {
token.onCancellationRequested(() => {
console.log("User canceled the long running operation");
});

const p = new Promise<void>(async (resolve, reject) => {

if (workspace.workspaceFolders) {
const workingDir = workspace.workspaceFolders[0].uri;
const dotdevbox = Uri.joinPath(workingDir, '/.devbox');
try {
// check if .devbox exists
await workspace.fs.stat(dotdevbox);
} catch (error) {
//.devbox doesn't exist
// running devbox shellenv to create it
spawnSync('devbox', ['shellenv'], {
cwd: workingDir.path
});
}
// To use a custom compiled devbox when testing, change this to an absolute path.
const devbox = 'devbox';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would have to be something else for prod?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LucilleH No, for testing locally on a compiled devbox this would have to point to the path to the compiled devbox.
This comment is mostly a note-to-self for future changes.
For example, if I change something in CLI and want to see how it interacts with the extension, I should change this line to /Users/mohsen/projects/devbox/dist/devbox so that my changes in compiled devbox CLI gets used instead of the installed devbox. Hope that makes sense.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, To use a custom compiled devbox when testing, change this to an absolute path. lol

// run devbox integrate and then close this window
let child = spawn(devbox, ['integrate', 'vscode'], {
cwd: workingDir.path,
stdio: [0, 1, 2, 'ipc']
});
// if CLI closes before sending "finished" message
child.on('close', (code: number) => {
console.log("child process closed with exit code:", code);
window.showErrorMessage("Failed to setup devbox environment.");
reject();
});
// send config path to CLI
child.send({ configDir: workingDir.path });
// handle CLI finishing the env and sending "finished"
child.on('message', function (msg: Message, handle) {
if (msg.status === "finished") {
resolve();
commands.executeCommand("workbench.action.closeWindow");
}
});
}
});
return p;
}
);
}
6 changes: 6 additions & 0 deletions vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { workspace, window, commands, Uri, ExtensionContext } from 'vscode';
import { posix } from 'path';

import { handleOpenInVSCode } from './openinvscode';
import { devboxReopen } from './devbox';

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

const reopen = commands.registerCommand('devbox.reopen', async () => {
await devboxReopen();
});

context.subscriptions.push(reopen);
context.subscriptions.push(devboxAdd);
context.subscriptions.push(devboxRun);
context.subscriptions.push(devboxInit);
Expand Down