From d51a6fb98f96ee42ec14e55aa90446df532b86c8 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Mon, 9 Jun 2025 19:31:14 -0500 Subject: [PATCH 1/4] Fixes for hot reload --- cmd/cloud.go | 45 +++++++------ cmd/dev.go | 90 +++++++++++++------------ go.mod | 1 - go.sum | 2 - internal/bundler/bundler.go | 25 +++++++ internal/dev/watcher.go | 121 +++++++++++++++------------------- internal/ignore/rules.go | 39 +++++++---- internal/ignore/rules_test.go | 39 +++++++++++ 8 files changed, 210 insertions(+), 152 deletions(-) create mode 100644 internal/ignore/rules_test.go diff --git a/cmd/cloud.go b/cmd/cloud.go index e0af06e3..722d4417 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -127,6 +127,30 @@ var envTemplateFileNames = []string{".env.example", ".env.template"} var border = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(1).BorderForeground(lipgloss.AdaptiveColor{Light: "#999999", Dark: "#999999"}) var redDiff = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#990000", Dark: "#EE0000"}) +func createProjectIgnoreRules(dir string, theproject *project.Project) *ignore.Rules { + // load up any gitignore files + gitignore := filepath.Join(dir, ignore.Ignore) + rules := ignore.Empty() + if util.Exists(gitignore) { + r, err := ignore.ParseFile(gitignore) + if err != nil { + errsystem.New(errsystem.ErrInvalidConfiguration, err, + errsystem.WithContextMessage("Error parsing .gitignore file")).ShowErrorAndExit() + } + rules = r + } + rules.AddDefaults() + + // add any provider specific ignore rules + for _, rule := range theproject.Bundler.Ignore { + if err := rules.Add(rule); err != nil { + errsystem.New(errsystem.ErrInvalidConfiguration, err, + errsystem.WithContextMessage(fmt.Sprintf("Error adding project ignore rule: %s. %s", rule, err))).ShowErrorAndExit() + } + } + return rules +} + var cloudDeployCmd = &cobra.Command{ Use: "deploy", Short: "Deploy project to the cloud", @@ -444,26 +468,7 @@ Examples: logger.Debug("saved project with updated Agents") } - // load up any gitignore files - gitignore := filepath.Join(dir, ignore.Ignore) - rules := ignore.Empty() - if util.Exists(gitignore) { - r, err := ignore.ParseFile(gitignore) - if err != nil { - errsystem.New(errsystem.ErrInvalidConfiguration, err, - errsystem.WithContextMessage("Error parsing .gitignore file")).ShowErrorAndExit() - } - rules = r - } - rules.AddDefaults() - - // add any provider specific ignore rules - for _, rule := range theproject.Bundler.Ignore { - if err := rules.Add(rule); err != nil { - errsystem.New(errsystem.ErrInvalidConfiguration, err, - errsystem.WithContextMessage(fmt.Sprintf("Error adding project ignore rule: %s. %s", rule, err))).ShowErrorAndExit() - } - } + rules := createProjectIgnoreRules(dir, theproject) // create a temp file we're going to use for zip and upload tmpfile, err := os.CreateTemp("", "agentuity-deploy-*.zip") diff --git a/cmd/dev.go b/cmd/dev.go index 85c44031..d384f9e7 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "path/filepath" + "sync" "syscall" "time" @@ -19,7 +20,6 @@ import ( "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/env" "github.com/agentuity/go-common/tui" - "github.com/bep/debounce" "github.com/spf13/cobra" ) @@ -51,7 +51,7 @@ Examples: apiKey, userId := util.EnsureLoggedIn(ctx, log, cmd) theproject := project.EnsureProject(ctx, cmd) dir := theproject.Dir - isDeliberateRestart := false + // var isDeliberateRestart bool checkForUpgrade(ctx, log, false) @@ -167,26 +167,52 @@ Examples: return ok } + runServer := func() { + projectServerCmd, err = dev.CreateRunProjectCmd(processCtx, log, theproject, server, dir, orgId, port, os.Stdout, os.Stderr) + if err != nil { + errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to run project")).ShowErrorAndExit() + } + if err := projectServerCmd.Start(); err != nil { + errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start project: %s", err))).ShowErrorAndExit() + } + pid = projectServerCmd.Process.Pid + // running = true + log.Trace("restarted project server (pid: %d)", pid) + log.Trace("waiting for project server to exit (pid: %d)", pid) + if err := projectServerCmd.Wait(); err != nil { + log.Error("project server (pid: %d) exited with error: %s", pid, err) + } + if projectServerCmd.ProcessState != nil { + log.Debug("project server (pid: %d) exited with code %d", pid, projectServerCmd.ProcessState.ExitCode()) + } else { + log.Debug("project server (pid: %d) exited", pid) + } + } + // Initial build must exit if it fails if !build(true) { return } + var restartingLock sync.Mutex + restart := func() { - isDeliberateRestart = true - build(false) - log.Debug("killing project server") + // prevent multiple restarts from happening at once + restartingLock.Lock() + defer restartingLock.Unlock() dev.KillProjectServer(log, projectServerCmd, pid) - log.Debug("killing project server done") + if build(false) { + log.Trace("build ready") + go runServer() + } } - // debounce a lot of changes at once to avoid multiple restarts in succession - debounced := debounce.New(250 * time.Millisecond) + rules := createProjectIgnoreRules(dir, theproject.Project) // Watch for changes - watcher, err := dev.NewWatcher(log, dir, theproject.Project.Development.Watch.Files, func(path string) { + watcher, err := dev.NewWatcher(log, dir, rules, func(path string) { log.Trace("%s has changed", path) - debounced(restart) + restart() }) if err != nil { errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start watcher: %s", err))).ShowErrorAndExit() @@ -213,50 +239,22 @@ Examples: log.Info("🚀 DevMode ready") - go func() { - for { - log.Trace("waiting for project server to exit (pid: %d)", pid) - if err := projectServerCmd.Wait(); err != nil { - if !isDeliberateRestart { - log.Error("project server (pid: %d) exited with error: %s", pid, err) - } - } - if projectServerCmd.ProcessState != nil { - log.Debug("project server (pid: %d) exited with code %d", pid, projectServerCmd.ProcessState.ExitCode()) - } else { - log.Debug("project server (pid: %d) exited", pid) - } - log.Debug("isDeliberateRestart: %t, pid: %d", isDeliberateRestart, pid) - if !isDeliberateRestart { - return - } - - // If it was a deliberate restart, start the new process here - if isDeliberateRestart { - isDeliberateRestart = false - log.Trace("restarting project server") - projectServerCmd, err = dev.CreateRunProjectCmd(processCtx, log, theproject, server, dir, orgId, port, os.Stdout, os.Stderr) - if err != nil { - errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to run project")).ShowErrorAndExit() - } - if err := projectServerCmd.Start(); err != nil { - errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start project: %s", err))).ShowErrorAndExit() - } - pid = projectServerCmd.Process.Pid - log.Trace("restarted project server (pid: %d)", pid) - } - } - }() - teardown := func() { watcher.Close(log) server.Close() - dev.KillProjectServer(log, projectServerCmd, pid) + if projectServerCmd != nil { + dev.KillProjectServer(log, projectServerCmd, pid) + projectServerCmd.Wait() + } } <-ctx.Done() + + fmt.Printf("\b\b\033[K") // remove the ^C + teardown() + log.Info("👋 See you next time!") }, } diff --git a/go.mod b/go.mod index 94921f2b..37b62aba 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/agentuity/go-common v1.0.64 github.com/agentuity/mcp-golang/v2 v2.0.2 - github.com/bep/debounce v1.2.1 github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbletea v1.3.4 diff --git a/go.sum b/go.sum index 48a3f2a7..0d684785 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,6 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= -github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 5f8ac869..8eea1514 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -117,6 +117,27 @@ func installSourceMapSupportIfNeeded(ctx BundleContext, dir string) error { return nil } +func runTypecheck(ctx BundleContext, dir string) error { + tsc := filepath.Join(dir, "node_modules", ".bin", "tsc") + if !util.Exists(tsc) { + ctx.Logger.Warn("no tsc found at %s, skipping typecheck", tsc) + return nil + } + cmd := exec.CommandContext(ctx.Context, tsc, "--noEmit") + cmd.Dir = dir + cmd.Stdout = ctx.Writer + cmd.Stderr = ctx.Writer + if err := cmd.Run(); err != nil { + if ctx.DevMode { + ctx.Logger.Error("🚫 TypeScript check failed") + return ErrBuildFailed // output goes to the console so we don't need to show it + } + os.Exit(2) + } + ctx.Logger.Debug("✅ TypeScript passed") + return nil +} + func bundleJavascript(ctx BundleContext, dir string, outdir string, theproject *project.Project) error { if ctx.Install || !util.Exists(filepath.Join(dir, "node_modules")) { @@ -167,6 +188,10 @@ func bundleJavascript(ctx BundleContext, dir string, outdir string, theproject * return err } + if err := runTypecheck(ctx, dir); err != nil { + return err + } + var entryPoints []string entryPoints = append(entryPoints, filepath.Join(dir, "index.js")) files, err := util.ListDir(filepath.Join(dir, theproject.Bundler.AgentConfig.Dir)) diff --git a/internal/dev/watcher.go b/internal/dev/watcher.go index 26b7c481..7c8a13d0 100644 --- a/internal/dev/watcher.go +++ b/internal/dev/watcher.go @@ -3,52 +3,65 @@ package dev import ( "os" "path/filepath" - "strings" + "time" + "github.com/agentuity/cli/internal/ignore" + "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/logger" "github.com/fsnotify/fsnotify" ) +const debugLogging = false + type FileWatcher struct { watcher *fsnotify.Watcher - patterns []string + ignore *ignore.Rules callback func(string) dir string } var ignorePatterns = []string{ - "__pycache__", - "__test__", - "node_modules", - ".pyc", + "**/.agentuity/**", + "**/package-lock.json", + "**/package.json", + "**/yarn.lock", + "**/pnpm-lock.yaml", + "**/bun.lock", + "**/bun.lockb", + "**/tsconfig.json", + "**/agentuity.yaml", + "**/.agentuity", } -func NewWatcher(logger logger.Logger, dir string, patterns []string, callback func(string)) (*FileWatcher, error) { +func NewWatcher(logger logger.Logger, dir string, rules *ignore.Rules, callback func(string)) (*FileWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } + for _, pattern := range ignorePatterns { + rules.Add(pattern) + } + fw := &FileWatcher{ watcher: watcher, - patterns: patterns, callback: callback, dir: dir, + ignore: rules, } err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - for _, ignorePattern := range ignorePatterns { - if strings.Contains(path, ignorePattern) { - return nil + if rules.Ignore(path, info) { + if debugLogging { + logger.Trace("Ignoring path: %s", path) } + return nil } - if fw.matchesPattern(logger, path) { - logger.Trace("Adding path to watcher: %s", path) - return watcher.Add(path) - } + logger.Trace("Adding path to watcher: %s", path) + fw.watcher.Add(path) return nil }) @@ -57,78 +70,48 @@ func NewWatcher(logger logger.Logger, dir string, patterns []string, callback fu } func (fw *FileWatcher) watch(logger logger.Logger) { + t := time.NewTicker(250 * time.Millisecond) // how long to debounce changes + defer t.Stop() + pending := make(map[string]bool) for { select { + case <-t.C: + for path := range pending { + fw.callback(path) + delete(pending, path) + } case event, ok := <-fw.watcher.Events: if !ok { return } + logger.Trace("Event: %s => %s", event.Op, event.Name) + if !util.Exists(event.Name) { + logger.Trace("File %s no longer exists", event.Name) + continue + } + fi, err := os.Stat(event.Name) + if err != nil { + logger.Error("Error statting %s: %s", event.Name, err) + continue + } // Watch new directories if event.Op&fsnotify.Create == fsnotify.Create { - if info, err := os.Stat(event.Name); err == nil && info.IsDir() { + if fi.IsDir() && !fw.ignore.Ignore(event.Name, fi) { + logger.Trace("Adding directory to watcher: %s", event.Name) fw.watcher.Add(event.Name) } } - if event.Op&fsnotify.Write == fsnotify.Write { - if fw.matchesPattern(logger, event.Name) { - fw.callback(event.Name) - } + if (event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create) && !fw.ignore.Ignore(event.Name, fi) { + logger.Trace("Write detected for %s", event.Name) + pending[event.Name] = true } case err, ok := <-fw.watcher.Errors: if !ok { return } - // Handle error if needed - _ = err - } - } -} - -func (fw *FileWatcher) matchesPattern(logger logger.Logger, filename string) bool { - for _, pattern := range fw.patterns { - // Make pattern relative to watched directory - if ok, _ := doubleStarMatch(logger, pattern, filename, fw.dir); ok { - return true + logger.Error("watcher error: %s", err) } } - return false -} - -func doubleStarMatch(logger logger.Logger, pattern, path, baseDir string) (bool, error) { - - // Convert absolute path to relative path from baseDir - relPath, err := filepath.Rel(baseDir, path) - if err != nil { - logger.Error("Failed to get relative path: %v", err) - return false, err - } - - // Clean and split paths - relPath = filepath.ToSlash(relPath) - pattern = filepath.ToSlash(pattern) - - patternParts := strings.Split(pattern, "/") - - // Base cases - if pattern == "**" { - return true, nil - } - - // If pattern ends with **, it matches any path that starts with the pattern prefix - if patternParts[len(patternParts)-1] == "**" { - prefix := strings.Join(patternParts[:len(patternParts)-1], "/") - return strings.HasPrefix(relPath, prefix), nil - } - - // If pattern starts with **, it matches any path that ends with the pattern suffix - if patternParts[0] == "**" { - suffix := strings.Join(patternParts[1:], "/") - return strings.HasSuffix(relPath, suffix), nil - } - - // Regular path matching - matched, err := filepath.Match(pattern, relPath) - return matched, err } func (fw *FileWatcher) Close(logger logger.Logger) error { diff --git a/internal/ignore/rules.go b/internal/ignore/rules.go index 7894134a..02427292 100644 --- a/internal/ignore/rules.go +++ b/internal/ignore/rules.go @@ -54,25 +54,36 @@ func Empty() *Rules { // AddDefaults adds default ignore patterns. func (r *Rules) AddDefaults() { - r.parseRule(".venv/**/*") - r.parseRule(".git/**/*") + r.parseRule("**/.venv/**/*") + r.parseRule("**/.git/**/*") + r.parseRule("**/.git") r.parseRule("**/__pycache__/**") r.parseRule("**/__tests__/**") r.parseRule("**/*.zip") r.parseRule("**/*.tar") r.parseRule("**/*.tar.gz") - r.parseRule(".gitignore") - r.parseRule("README.md") - r.parseRule("README") - r.parseRule("LICENSE.md") - r.parseRule("LICENSE") - r.parseRule("Makefile") - r.parseRule(".editorconfig") - r.parseRule(".agentuity/config.json") - r.parseRule(".cursor/**") - r.parseRule(".env*") - r.parseRule(".github/**") - r.parseRule(".vscode/**") + r.parseRule("**/.gitignore") + r.parseRule("**/README.md") + r.parseRule("**/README") + r.parseRule("**/LICENSE.md") + r.parseRule("**/LICENSE") + r.parseRule("**/Makefile") + r.parseRule("**/.editorconfig") + r.parseRule("**/.agentuity/config.json") + r.parseRule("**/.cursor/**") + r.parseRule("**/.env*") + r.parseRule("**/.github/**") + r.parseRule("**/.vscode/**") + r.parseRule("**/*.swp") + r.parseRule("**/.*.swp") + r.parseRule("**/*~") + r.parseRule("**/__pycache__/**") + r.parseRule("**/__test__/**") + r.parseRule("**/node_modules/**") + r.parseRule("**/*.pyc") + r.parseRule("**/.cursor/**") + r.parseRule("**/.vscode/**") + r.parseRule("**/.agentuity-*") } // Add a rule to the ignore set. diff --git a/internal/ignore/rules_test.go b/internal/ignore/rules_test.go new file mode 100644 index 00000000..e432d7fa --- /dev/null +++ b/internal/ignore/rules_test.go @@ -0,0 +1,39 @@ +package ignore + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRules(t *testing.T) { + rules := Empty() + rules.AddDefaults() + assert.True(t, rules.Ignore("/Users/foobar/example/src/agents/my-agent/.index.ts.swp", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/src/agents/my-agent/.index.ts~", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.venv/lib/foo.py", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.gitignore", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/README.md", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/README", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/LICENSE.md", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/LICENSE", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/Makefile", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.editorconfig", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.agentuity/config.json", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.cursor/file1", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.env.local", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.github/workflows/ci.yml", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.vscode/settings.json", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/src/__pycache__/foo.pyc", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/src/__tests__/test_foo.py", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/src/node_modules/lodash/index.js", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/src/agents/my-agent/.index.pyc", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/src/agents/my-agent/.index.tar.gz", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/src/agents/my-agent/.index.zip", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/src/agents/my-agent/.index.tar", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.git/objects/pack/pack-123.pack", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.git", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.foo.swp", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/src/__test__/test_bar.py", nil)) + assert.True(t, rules.Ignore("/Users/foobar/example/.agentuity-12345", nil)) +} From 64e47068cc676f9024f165c992c8ebc991002204 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Mon, 9 Jun 2025 19:32:24 -0500 Subject: [PATCH 2/4] remove unused --- cmd/dev.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/dev.go b/cmd/dev.go index d384f9e7..1e2d8152 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -51,7 +51,6 @@ Examples: apiKey, userId := util.EnsureLoggedIn(ctx, log, cmd) theproject := project.EnsureProject(ctx, cmd) dir := theproject.Dir - // var isDeliberateRestart bool checkForUpgrade(ctx, log, false) From c4b2a9ff89e146bbfb0f910f0fde1923a4d5dd93 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Mon, 9 Jun 2025 19:49:35 -0500 Subject: [PATCH 3/4] safer pid usage --- cmd/dev.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cmd/dev.go b/cmd/dev.go index 1e2d8152..593a0f63 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -8,6 +8,7 @@ import ( "os/signal" "path/filepath" "sync" + "sync/atomic" "syscall" "time" @@ -116,7 +117,7 @@ Examples: defer server.Close() processCtx := context.Background() - var pid int + var pid int32 waitForConnection := func() { if err := server.Connect(); err != nil { @@ -174,17 +175,17 @@ Examples: if err := projectServerCmd.Start(); err != nil { errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start project: %s", err))).ShowErrorAndExit() } - pid = projectServerCmd.Process.Pid + atomic.StoreInt32(&pid, int32(projectServerCmd.Process.Pid)) // running = true - log.Trace("restarted project server (pid: %d)", pid) - log.Trace("waiting for project server to exit (pid: %d)", pid) + log.Trace("restarted project server (pid: %d)", projectServerCmd.Process.Pid) + log.Trace("waiting for project server to exit (pid: %d)", projectServerCmd.Process.Pid) if err := projectServerCmd.Wait(); err != nil { - log.Error("project server (pid: %d) exited with error: %s", pid, err) + log.Error("project server (pid: %d) exited with error: %s", projectServerCmd.Process.Pid, err) } if projectServerCmd.ProcessState != nil { - log.Debug("project server (pid: %d) exited with code %d", pid, projectServerCmd.ProcessState.ExitCode()) + log.Debug("project server (pid: %d) exited with code %d", projectServerCmd.Process.Pid, projectServerCmd.ProcessState.ExitCode()) } else { - log.Debug("project server (pid: %d) exited", pid) + log.Debug("project server (pid: %d) exited", projectServerCmd.Process.Pid) } } @@ -199,7 +200,7 @@ Examples: // prevent multiple restarts from happening at once restartingLock.Lock() defer restartingLock.Unlock() - dev.KillProjectServer(log, projectServerCmd, pid) + dev.KillProjectServer(log, projectServerCmd, int(atomic.LoadInt32(&pid))) if build(false) { log.Trace("build ready") go runServer() @@ -224,12 +225,12 @@ Examples: errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start project: %s", err))).ShowErrorAndExit() } - pid = projectServerCmd.Process.Pid - log.Trace("started project server with pid: %d", pid) + atomic.StoreInt32(&pid, int32(projectServerCmd.Process.Pid)) + log.Trace("started project server with pid: %d", projectServerCmd.Process.Pid) if err := server.HealthCheck(devModeUrl); err != nil { log.Error("failed to health check connection: %s", err) - dev.KillProjectServer(log, projectServerCmd, pid) + dev.KillProjectServer(log, projectServerCmd, projectServerCmd.Process.Pid) return } } @@ -242,7 +243,7 @@ Examples: watcher.Close(log) server.Close() if projectServerCmd != nil { - dev.KillProjectServer(log, projectServerCmd, pid) + dev.KillProjectServer(log, projectServerCmd, int(atomic.LoadInt32(&pid))) projectServerCmd.Wait() } } From 88ee9262f6cddd972e1cd1197c8e686a9cbfca76 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Mon, 9 Jun 2025 20:00:08 -0500 Subject: [PATCH 4/4] slight safety check to hold on teardown in case restart attempts --- cmd/dev.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/dev.go b/cmd/dev.go index 593a0f63..5563ae8e 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -240,6 +240,8 @@ Examples: log.Info("🚀 DevMode ready") teardown := func() { + restartingLock.Lock() + defer restartingLock.Unlock() watcher.Close(log) server.Close() if projectServerCmd != nil {