diff --git a/.sail/Dockerfile b/.sail/Dockerfile index 2e5b81d..00842a3 100644 --- a/.sail/Dockerfile +++ b/.sail/Dockerfile @@ -1,3 +1,6 @@ FROM codercom/ubuntu-dev-go +# Go module tooling is completely broken. +ENV GO111MODULE=off + LABEL project_root "~/go/src/go.coder.com" diff --git a/README.md b/README.md index b0d4ecd..8ad9a86 100644 --- a/README.md +++ b/README.md @@ -50,3 +50,9 @@ This operation may take a while on a slow connections, but will be fast on follow-up connections to the same server. To disable this feature entirely, pass the `--skipsync` flag. + +### Sync-back + +By default, VS Code changes on the remote server won't be synced back +when the connection closes. To synchronize back to local when the connection ends, +pass the `-b` flag. \ No newline at end of file diff --git a/main.go b/main.go index 3e69bbc..2e6477a 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "os/exec" + "os/signal" "path/filepath" "runtime" "strconv" @@ -24,8 +25,12 @@ func init() { } func main() { - skipSyncFlag := flag.Bool("skipsync", false, "skip syncing local settings and extensions to remote host") - sshFlags := flag.String("ssh-flags", "", "custom SSH flags") + var ( + skipSyncFlag = flag.Bool("skipsync", false, "skip syncing local settings and extensions to remote host") + sshFlags = flag.String("ssh-flags", "", "custom SSH flags") + syncBack = flag.Bool("b", false, "sync extensions back on termination") + ) + flag.Usage = func() { fmt.Printf(`Usage: [-skipsync] %v HOST [DIR] [SSH ARGS...] @@ -58,6 +63,9 @@ More info: https://github.com/codercom/sshcode "-tt", host, `/bin/bash -c 'set -euxo pipefail || exit 1 +# Make sure any currently running code-server is gone so we can overwrite +# the binary. +pkill -9 `+filepath.Base(codeServerPath)+` || true wget -q https://codesrv-ci.cdr.sh/latest-linux -O `+codeServerPath+` mkdir -p ~/.local/share/code-server cd `+filepath.Dir(codeServerPath)+` @@ -74,17 +82,17 @@ chmod +x `+codeServerPath+` flog.Fatal("failed to update code-server: %v", err) } - if !(*skipSyncFlag) { + if !*skipSyncFlag { start := time.Now() flog.Info("syncing settings") - err = syncUserSettings(host) + err = syncUserSettings(host, false) if err != nil { flog.Fatal("failed to sync settings: %v", err) } flog.Info("synced settings in %s", time.Since(start)) flog.Info("syncing extensions") - err = syncExtensions(host) + err = syncExtensions(host, false) if err != nil { flog.Fatal("failed to sync extensions: %v", err) } @@ -131,8 +139,38 @@ chmod +x `+codeServerPath+` break } + ctx, cancel = context.WithCancel(context.Background()) openBrowser(url) - sshCmd.Wait() + + go func() { + defer cancel() + sshCmd.Wait() + }() + + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt) + + select { + case <-ctx.Done(): + case <-c: + } + + if !*syncBack { + flog.Info("shutting down") + return + } + + flog.Info("synchronizing VS Code back to local") + + err = syncExtensions(host, true) + if err != nil { + flog.Fatal("failed to sync extensions back: %v", err) + } + + err = syncUserSettings(host, true) + if err != nil { + flog.Fatal("failed to user settigns extensions back: %v", err) + } } func openBrowser(url string) { @@ -197,40 +235,63 @@ func randomPort() (string, error) { return "", xerrors.Errorf("max number of tries exceeded: %d", maxTries) } -func syncUserSettings(host string) error { +func syncUserSettings(host string, back bool) error { localConfDir, err := configDir() if err != nil { return err } - const remoteSettingsDir = ".local/share/code-server/User" + const remoteSettingsDir = ".local/share/code-server/User/" + + var ( + src = localConfDir + "/" + dest = host + ":" + remoteSettingsDir + ) + + if back { + dest, src = src, dest + } // Append "/" to have rsync copy the contents of the dir. - return rsync(localConfDir+"/", remoteSettingsDir, host, "workspaceStorage", "logs", "CachedData") + return rsync(src, dest, "workspaceStorage", "logs", "CachedData") } -func syncExtensions(host string) error { +func syncExtensions(host string, back bool) error { localExtensionsDir, err := extensionsDir() if err != nil { return err } - const remoteExtensionsDir = ".local/share/code-server/extensions" + const remoteExtensionsDir = ".local/share/code-server/extensions/" + + var ( + src = localExtensionsDir + "/" + dest = host + ":" + remoteExtensionsDir + ) + if back { + dest, src = src, dest + } - return rsync(localExtensionsDir+"/", remoteExtensionsDir, host) + return rsync(src, dest) } -func rsync(src string, dest string, host string, excludePaths ...string) error { - remoteDest := fmt.Sprintf("%s:%s", host, dest) +func rsync(src string, dest string, excludePaths ...string) error { excludeFlags := make([]string, len(excludePaths)) for i, path := range excludePaths { excludeFlags[i] = "--exclude=" + path } - cmd := exec.Command("rsync", append(excludeFlags, "-azv", "--copy-unsafe-links", src, remoteDest)...) + cmd := exec.Command("rsync", append(excludeFlags, "-azvr", + // Only update newer directories, and sync times + // to keep things simple. + "-u", "--times", + "--copy-unsafe-links", + src, dest, + )..., + ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { - return xerrors.Errorf("failed to rsync '%s' to '%s': %w", src, remoteDest, err) + return xerrors.Errorf("failed to rsync '%s' to '%s': %w", src, dest, err) } return nil @@ -240,9 +301,9 @@ func configDir() (string, error) { var path string switch runtime.GOOS { case "linux": - path = os.ExpandEnv("$HOME/.config/Code/User") + path = os.ExpandEnv("$HOME/.config/Code/User/") case "darwin": - path = os.ExpandEnv("$HOME/Library/Application Support/Code/User") + path = os.ExpandEnv("$HOME/Library/Application Support/Code/User/") default: return "", xerrors.Errorf("unsupported platform: %s", runtime.GOOS) } @@ -253,7 +314,7 @@ func extensionsDir() (string, error) { var path string switch runtime.GOOS { case "linux", "darwin": - path = os.ExpandEnv("$HOME/.vscode/extensions") + path = os.ExpandEnv("$HOME/.vscode/extensions/") default: return "", xerrors.Errorf("unsupported platform: %s", runtime.GOOS) }