3a292098ca
fix: correctly open file with vscode if vscode is not started yet
669 lines
18 KiB
Go
669 lines
18 KiB
Go
// Package process - VscodeManager manages a code-server subprocess for VS Code web access.
|
|
package process
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
tools "github.com/kandev/kandev/internal/tools/installer"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// VscodeStatus represents the code-server process status.
|
|
type VscodeStatus string
|
|
|
|
const (
|
|
VscodeStatusStopped VscodeStatus = "stopped"
|
|
VscodeStatusInstalling VscodeStatus = "installing"
|
|
VscodeStatusStarting VscodeStatus = "starting"
|
|
VscodeStatusRunning VscodeStatus = "running"
|
|
VscodeStatusError VscodeStatus = "error"
|
|
)
|
|
|
|
// VscodeInfo holds the current state of the VS Code server.
|
|
type VscodeInfo struct {
|
|
Status VscodeStatus `json:"status"`
|
|
Port int `json:"port"`
|
|
Error string `json:"error,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// VscodeManager manages a code-server subprocess.
|
|
type VscodeManager struct {
|
|
command string // code-server binary name or path
|
|
workDir string
|
|
port int
|
|
theme string // "dark" or "light"
|
|
installStrategy tools.Strategy // optional: auto-installs code-server if not found
|
|
logger *logger.Logger
|
|
|
|
cmd *exec.Cmd
|
|
resolvedPath string // resolved code-server binary path (set after startup)
|
|
status VscodeStatus
|
|
err string
|
|
message string
|
|
mu sync.Mutex
|
|
cancelStart context.CancelFunc // cancels startAsync goroutine
|
|
stopCh chan struct{}
|
|
doneCh chan struct{}
|
|
stopped bool // guards stopCh against double-close
|
|
}
|
|
|
|
// NewVscodeManager creates a new VS Code process manager.
|
|
// The port is allocated via the OS when Start is called.
|
|
func NewVscodeManager(
|
|
command, workDir string,
|
|
theme string,
|
|
strategy tools.Strategy,
|
|
log *logger.Logger,
|
|
) *VscodeManager {
|
|
return &VscodeManager{
|
|
command: command,
|
|
workDir: workDir,
|
|
theme: theme,
|
|
installStrategy: strategy,
|
|
logger: log.WithFields(zap.String("component", "vscode-manager")),
|
|
status: VscodeStatusStopped,
|
|
}
|
|
}
|
|
|
|
// Start launches the code-server process asynchronously.
|
|
// It returns immediately after setting status to "installing" and spawns
|
|
// a background goroutine that resolves the binary, writes theme settings,
|
|
// and starts the process.
|
|
func (v *VscodeManager) Start() {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
if v.status == VscodeStatusRunning || v.status == VscodeStatusStarting || v.status == VscodeStatusInstalling {
|
|
return
|
|
}
|
|
|
|
v.setStatusLocked(VscodeStatusInstalling)
|
|
v.setMessageLocked("Preparing code-server...")
|
|
v.err = ""
|
|
v.stopped = false
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
v.cancelStart = cancel
|
|
|
|
go v.startAsync(ctx)
|
|
}
|
|
|
|
// startAsync runs the full startup sequence in a background goroutine.
|
|
func (v *VscodeManager) startAsync(ctx context.Context) {
|
|
// Write theme settings before starting
|
|
v.writeThemeSettings()
|
|
|
|
// Resolve the binary, auto-installing if needed (the slow part)
|
|
v.setMessage("Installing code-server (this may take a moment)...")
|
|
resolvedPath, err := tools.ResolveBinary(ctx, v.command, nil, v.installStrategy, v.logger)
|
|
if err != nil {
|
|
v.setError(fmt.Sprintf("code-server binary not available: %s", err))
|
|
return
|
|
}
|
|
v.mu.Lock()
|
|
v.resolvedPath = resolvedPath
|
|
v.mu.Unlock()
|
|
|
|
// Allocate a random port via the OS to avoid collisions
|
|
// with other kandev instances or concurrent sessions.
|
|
port, err := allocatePort()
|
|
if err != nil {
|
|
v.setError(fmt.Sprintf("failed to allocate port: %s", err))
|
|
return
|
|
}
|
|
v.mu.Lock()
|
|
v.port = port
|
|
v.mu.Unlock()
|
|
|
|
v.setStatus(VscodeStatusStarting)
|
|
v.setMessage("Starting code-server...")
|
|
|
|
if err := v.startProcess(ctx, resolvedPath); err != nil {
|
|
v.setError(fmt.Sprintf("code-server failed to start: %s", err))
|
|
return
|
|
}
|
|
|
|
v.setStatus(VscodeStatusRunning)
|
|
v.setMessage("")
|
|
v.logger.Info("code-server started",
|
|
zap.Int("port", v.port),
|
|
zap.Int("pid", v.cmd.Process.Pid))
|
|
}
|
|
|
|
// allocatePort finds a free port using the OS.
|
|
func allocatePort() (int, error) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to allocate port: %w", err)
|
|
}
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
_ = listener.Close()
|
|
return port, nil
|
|
}
|
|
|
|
// startProcess creates and starts the code-server subprocess.
|
|
func (v *VscodeManager) startProcess(ctx context.Context, binaryPath string) error {
|
|
workDir := resolveExistingWorkDir(v.workDir, v.logger)
|
|
bindAddr := fmt.Sprintf("0.0.0.0:%d", v.port)
|
|
args := []string{
|
|
"--bind-addr", bindAddr,
|
|
"--auth", "none",
|
|
"--disable-telemetry",
|
|
"--disable-update-check",
|
|
"--user-data-dir", v.userDataDir(),
|
|
}
|
|
// Append workDir as positional argument so code-server opens the folder.
|
|
args = append(args, workDir)
|
|
|
|
v.mu.Lock()
|
|
v.cmd = exec.Command(binaryPath, args...)
|
|
v.cmd.Dir = workDir
|
|
v.workDir = workDir
|
|
// Give code-server its own process group so Stop() can kill the entire
|
|
// process tree (main process + Node.js workers) without affecting agentctl.
|
|
setProcGroup(v.cmd)
|
|
|
|
stderr, err := v.cmd.StderrPipe()
|
|
if err != nil {
|
|
v.mu.Unlock()
|
|
return fmt.Errorf("failed to create stderr pipe: %w", err)
|
|
}
|
|
|
|
if err := v.cmd.Start(); err != nil {
|
|
v.mu.Unlock()
|
|
return fmt.Errorf("failed to start code-server: %w", err)
|
|
}
|
|
|
|
v.stopCh = make(chan struct{})
|
|
v.doneCh = make(chan struct{})
|
|
v.mu.Unlock()
|
|
|
|
// Read stderr in background for logging
|
|
go func() {
|
|
scanner := bufio.NewScanner(stderr)
|
|
for scanner.Scan() {
|
|
v.logger.Debug("code-server", zap.String("line", scanner.Text()))
|
|
}
|
|
}()
|
|
|
|
// Wait for process exit in background
|
|
pid := v.cmd.Process.Pid
|
|
go func() {
|
|
defer close(v.doneCh)
|
|
waitErr := v.cmd.Wait()
|
|
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
if waitErr != nil && v.status != VscodeStatusStopped {
|
|
v.status = VscodeStatusError
|
|
v.err = waitErr.Error()
|
|
v.logger.Error("code-server exited with error",
|
|
zap.Int("pid", pid), zap.Error(waitErr))
|
|
} else {
|
|
v.status = VscodeStatusStopped
|
|
v.logger.Info("code-server exited", zap.Int("pid", pid))
|
|
}
|
|
}()
|
|
|
|
// Wait for code-server to become ready
|
|
if err := v.waitForReady(ctx); err != nil {
|
|
_ = v.Stop(ctx)
|
|
return fmt.Errorf("code-server failed to become ready: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resolveExistingWorkDir(workDir string, log *logger.Logger) string {
|
|
candidate := strings.TrimSpace(workDir)
|
|
if candidate == "" {
|
|
if cwd, err := os.Getwd(); err == nil && cwd != "" {
|
|
return cwd
|
|
}
|
|
return "."
|
|
}
|
|
|
|
info, err := os.Stat(candidate)
|
|
if err == nil && info.IsDir() {
|
|
return candidate
|
|
}
|
|
|
|
// Walk up to the nearest existing parent directory.
|
|
current := candidate
|
|
for {
|
|
parent := filepath.Dir(current)
|
|
if parent == current {
|
|
break
|
|
}
|
|
info, statErr := os.Stat(parent)
|
|
if statErr == nil && info.IsDir() {
|
|
if log != nil {
|
|
log.Warn("vscode workdir missing; using nearest existing parent directory",
|
|
zap.String("requested_workdir", candidate),
|
|
zap.String("fallback_workdir", parent))
|
|
}
|
|
return parent
|
|
}
|
|
current = parent
|
|
}
|
|
|
|
if cwd, cwdErr := os.Getwd(); cwdErr == nil && cwd != "" {
|
|
if log != nil {
|
|
log.Warn("vscode workdir missing; using current directory fallback",
|
|
zap.String("requested_workdir", candidate),
|
|
zap.String("fallback_workdir", cwd))
|
|
}
|
|
return cwd
|
|
}
|
|
if log != nil {
|
|
log.Warn("vscode workdir missing; using relative dot fallback",
|
|
zap.String("requested_workdir", candidate))
|
|
}
|
|
return "."
|
|
}
|
|
|
|
// waitForReady polls the code-server port until it accepts connections.
|
|
func (v *VscodeManager) waitForReady(ctx context.Context) error {
|
|
addr := fmt.Sprintf("127.0.0.1:%d", v.port)
|
|
deadline := time.After(30 * time.Second)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-deadline:
|
|
return fmt.Errorf("timeout waiting for code-server on port %d", v.port)
|
|
case <-v.doneCh:
|
|
return fmt.Errorf("code-server exited before becoming ready")
|
|
default:
|
|
conn, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
|
|
if err == nil {
|
|
_ = conn.Close()
|
|
return nil
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop stops the code-server process.
|
|
func (v *VscodeManager) Stop(ctx context.Context) error {
|
|
v.mu.Lock()
|
|
if v.status == VscodeStatusStopped {
|
|
v.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
v.logger.Info("stopping code-server")
|
|
v.status = VscodeStatusStopped
|
|
|
|
// Cancel any in-progress startAsync goroutine (e.g. slow binary download).
|
|
if v.cancelStart != nil {
|
|
v.cancelStart()
|
|
v.cancelStart = nil
|
|
}
|
|
|
|
if v.stopCh != nil && !v.stopped {
|
|
close(v.stopCh)
|
|
v.stopped = true
|
|
}
|
|
|
|
cmd := v.cmd
|
|
doneCh := v.doneCh
|
|
v.mu.Unlock()
|
|
|
|
if cmd != nil && cmd.Process != nil {
|
|
pid := cmd.Process.Pid
|
|
v.logger.Info("stopping code-server process group",
|
|
zap.Int("pid", pid), zap.Int("pgid", pid))
|
|
logChildProcesses(v.logger, pid)
|
|
|
|
// Phase 1: Graceful SIGTERM to the entire process group.
|
|
if err := terminateProcessGroup(pid); err != nil {
|
|
v.logger.Warn("failed to send SIGTERM to code-server group",
|
|
zap.Int("pgid", pid), zap.Error(err))
|
|
}
|
|
|
|
// Phase 2: Wait for graceful exit, then escalate to SIGKILL.
|
|
if doneCh != nil {
|
|
select {
|
|
case <-doneCh:
|
|
v.logger.Info("code-server stopped gracefully", zap.Int("pid", pid))
|
|
return nil
|
|
case <-ctx.Done():
|
|
v.logger.Warn("context cancelled during SIGTERM wait, force killing",
|
|
zap.Int("pgid", pid))
|
|
case <-time.After(5 * time.Second):
|
|
v.logger.Warn("code-server did not exit after SIGTERM, force killing",
|
|
zap.Int("pgid", pid))
|
|
}
|
|
|
|
if err := killProcessGroup(pid); err != nil {
|
|
v.logger.Warn("failed to force kill code-server group",
|
|
zap.Int("pgid", pid), zap.Error(err))
|
|
}
|
|
select {
|
|
case <-doneCh:
|
|
case <-ctx.Done():
|
|
case <-time.After(2 * time.Second):
|
|
v.logger.Error("code-server still alive after SIGKILL",
|
|
zap.Int("pid", pid))
|
|
}
|
|
}
|
|
} else if doneCh != nil {
|
|
// No process but doneCh exists (startup cancelled before process started)
|
|
select {
|
|
case <-doneCh:
|
|
case <-ctx.Done():
|
|
case <-time.After(2 * time.Second):
|
|
}
|
|
}
|
|
|
|
v.logger.Info("code-server stopped")
|
|
return nil
|
|
}
|
|
|
|
// WaitForRunning blocks until the code-server reaches "running" status.
|
|
// Returns immediately if already running. Returns an error if the status
|
|
// transitions to "error" or "stopped", or if the context is cancelled.
|
|
func (v *VscodeManager) WaitForRunning(ctx context.Context) error {
|
|
for {
|
|
v.mu.Lock()
|
|
s := v.status
|
|
errMsg := v.err
|
|
v.mu.Unlock()
|
|
|
|
switch s {
|
|
case VscodeStatusRunning:
|
|
return nil
|
|
case VscodeStatusError:
|
|
return fmt.Errorf("code-server failed to start: %s", errMsg)
|
|
case VscodeStatusStopped:
|
|
return fmt.Errorf("code-server is stopped")
|
|
case VscodeStatusInstalling, VscodeStatusStarting:
|
|
// still booting — wait and retry
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(500 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
// Info returns the current VS Code server state.
|
|
func (v *VscodeManager) Info() VscodeInfo {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return VscodeInfo{
|
|
Status: v.status,
|
|
Port: v.port,
|
|
Error: v.err,
|
|
Message: v.message,
|
|
}
|
|
}
|
|
|
|
// Port returns the port code-server is bound to.
|
|
func (v *VscodeManager) Port() int {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return v.port
|
|
}
|
|
|
|
// userDataDir returns the path for code-server user data.
|
|
func (v *VscodeManager) userDataDir() string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
home = "."
|
|
}
|
|
return filepath.Join(home, ".kandev", "tools", "code-server-data")
|
|
}
|
|
|
|
// writeThemeSettings ensures VS Code settings.json contains our managed settings.
|
|
// Merges with any existing user settings, overwriting only the keys we manage.
|
|
func (v *VscodeManager) writeThemeSettings() {
|
|
settingsDir := filepath.Join(v.userDataDir(), "User")
|
|
settingsPath := filepath.Join(settingsDir, "settings.json")
|
|
|
|
themeName := "Default Dark Modern"
|
|
if v.theme == "light" {
|
|
themeName = "Default Light Modern"
|
|
}
|
|
|
|
managed := map[string]any{
|
|
"workbench.colorTheme": themeName,
|
|
"security.workspace.trust.enabled": false,
|
|
"workbench.startupEditor": "none",
|
|
"workbench.enableExperiments": false,
|
|
"workbench.accounts.experimental.showEntitlements": false,
|
|
"settingsSync.enabled": false,
|
|
"telemetry.enabled": false,
|
|
"workbench.sideBar.location": "right",
|
|
"window.commandCenter": false,
|
|
"workbench.layoutControl.enabled": false,
|
|
"editor.minimap.autohide": true,
|
|
}
|
|
|
|
// Read existing settings and merge our managed keys on top.
|
|
settings := make(map[string]any)
|
|
if existing, err := os.ReadFile(settingsPath); err == nil {
|
|
_ = json.Unmarshal(existing, &settings)
|
|
}
|
|
for k, v := range managed {
|
|
settings[k] = v
|
|
}
|
|
|
|
data, err := json.MarshalIndent(settings, "", " ")
|
|
if err != nil {
|
|
v.logger.Warn("failed to marshal theme settings", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
if err := os.MkdirAll(settingsDir, 0o755); err != nil {
|
|
v.logger.Warn("failed to create settings dir", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
if err := os.WriteFile(settingsPath, data, 0o644); err != nil {
|
|
v.logger.Warn("failed to write theme settings", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// OpenFile opens a file in the running VS Code instance via the Remote CLI.
|
|
// It uses the code-server bundled Remote CLI script and the VSCODE_IPC_HOOK_CLI
|
|
// environment variable to communicate with the running VS Code process.
|
|
func (v *VscodeManager) OpenFile(ctx context.Context, path string, line, col int) error {
|
|
v.mu.Lock()
|
|
if v.status != VscodeStatusRunning {
|
|
v.mu.Unlock()
|
|
return fmt.Errorf("code-server is not running")
|
|
}
|
|
resolvedPath := v.resolvedPath
|
|
workDir := v.workDir
|
|
v.mu.Unlock()
|
|
|
|
if resolvedPath == "" {
|
|
return fmt.Errorf("code-server binary path not resolved")
|
|
}
|
|
|
|
// Resolve the Remote CLI script relative to the code-server binary.
|
|
remoteCLI := resolveRemoteCLI(resolvedPath)
|
|
if _, err := os.Stat(remoteCLI); err != nil {
|
|
return fmt.Errorf("remote CLI not found at %s: %w", remoteCLI, err)
|
|
}
|
|
|
|
// Find the IPC socket for the running VS Code instance.
|
|
// The IPC socket may not appear immediately after code-server's HTTP port
|
|
// is ready, so poll for it with a timeout.
|
|
ipcSocket, err := waitForVscodeIPCSocket(ctx, 15*time.Second)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find VS Code IPC socket: %w", err)
|
|
}
|
|
|
|
// Build absolute path if relative.
|
|
absPath := path
|
|
if !filepath.IsAbs(absPath) {
|
|
absPath = filepath.Join(workDir, absPath)
|
|
}
|
|
|
|
// Build the --goto argument: <path>:<line>:<col>
|
|
gotoArg := absPath
|
|
if line > 0 {
|
|
gotoArg = fmt.Sprintf("%s:%d", absPath, line)
|
|
if col > 0 {
|
|
gotoArg = fmt.Sprintf("%s:%d", gotoArg, col)
|
|
}
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, remoteCLI, "--goto", gotoArg)
|
|
cmd.Env = append(os.Environ(), "VSCODE_IPC_HOOK_CLI="+ipcSocket)
|
|
cmd.Dir = workDir
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("remote CLI failed: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
|
}
|
|
|
|
v.logger.Info("opened file in VS Code",
|
|
zap.String("path", absPath),
|
|
zap.Int("line", line),
|
|
zap.Int("col", col))
|
|
return nil
|
|
}
|
|
|
|
// resolveRemoteCLI returns the path to the VS Code Remote CLI script
|
|
// relative to the resolved code-server binary.
|
|
// Layout: <binary-dir>/../lib/vscode/bin/remote-cli/code-{platform}.sh
|
|
func resolveRemoteCLI(binaryPath string) string {
|
|
binDir := filepath.Dir(binaryPath) // .../bin/
|
|
baseDir := filepath.Dir(binDir) // .../code-server-x.y.z-os-arch/
|
|
platform := runtime.GOOS // "darwin" or "linux"
|
|
return filepath.Join(baseDir, "lib", "vscode", "bin", "remote-cli", "code-"+platform+".sh")
|
|
}
|
|
|
|
// findVscodeIPCSocket searches /tmp for the most recent vscode-ipc-*.sock file.
|
|
// It validates each candidate by attempting a Unix socket connection to skip
|
|
// stale sockets left behind by crashed VS Code instances.
|
|
func findVscodeIPCSocket() (string, error) {
|
|
tmpDir := os.TempDir()
|
|
entries, err := os.ReadDir(tmpDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read temp dir: %w", err)
|
|
}
|
|
|
|
type sockEntry struct {
|
|
path string
|
|
modTime time.Time
|
|
}
|
|
var socks []sockEntry
|
|
|
|
for _, e := range entries {
|
|
if !strings.HasPrefix(e.Name(), "vscode-ipc-") || !strings.HasSuffix(e.Name(), ".sock") {
|
|
continue
|
|
}
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
socks = append(socks, sockEntry{
|
|
path: filepath.Join(tmpDir, e.Name()),
|
|
modTime: info.ModTime(),
|
|
})
|
|
}
|
|
|
|
if len(socks) == 0 {
|
|
return "", fmt.Errorf("no vscode-ipc-*.sock found in %s", tmpDir)
|
|
}
|
|
|
|
// Sort by modification time descending (most recent first).
|
|
sort.Slice(socks, func(i, j int) bool {
|
|
return socks[i].modTime.After(socks[j].modTime)
|
|
})
|
|
|
|
// Try each socket, most recent first, and return the first one that accepts
|
|
// a connection. This skips stale sockets from crashed processes.
|
|
for _, s := range socks {
|
|
conn, dialErr := net.DialTimeout("unix", s.path, 500*time.Millisecond)
|
|
if dialErr != nil {
|
|
continue
|
|
}
|
|
_ = conn.Close()
|
|
return s.path, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("no live vscode-ipc-*.sock found in %s (%d stale sockets skipped)", tmpDir, len(socks))
|
|
}
|
|
|
|
// waitForVscodeIPCSocket polls for a live VS Code IPC socket until one is found
|
|
// or the timeout/context expires. Code-server may take several seconds after its
|
|
// HTTP port is ready before creating the IPC socket.
|
|
func waitForVscodeIPCSocket(ctx context.Context, timeout time.Duration) (string, error) {
|
|
deadline := time.After(timeout)
|
|
var lastErr error
|
|
|
|
for {
|
|
sock, err := findVscodeIPCSocket()
|
|
if err == nil {
|
|
return sock, nil
|
|
}
|
|
lastErr = err
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", fmt.Errorf("%w (last attempt: %v)", ctx.Err(), lastErr)
|
|
case <-deadline:
|
|
return "", lastErr
|
|
case <-time.After(1 * time.Second):
|
|
}
|
|
}
|
|
}
|
|
|
|
// setStatus updates the status under lock.
|
|
func (v *VscodeManager) setStatus(s VscodeStatus) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
v.status = s
|
|
}
|
|
|
|
// setStatusLocked updates the status (caller must hold v.mu).
|
|
func (v *VscodeManager) setStatusLocked(s VscodeStatus) {
|
|
v.status = s
|
|
}
|
|
|
|
// setMessage updates the message under lock.
|
|
func (v *VscodeManager) setMessage(msg string) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
v.message = msg
|
|
}
|
|
|
|
// setMessageLocked updates the message (caller must hold v.mu).
|
|
func (v *VscodeManager) setMessageLocked(msg string) {
|
|
v.message = msg
|
|
}
|
|
|
|
// setError sets error status with a message, under lock.
|
|
func (v *VscodeManager) setError(errMsg string) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
v.status = VscodeStatusError
|
|
v.err = errMsg
|
|
v.message = ""
|
|
}
|