Skip to content
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
134 changes: 51 additions & 83 deletions cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"syscall"
Expand Down Expand Up @@ -98,175 +97,144 @@ Examples:
processCtx := context.Background()
var pid int

consoleUrl := server.WebURL(appUrl)
devModeUrl := fmt.Sprintf("http://127.0.0.1:%d", port)

agents := make([]*dev.Agent, 0)
for _, agent := range theproject.Project.Agents {
agents = append(agents, &dev.Agent{
ID: agent.ID,
Name: agent.Name,
LocalURL: fmt.Sprintf("%s/%s", devModeUrl, agent.ID),
})
}

ui := dev.NewDevModeUI(ctx, dev.DevModeConfig{
DevModeUrl: devModeUrl,
AppUrl: consoleUrl,
Agents: agents,
})

ui.Start()

defer ui.Close(false)

tuiLogger := dev.NewTUILogger(logLevel, ui, dev.Stdout)
tuiLoggerErr := dev.NewTUILogger(logLevel, ui, dev.StdErr)

if err := server.Connect(ui, tuiLogger, tuiLoggerErr); err != nil {
if errors.Is(err, context.Canceled) {
ui.Close(true)
waitForConnection := func() {
if err := server.Connect(); err != nil {
if errors.Is(err, context.Canceled) {
return
}
log.Error("failed to start live dev connection: %s", err)
return
}
log.Error("failed to start live dev connection: %s", err)
ui.Close(true)
return
}

publicUrl := server.PublicURL(appUrl)
ui.SetPublicURL(publicUrl)
tui.ShowSpinner("Connecting ...", waitForConnection)

for _, agent := range agents {
agent.PublicURL = fmt.Sprintf("%s/%s", publicUrl, agent.ID)
}
publicUrl := server.PublicURL(appUrl)
consoleUrl := server.WebURL(appUrl)
devModeUrl := fmt.Sprintf("http://127.0.0.1:%d", port)
infoBox := server.GenerateInfoBox(publicUrl, consoleUrl, devModeUrl)
fmt.Println(infoBox)

projectServerCmd, err := dev.CreateRunProjectCmd(processCtx, tuiLogger, theproject, server, dir, orgId, port, tuiLogger, tuiLoggerErr)
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()
}

build := func(initial bool) bool {
started := time.Now()
var ok bool
ui.ShowSpinner("Building project ...", func() {
var w io.Writer = tuiLogger
tui.ShowSpinner("Building project ...", func() {
if err := bundler.Bundle(bundler.BundleContext{
Context: ctx,
Logger: tuiLogger,
Logger: log,
ProjectDir: dir,
Production: false,
DevMode: true,
Writer: w,
Writer: os.Stdout,
}); err != nil {
if err == bundler.ErrBuildFailed {
return
}
ui.Close(true)
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to bundle project: %s", err))).ShowErrorAndExit()
}
ok = true
})
if ok && !initial {
ui.SetStatusMessage("✨ Built in %s", time.Since(started).Round(time.Millisecond))
log.Info("✨ Built in %s", time.Since(started).Round(time.Millisecond))
}
return ok
}

// Initial build must exit if it fails
if !build(true) {
ui.Close(true)
return
}

restart := func() {
isDeliberateRestart = true
build(false)
tuiLogger.Debug("killing project server")
dev.KillProjectServer(tuiLogger, projectServerCmd, pid)
tuiLogger.Debug("killing project server done")
log.Debug("killing project server")
dev.KillProjectServer(log, projectServerCmd, pid)
log.Debug("killing project server done")
}

ui.SetStatusMessage("starting ...")
ui.SetSpinner(true)

// debounce a lot of changes at once to avoid multiple restarts in succession
debounced := debounce.New(250 * time.Millisecond)

// Watch for changes
watcher, err := dev.NewWatcher(tuiLogger, dir, theproject.Project.Development.Watch.Files, func(path string) {
tuiLogger.Trace("%s has changed", path)
watcher, err := dev.NewWatcher(log, dir, theproject.Project.Development.Watch.Files, func(path string) {
log.Trace("%s has changed", path)
debounced(restart)
})
if err != nil {
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start watcher: %s", err))).ShowErrorAndExit()
}
defer watcher.Close(tuiLogger)
defer watcher.Close(log)

tuiLogger.Trace("starting project server")
if err := projectServerCmd.Start(); err != nil {
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start project: %s", err))).ShowErrorAndExit()
}
initRun := func() {
Comment on lines +172 to +174
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

watcher.Close is invoked twice – second call may error.

You defer watcher.Close(log) right after the watcher is created and call the same method again inside teardown().
Unless (*Watcher).Close is explicitly idempotent this double-close can return an error or panic (e.g. “send on closed channel”). Remove one of the calls – the defer is usually sufficient.

-		defer watcher.Close(log)
...
-		teardown := func() {
-			watcher.Close(log)
+		teardown := func() {
 			server.Close()
 			dev.KillProjectServer(log, projectServerCmd, pid)
 		}

Also applies to: 230-233

🤖 Prompt for AI Agents
In cmd/dev.go around lines 172 to 174 and also lines 230 to 233, the
watcher.Close(log) method is called twice: once deferred immediately after
watcher creation and again inside the teardown() function. This double close can
cause errors or panics if Close is not idempotent. To fix this, remove the
explicit call to watcher.Close(log) inside teardown() and rely solely on the
deferred call to ensure the watcher is closed exactly once.

log.Trace("starting project server")
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
tuiLogger.Trace("started project server with pid: %d", pid)
pid = projectServerCmd.Process.Pid
log.Trace("started project server with pid: %d", pid)

if err := server.HealthCheck(devModeUrl); err != nil {
tuiLogger.Error("failed to health check connection: %s", err)
dev.KillProjectServer(tuiLogger, projectServerCmd, pid)
ui.Close(true)
return
if err := server.HealthCheck(devModeUrl); err != nil {
log.Error("failed to health check connection: %s", err)
dev.KillProjectServer(log, projectServerCmd, pid)
return
}
}

ui.SetStatusMessage("🚀 DevMode ready")
ui.SetSpinner(false)
tui.ShowSpinner("Starting Agents ...", initRun)

log.Info("🚀 DevMode ready")

go func() {
for {
tuiLogger.Trace("waiting for project server to exit (pid: %d)", pid)
log.Trace("waiting for project server to exit (pid: %d)", pid)
if err := projectServerCmd.Wait(); err != nil {
if !isDeliberateRestart {
tuiLogger.Error("project server (pid: %d) exited with error: %s", pid, err)
log.Error("project server (pid: %d) exited with error: %s", pid, err)
}
}
if projectServerCmd.ProcessState != nil {
tuiLogger.Debug("project server (pid: %d) exited with code %d", pid, projectServerCmd.ProcessState.ExitCode())
log.Debug("project server (pid: %d) exited with code %d", pid, projectServerCmd.ProcessState.ExitCode())
} else {
tuiLogger.Debug("project server (pid: %d) exited", pid)
log.Debug("project server (pid: %d) exited", pid)
}
tuiLogger.Debug("isDeliberateRestart: %t, pid: %d", isDeliberateRestart, 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
tuiLogger.Trace("restarting project server")
projectServerCmd, err = dev.CreateRunProjectCmd(processCtx, tuiLogger, theproject, server, dir, orgId, port, tuiLogger, tuiLoggerErr)
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
tuiLogger.Trace("restarted project server (pid: %d)", pid)
log.Trace("restarted project server (pid: %d)", pid)
}
}
}()

teardown := func() {
watcher.Close(tuiLogger)
watcher.Close(log)
server.Close()
dev.KillProjectServer(tuiLogger, projectServerCmd, pid)
dev.KillProjectServer(log, projectServerCmd, pid)
}

select {
case <-ui.Done():
teardown()
case <-ctx.Done():
teardown()
}
<-ctx.Done()
teardown()

},
}

Expand Down
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ require (
github.com/stretchr/testify v1.10.0
github.com/zijiren233/yaml-comment v0.2.2
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/term v0.30.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.32.1
)
Expand All @@ -48,7 +47,6 @@ require (
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
Expand All @@ -59,6 +57,7 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/term v0.30.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
Expand Down Expand Up @@ -210,8 +208,6 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
Expand Down
Loading
Loading