Compare commits

..

1 Commits

Author SHA1 Message Date
Ammar Bandukwala b7b40f40b9 chore(mux): add go format tool hook 2026-02-03 15:57:54 -06:00
28 changed files with 321 additions and 260 deletions
Executable
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Mux tool post-hook to format Go files after edits.
# It runs after file_edit_* tools and formats only .go files.
set -euo pipefail
if [[ "${MUX_TOOL:-}" != file_edit_* ]]; then
exit 0
fi
tool_input="${MUX_TOOL_INPUT:-}"
if [[ -n "${MUX_TOOL_INPUT_PATH:-}" && -f "${MUX_TOOL_INPUT_PATH}" ]]; then
tool_input="$(cat "${MUX_TOOL_INPUT_PATH}")"
fi
if [[ -z "$tool_input" ]]; then
exit 0
fi
file_path="$(echo "$tool_input" | jq -r '.file_path // empty')"
if [[ -z "$file_path" ]]; then
exit 0
fi
case "$file_path" in
*.go)
project_dir="${MUX_PROJECT_DIR:-$(pwd)}"
cd "$project_dir"
if command -v brew >/dev/null 2>&1; then
brew_prefix="$(brew --prefix gnu-getopt 2>/dev/null || true)"
if [[ -n "$brew_prefix" && -d "$brew_prefix/bin" ]]; then
export PATH="$brew_prefix/bin:$PATH"
fi
fi
if ! make fmt/go FILE="$file_path" >/dev/null 2>&1; then
if command -v gofmt >/dev/null 2>&1; then
gofmt -w "$file_path"
fi
fi
;;
esac
+24 -25
View File
@@ -39,7 +39,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/clistat"
"github.com/coder/coder/v2/agent/agentcontainers"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/agent/agentfiles"
"github.com/coder/coder/v2/agent/agentscripts"
@@ -554,7 +553,7 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
// Set up collect and report as a single ticker with two channels,
// this is to allow collection and reporting to be triggered
// independently of each other.
agentutil.Go(ctx, a.logger, func() {
go func() {
t := time.NewTicker(a.reportMetadataInterval)
defer func() {
t.Stop()
@@ -579,9 +578,9 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
wake(collect)
}
}
})
}()
agentutil.Go(ctx, a.logger, func() {
go func() {
defer close(collectDone)
var (
@@ -628,7 +627,7 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
// We send the result to the channel in the goroutine to avoid
// sending the same result multiple times. So, we don't care about
// the return values.
agentutil.Go(ctx, a.logger, func() { flight.Do(md.Key, func() {
go flight.Do(md.Key, func() {
ctx := slog.With(ctx, slog.F("key", md.Key))
lastCollectedAtMu.RLock()
collectedAt, ok := lastCollectedAts[md.Key]
@@ -681,10 +680,10 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
lastCollectedAts[md.Key] = now
lastCollectedAtMu.Unlock()
}
}) })
})
}
}
})
}()
// Gather metadata updates and report them once every interval. If a
// previous report is in flight, wait for it to complete before
@@ -735,14 +734,14 @@ func (a *agent) reportMetadata(ctx context.Context, aAPI proto.DRPCAgentClient28
}
reportInFlight = true
agentutil.Go(ctx, a.logger, func() {
go func() {
a.logger.Debug(ctx, "batch updating metadata")
ctx, cancel := context.WithTimeout(ctx, reportTimeout)
defer cancel()
_, err := aAPI.BatchUpdateMetadata(ctx, &proto.BatchUpdateMetadataRequest{Metadata: metadata})
reportError <- err
})
}()
}
}
}
@@ -1519,10 +1518,10 @@ func (a *agent) trackGoroutine(fn func()) error {
return xerrors.Errorf("track conn goroutine: %w", ErrAgentClosing)
}
a.closeWaitGroup.Add(1)
agentutil.Go(a.hardCtx, a.logger, func() {
go func() {
defer a.closeWaitGroup.Done()
fn()
})
}()
return nil
}
@@ -1626,15 +1625,15 @@ func (a *agent) createTailnet(
clog.Info(ctx, "accepted conn")
wg.Add(1)
closed := make(chan struct{})
agentutil.Go(ctx, clog, func() {
go func() {
select {
case <-closed:
case <-a.hardCtx.Done():
_ = conn.Close()
}
wg.Done()
})
agentutil.Go(ctx, clog, func() {
}()
go func() {
defer close(closed)
sErr := speedtest.ServeConn(conn)
if sErr != nil {
@@ -1642,7 +1641,7 @@ func (a *agent) createTailnet(
return
}
clog.Info(ctx, "test ended")
})
}()
}
wg.Wait()
}); err != nil {
@@ -1669,13 +1668,13 @@ func (a *agent) createTailnet(
WriteTimeout: 20 * time.Second,
ErrorLog: slog.Stdlib(ctx, a.logger.Named("http_api_server"), slog.LevelInfo),
}
agentutil.Go(ctx, a.logger, func() {
go func() {
select {
case <-ctx.Done():
case <-a.hardCtx.Done():
}
_ = server.Close()
})
}()
apiServErr := server.Serve(apiListener)
if apiServErr != nil && !xerrors.Is(apiServErr, http.ErrServerClosed) && !strings.Contains(apiServErr.Error(), "use of closed network connection") {
@@ -1717,7 +1716,7 @@ func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTai
coordination := ctrl.New(coordinate)
errCh := make(chan error, 1)
agentutil.Go(ctx, a.logger, func() {
go func() {
defer close(errCh)
select {
case <-ctx.Done():
@@ -1729,7 +1728,7 @@ func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTai
case err := <-coordination.Wait():
errCh <- err
}
})
}()
return <-errCh
}
@@ -1820,7 +1819,7 @@ func (a *agent) Collect(ctx context.Context, networkStats map[netlogtype.Connect
continue
}
wg.Add(1)
agentutil.Go(pingCtx, a.logger, func() {
go func() {
defer wg.Done()
duration, p2p, _, err := a.network.Ping(pingCtx, addresses[0].Addr())
if err != nil {
@@ -1834,7 +1833,7 @@ func (a *agent) Collect(ctx context.Context, networkStats map[netlogtype.Connect
} else {
derpConns++
}
})
}()
}
wg.Wait()
sort.Float64s(durations)
@@ -2032,13 +2031,13 @@ func (a *agent) Close() error {
// Wait for the graceful shutdown to complete, but don't wait forever so
// that we don't break user expectations.
agentutil.Go(a.hardCtx, a.logger, func() {
go func() {
defer a.hardCancel()
select {
case <-a.hardCtx.Done():
case <-time.After(5 * time.Second):
}
})
}()
// Wait for lifecycle to be reported
lifecycleWaitLoop:
@@ -2128,13 +2127,13 @@ const EnvAgentSubsystem = "CODER_AGENT_SUBSYSTEM"
// eitherContext returns a context that is canceled when either context ends.
func eitherContext(a, b context.Context) context.Context {
ctx, cancel := context.WithCancel(a)
agentutil.Go(ctx, slog.Logger{}, func() {
go func() {
defer cancel()
select {
case <-a.Done():
case <-b.Done():
}
})
}()
return ctx
}
+4 -5
View File
@@ -28,7 +28,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentcontainers/ignore"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/agent/usershell"
@@ -564,11 +563,11 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error {
if dc.Status == codersdk.WorkspaceAgentDevcontainerStatusStarting {
api.asyncWg.Add(1)
agentutil.Go(api.ctx, api.logger, func() {
go func() {
defer api.asyncWg.Done()
_ = api.CreateDevcontainer(dc.WorkspaceFolder, dc.ConfigPath)
})
}()
}
}
api.mu.Unlock()
@@ -1424,9 +1423,9 @@ func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Reques
api.knownDevcontainers[dc.WorkspaceFolder] = dc
api.broadcastUpdatesLocked()
agentutil.Go(ctx, api.logger, func() {
go func() {
_ = api.CreateDevcontainer(dc.WorkspaceFolder, dc.ConfigPath, WithRemoveExistingContainer())
})
}()
api.mu.Unlock()
+2 -3
View File
@@ -22,7 +22,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
@@ -474,10 +473,10 @@ func (r *Runner) trackCommandGoroutine(fn func()) error {
return xerrors.New("track command goroutine: closed")
}
r.cmdCloseWait.Add(1)
agentutil.Go(r.cronCtx, r.Logger, func() {
go func() {
defer r.cmdCloseWait.Done()
fn()
})
}()
return nil
}
+2 -3
View File
@@ -12,7 +12,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentsocket/proto"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/agent/unit"
"github.com/coder/coder/v2/codersdk/drpcsdk"
)
@@ -80,10 +79,10 @@ func NewServer(logger slog.Logger, opts ...Option) (*Server, error) {
server.logger.Info(server.ctx, "agent socket server started", slog.F("path", server.path))
server.wg.Add(1)
agentutil.Go(server.ctx, server.logger, func() {
go func() {
defer server.wg.Done()
server.acceptConnections()
})
}()
return server, nil
}
+10 -11
View File
@@ -29,7 +29,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentcontainers"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/agent/agentrsa"
"github.com/coder/coder/v2/agent/usershell"
@@ -635,13 +634,13 @@ func (s *Server) startNonPTYSession(logger slog.Logger, session ssh.Session, mag
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "stdin_pipe").Add(1)
return xerrors.Errorf("create stdin pipe: %w", err)
}
agentutil.Go(session.Context(), logger, func() {
go func() {
_, err := io.Copy(stdinPipe, session)
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "stdin_io_copy").Add(1)
}
_ = stdinPipe.Close()
})
}()
err = cmd.Start()
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "start_command").Add(1)
@@ -663,11 +662,11 @@ func (s *Server) startNonPTYSession(logger slog.Logger, session ssh.Session, mag
session.Signals(nil)
close(sigs)
}()
agentutil.Go(session.Context(), logger, func() {
go func() {
for sig := range sigs {
handleSignal(logger, sig, cmd.Process, s.metrics, magicTypeLabel)
}
})
}()
return cmd.Wait()
}
@@ -738,7 +737,7 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
session.Signals(nil)
close(sigs)
}()
agentutil.Go(ctx, logger, func() {
go func() {
for {
if sigs == nil && windowSize == nil {
return
@@ -765,14 +764,14 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
}
}
}
})
}()
agentutil.Go(ctx, logger, func() {
go func() {
_, err := io.Copy(ptty.InputWriter(), session)
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "input_io_copy").Add(1)
}
})
}()
// We need to wait for the command output to finish copying. It's safe to
// just do this copy on the main handler goroutine because one of two things
@@ -1214,11 +1213,11 @@ func (s *Server) Close() error {
// but Close() may not have completed.
func (s *Server) Shutdown(ctx context.Context) error {
ch := make(chan error, 1)
agentutil.Go(ctx, s.logger, func() {
go func() {
// TODO(mafredri): Implement shutdown, SIGHUP running commands, etc.
// For now we just close the server.
ch <- s.Close()
})
}()
var err error
select {
case <-ctx.Done():
+2 -5
View File
@@ -4,9 +4,6 @@ import (
"context"
"io"
"sync"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentutil"
)
// Bicopy copies all of the data between the two connections and will close them
@@ -38,10 +35,10 @@ func Bicopy(ctx context.Context, c1, c2 io.ReadWriteCloser) {
// Convert waitgroup to a channel so we can also wait on the context.
done := make(chan struct{})
agentutil.Go(ctx, slog.Logger{}, func() {
go func() {
defer close(done)
wg.Wait()
})
}()
select {
case <-ctx.Done():
+6 -7
View File
@@ -16,7 +16,6 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentutil"
)
// streamLocalForwardPayload describes the extra data sent in a
@@ -131,11 +130,11 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
log.Debug(ctx, "SSH unix forward added to cache")
ctx, cancel := context.WithCancel(ctx)
agentutil.Go(ctx, log, func() {
go func() {
<-ctx.Done()
_ = ln.Close()
})
agentutil.Go(ctx, log, func() {
}()
go func() {
defer cancel()
for {
@@ -153,7 +152,7 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
SocketPath: addr,
})
agentutil.Go(ctx, log, func() {
go func() {
ch, reqs, err := conn.OpenChannel("forwarded-streamlocal@openssh.com", payload)
if err != nil {
h.log.Warn(ctx, "open SSH unix forward channel to client", slog.Error(err))
@@ -162,7 +161,7 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
}
go gossh.DiscardRequests(reqs)
Bicopy(ctx, ch, c)
})
}()
}
h.Lock()
@@ -172,7 +171,7 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
h.Unlock()
log.Debug(ctx, "SSH unix forward listener removed from cache")
_ = ln.Close()
})
}()
return true, nil
+4 -5
View File
@@ -22,7 +22,6 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentutil"
)
const (
@@ -123,10 +122,10 @@ func (x *x11Forwarder) x11Handler(sshCtx ssh.Context, sshSession ssh.Session) (d
}
// clean up the X11 session if the SSH session completes.
agentutil.Go(ctx, x.logger, func() {
go func() {
<-ctx.Done()
x.closeAndRemoveSession(x11session)
})
}()
go x.listenForConnections(ctx, x11session, serverConn, x11)
x.logger.Debug(ctx, "X11 forwarding started", slog.F("display", x11session.display))
@@ -207,10 +206,10 @@ func (x *x11Forwarder) listenForConnections(
_ = conn.Close()
continue
}
agentutil.Go(ctx, x.logger, func() {
go func() {
defer x.trackConn(conn, false)
Bicopy(ctx, conn, channel)
})
}()
}
}
-25
View File
@@ -1,25 +0,0 @@
package agentutil
import (
"context"
"runtime/debug"
"cdr.dev/slog/v3"
)
// Go runs the provided function in a goroutine, recovering from panics and
// logging them before re-panicking.
func Go(ctx context.Context, log slog.Logger, fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Critical(ctx, "panic in goroutine",
slog.F("panic", r),
slog.F("stack", string(debug.Stack())),
)
panic(r)
}
}()
fn()
}()
}
+2 -3
View File
@@ -10,7 +10,6 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/quartz"
@@ -70,7 +69,7 @@ func NewAppHealthReporterWithClock(
continue
}
app := nextApp
agentutil.Go(ctx, logger, func() {
go func() {
_ = clk.TickerFunc(ctx, time.Duration(app.Healthcheck.Interval)*time.Second, func() error {
// We time out at the healthcheck interval to prevent getting too backed up, but
// set it 1ms early so that it's not simultaneous with the next tick in testing,
@@ -134,7 +133,7 @@ func NewAppHealthReporterWithClock(
}
return nil
}, "healthcheck", app.Slug)
})
}()
}
mu.Lock()
+2 -3
View File
@@ -15,7 +15,6 @@ import (
"google.golang.org/protobuf/proto"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/agent/boundarylogproxy/codec"
agentproto "github.com/coder/coder/v2/agent/proto"
)
@@ -134,11 +133,11 @@ func (s *Server) handleConnection(ctx context.Context, conn net.Conn) {
defer cancel()
s.wg.Add(1)
agentutil.Go(ctx, s.logger, func() {
go func() {
defer s.wg.Done()
<-ctx.Done()
_ = conn.Close()
})
}()
// This is intended to be a sane starting point for the read buffer size. It may be
// grown by codec.ReadFrame if necessary.
+4 -5
View File
@@ -14,7 +14,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/pty"
)
@@ -77,7 +76,7 @@ func newBuffered(ctx context.Context, logger slog.Logger, execer agentexec.Exece
// We do not need to separately monitor for the process exiting. When it
// exits, our ptty.OutputReader() will return EOF after reading all process
// output.
agentutil.Go(ctx, logger, func() {
go func() {
buffer := make([]byte, 1024)
for {
read, err := ptty.OutputReader().Read(buffer)
@@ -119,7 +118,7 @@ func newBuffered(ctx context.Context, logger slog.Logger, execer agentexec.Exece
}
rpty.state.cond.L.Unlock()
}
})
}()
return rpty
}
@@ -134,7 +133,7 @@ func (rpty *bufferedReconnectingPTY) lifecycle(ctx context.Context, logger slog.
logger.Debug(ctx, "reconnecting pty ready")
rpty.state.setState(StateReady, nil)
state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing, logger)
state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing)
if state < StateClosing {
// If we have not closed yet then the context is what unblocked us (which
// means the agent is shutting down) so move into the closing phase.
@@ -191,7 +190,7 @@ func (rpty *bufferedReconnectingPTY) Attach(ctx context.Context, connID string,
delete(rpty.activeConns, connID)
}()
state, err := rpty.state.waitForStateOrContext(ctx, StateReady, logger)
state, err := rpty.state.waitForStateOrContext(ctx, StateReady)
if state != StateReady {
return err
}
+3 -4
View File
@@ -15,7 +15,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/pty"
)
@@ -178,20 +177,20 @@ func (s *ptyState) waitForState(state State) (State, error) {
// waitForStateOrContext blocks until the state or a greater one is reached or
// the provided context ends.
func (s *ptyState) waitForStateOrContext(ctx context.Context, state State, logger slog.Logger) (State, error) {
func (s *ptyState) waitForStateOrContext(ctx context.Context, state State) (State, error) {
s.cond.L.Lock()
defer s.cond.L.Unlock()
nevermind := make(chan struct{})
defer close(nevermind)
agentutil.Go(ctx, logger, func() {
go func() {
select {
case <-ctx.Done():
// Wake up when the context ends.
s.cond.Broadcast()
case <-nevermind:
}
})
}()
for ctx.Err() == nil && state > s.state {
s.cond.Wait()
+4 -5
View File
@@ -20,7 +20,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentexec"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/pty"
)
@@ -142,7 +141,7 @@ func (rpty *screenReconnectingPTY) lifecycle(ctx context.Context, logger slog.Lo
logger.Debug(ctx, "reconnecting pty ready")
rpty.state.setState(StateReady, nil)
state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing, logger)
state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing)
if state < StateClosing {
// If we have not closed yet then the context is what unblocked us (which
// means the agent is shutting down) so move into the closing phase.
@@ -167,7 +166,7 @@ func (rpty *screenReconnectingPTY) Attach(ctx context.Context, _ string, conn ne
ctx, cancel := context.WithCancel(ctx)
defer cancel()
state, err := rpty.state.waitForStateOrContext(ctx, StateReady, logger)
state, err := rpty.state.waitForStateOrContext(ctx, StateReady)
if state != StateReady {
return err
}
@@ -257,7 +256,7 @@ func (rpty *screenReconnectingPTY) doAttach(ctx context.Context, conn net.Conn,
// We do not need to separately monitor for the process exiting. When it
// exits, our ptty.OutputReader() will return EOF after reading all process
// output.
agentutil.Go(ctx, logger, func() {
go func() {
defer versionCancel()
defer func() {
err := conn.Close()
@@ -299,7 +298,7 @@ func (rpty *screenReconnectingPTY) doAttach(ctx context.Context, conn net.Conn,
break
}
}
})
}()
// Version seems to be the only command without a side effect (other than
// making the version pop up briefly) so use it to wait for the session to
+8 -9
View File
@@ -15,7 +15,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentcontainers"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/usershell"
"github.com/coder/coder/v2/codersdk/workspacesdk"
@@ -91,7 +90,7 @@ func (s *Server) Serve(ctx, hardCtx context.Context, l net.Listener) (retErr err
wg.Add(1)
disconnected := s.reportConnection(uuid.New(), remoteAddrString)
closed := make(chan struct{})
agentutil.Go(ctx, clog, func() {
go func() {
defer wg.Done()
select {
case <-closed:
@@ -99,9 +98,9 @@ func (s *Server) Serve(ctx, hardCtx context.Context, l net.Listener) (retErr err
disconnected(1, "server shut down")
_ = conn.Close()
}
})
}()
wg.Add(1)
agentutil.Go(ctx, clog, func() {
go func() {
defer close(closed)
defer wg.Done()
err := s.handleConn(ctx, clog, conn)
@@ -114,7 +113,7 @@ func (s *Server) Serve(ctx, hardCtx context.Context, l net.Listener) (retErr err
} else {
disconnected(0, "")
}
})
}()
}
wg.Wait()
return retErr
@@ -227,18 +226,18 @@ func (s *Server) handleConn(ctx context.Context, logger slog.Logger, conn net.Co
)
done := make(chan struct{})
agentutil.Go(ctx, connLogger, func() {
go func() {
select {
case <-done:
case <-ctx.Done():
rpty.Close(ctx.Err())
}
})
}()
agentutil.Go(ctx, connLogger, func() {
go func() {
rpty.Wait()
s.reconnectingPTYs.Delete(msg.ID)
})
}()
connected = true
sendConnected <- rpty
+2 -3
View File
@@ -10,7 +10,6 @@ import (
"tailscale.com/types/netlogtype"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentutil"
"github.com/coder/coder/v2/agent/proto"
)
@@ -87,13 +86,13 @@ func (s *statsReporter) reportLoop(ctx context.Context, dest statsDest) error {
// use a separate goroutine to monitor the context so that we notice immediately, rather than
// waiting for the next callback (which might never come if we are closing!)
ctxDone := false
agentutil.Go(ctx, s.logger, func() {
go func() {
<-ctx.Done()
s.L.Lock()
defer s.L.Unlock()
ctxDone = true
s.Broadcast()
})
}()
defer s.logger.Debug(ctx, "reportLoop exiting")
s.L.Lock()
-1
View File
@@ -470,7 +470,6 @@ require (
)
require (
cdr.dev/slog v1.6.2-0.20251120224544-40ff19937ff2
github.com/anthropics/anthropic-sdk-go v1.19.0
github.com/brianvoe/gofakeit/v7 v7.14.0
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
-2
View File
@@ -1,5 +1,3 @@
cdr.dev/slog v1.6.2-0.20251120224544-40ff19937ff2 h1:M4Z9eTbnHPdZI4GpBUNCae0lSgUucY+aW5j7+zB8lCk=
cdr.dev/slog v1.6.2-0.20251120224544-40ff19937ff2/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ=
cdr.dev/slog/v3 v3.0.0-rc1 h1:EN7Zim6GvTpAeHQjI0ERDEfqKbTyXRvgH4UhlzLpvWM=
cdr.dev/slog/v3 v3.0.0-rc1/go.mod h1:iO/OALX1VxlI03mkodCGdVP7pXzd2bRMvu3ePvlJ9ak=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
-11
View File
@@ -104,17 +104,6 @@ func testingWithOwnerUser(m dsl.Matcher) {
Report(`This client is operating as the owner user, which has unrestricted permissions. Consider creating a different user.`)
}
// doNotUseRawGoInAgent detects raw `go func()` in agent package.
// Use agentutil.Go() instead for panic recovery.
//
//nolint:unused,deadcode,varnamelen
func doNotUseRawGoInAgent(m dsl.Matcher) {
m.Match(`go func() { $*_ }()`, `go func($*_) { $*_ }($*_)`).
Where(m.File().PkgPath.Matches(`github\.com/coder/coder/v2/agent(/.*)?`) &&
!m.File().Name.Matches(`_test\.go$`)).
Report("Use agentutil.Go() instead of raw go func() for panic recovery")
}
// Use xerrors everywhere! It provides additional stacktrace info!
//
//nolint:unused,deadcode,varnamelen
+2
View File
@@ -18,6 +18,8 @@ export const Popover = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverClose = PopoverPrimitive.PopoverClose;
export const PopoverContent = forwardRef<
ElementRef<typeof PopoverPrimitive.Content>,
ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
@@ -23,7 +23,7 @@ export const DashboardLayout: FC = () => {
{canViewDeployment && <LicenseBanner />}
<AnnouncementBanners />
<div className="flex flex-col min-h-screen justify-between">
<div className="flex flex-col h-screen justify-between">
<Navbar />
<div className="relative flex flex-col flex-1 min-h-0 overflow-y-auto">
@@ -61,7 +61,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
const webPush = useWebpushNotifications();
return (
<div className="sticky top-0 bg-surface-primary z-40 border-0 border-b border-solid h-[72px] min-h-[72px] flex items-center leading-none px-6">
<div className="border-0 border-b border-solid h-[72px] min-h-[72px] flex items-center leading-none px-6">
<NavLink to="/workspaces">
{logo_url ? (
<ExternalImage className="h-7" src={logo_url} alt="Custom Logo" />
@@ -1,10 +1,10 @@
import type * as TypesGen from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import type { FC } from "react";
import { UserDropdownContent } from "./UserDropdownContent";
@@ -22,24 +22,28 @@ export const UserDropdown: FC<UserDropdownProps> = ({
onSignOut,
}) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="bg-transparent border-0 cursor-pointer p-0"
>
<Avatar fallback={user.username} src={user.avatar_url} size="lg" />
</button>
</DropdownMenuTrigger>
</PopoverTrigger>
<DropdownMenuContent align="end" className="min-w-auto w-[260px]">
<PopoverContent
align="end"
className="min-w-auto w-[260px] bg-surface-secondary border-surface-quaternary"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<UserDropdownContent
user={user}
buildInfo={buildInfo}
supportLinks={supportLinks}
onSignOut={onSignOut}
/>
</DropdownMenuContent>
</DropdownMenu>
</PopoverContent>
</Popover>
);
};
@@ -1,31 +1,20 @@
import { MockUserOwner } from "testHelpers/entities";
import { render, waitForLoaderToBeRemoved } from "testHelpers/renderHelpers";
import { screen } from "@testing-library/react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
import { Popover } from "components/Popover/Popover";
import { Language, UserDropdownContent } from "./UserDropdownContent";
const renderUserDropdownContent = (props: { onSignOut: () => void }) => {
return render(
<DropdownMenu defaultOpen>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent>
<UserDropdownContent
user={MockUserOwner}
onSignOut={props.onSignOut}
supportLinks={[]}
/>
</DropdownMenuContent>
</DropdownMenu>,
);
};
describe("UserDropdownContent", () => {
it("has the correct link for the account item", async () => {
renderUserDropdownContent({ onSignOut: vi.fn() });
render(
<Popover>
<UserDropdownContent
user={MockUserOwner}
onSignOut={vi.fn()}
supportLinks={[]}
/>
</Popover>,
);
await waitForLoaderToBeRemoved();
const link = screen.getByText(Language.accountLabel).closest("a");
@@ -38,7 +27,15 @@ describe("UserDropdownContent", () => {
it("calls the onSignOut function", async () => {
const onSignOut = vi.fn();
renderUserDropdownContent({ onSignOut });
render(
<Popover>
<UserDropdownContent
user={MockUserOwner}
onSignOut={onSignOut}
supportLinks={[]}
/>
</Popover>,
);
await waitForLoaderToBeRemoved();
screen.getByText(Language.signOutLabel).click();
expect(onSignOut).toBeCalledTimes(1);
@@ -1,18 +1,22 @@
import type * as TypesGen from "api/typesGenerated";
import {
DropdownMenuItem,
DropdownMenuSeparator,
} from "components/DropdownMenu/DropdownMenu";
type CSSObject,
css,
type Interpolation,
type Theme,
} from "@emotion/react";
import Divider from "@mui/material/Divider";
import MenuItem from "@mui/material/MenuItem";
import type * as TypesGen from "api/typesGenerated";
import { CopyButton } from "components/CopyButton/CopyButton";
import { PopoverClose } from "components/Popover/Popover";
import { Stack } from "components/Stack/Stack";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useClipboard } from "hooks/useClipboard";
import {
CheckIcon,
CircleUserIcon,
CopyIcon,
LogOutIcon,
MonitorDownIcon,
SquareArrowOutUpRightIcon,
@@ -40,94 +44,153 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
supportLinks,
onSignOut,
}) => {
const { showCopiedSuccess, copyToClipboard } = useClipboard();
return (
<>
<DropdownMenuItem
className="flex items-center gap-3 [&_img]:w-full [&_img]:h-full"
asChild
>
<Link to="/settings/account">
<div className="flex flex-col">
<span className="text-white">{user.username}</span>
<span className="text-xs font-semibold">{user.email}</span>
</div>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/install">
<MonitorDownIcon />
<span>Install CLI</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/settings/account">
<CircleUserIcon />
<span>Account</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={onSignOut}>
<LogOutIcon />
<span>Sign Out</span>
</DropdownMenuItem>
{supportLinks && supportLinks.length > 0 && (
<div>
<Stack css={styles.info} spacing={0}>
<span css={styles.userName}>{user.username}</span>
<span css={styles.userEmail}>{user.email}</span>
</Stack>
<Divider css={{ marginBottom: 8 }} />
<Link to="/install" css={styles.link}>
<PopoverClose asChild>
<MenuItem css={styles.menuItem}>
<MonitorDownIcon className="size-5 text-content-secondary" />
<span css={styles.menuItemText}>Install CLI</span>
</MenuItem>
</PopoverClose>
</Link>
<Link to="/settings/account" css={styles.link}>
<PopoverClose asChild>
<MenuItem css={styles.menuItem}>
<CircleUserIcon className="size-5 text-content-secondary" />
<span css={styles.menuItemText}>{Language.accountLabel}</span>
</MenuItem>
</PopoverClose>
</Link>
<MenuItem css={styles.menuItem} onClick={onSignOut}>
<LogOutIcon className="size-5 text-content-secondary" />
<span css={styles.menuItemText}>{Language.signOutLabel}</span>
</MenuItem>
{supportLinks && (
<>
<DropdownMenuSeparator />
<Divider />
{supportLinks.map((link) => (
<DropdownMenuItem key={link.name} asChild>
<a href={link.target} target="_blank" rel="noreferrer">
{link.icon && <SupportIcon icon={link.icon} />}
<span>{link.name}</span>
</a>
</DropdownMenuItem>
<a
href={link.target}
key={link.name}
target="_blank"
rel="noreferrer"
css={styles.link}
>
<PopoverClose asChild>
<MenuItem css={styles.menuItem}>
{link.icon && (
<SupportIcon
icon={link.icon}
className="size-5 text-content-secondary"
/>
)}
<span css={styles.menuItemText}>{link.name}</span>
</MenuItem>
</PopoverClose>
</a>
))}
</>
)}
<DropdownMenuSeparator />
<Tooltip disableHoverableContent>
<TooltipTrigger asChild>
<DropdownMenuItem className="text-xs" asChild>
<Divider css={{ marginBottom: "0 !important" }} />
<Stack css={styles.info} spacing={0}>
<Tooltip>
<TooltipTrigger asChild>
<a
css={[styles.footerText, styles.buildInfo]}
href={buildInfo?.external_url}
className="flex items-center gap-2"
target="_blank"
rel="noreferrer"
>
<span className="flex-1">{buildInfo?.version}</span>
<SquareArrowOutUpRightIcon className="!size-icon-xs" />
{buildInfo?.version} <SquareArrowOutUpRightIcon />
</a>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="bottom">Browse the source code</TooltipContent>
</Tooltip>
{buildInfo?.deployment_id && (
<Tooltip disableHoverableContent>
<TooltipTrigger asChild>
<DropdownMenuItem
className="text-xs"
onSelect={(e) => {
e.preventDefault();
copyToClipboard(buildInfo.deployment_id);
}}
>
<span className="truncate flex-1">{buildInfo.deployment_id}</span>
{showCopiedSuccess ? (
<CheckIcon className="!size-icon-xs ml-auto" />
) : (
<CopyIcon className="!size-icon-xs ml-auto" />
)}
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="bottom">
{showCopiedSuccess ? "Copied!" : "Copy deployment ID"}
</TooltipContent>
<TooltipContent side="bottom">Browse the source code</TooltipContent>
</Tooltip>
)}
<DropdownMenuItem className="text-xs" disabled>
<span>{Language.copyrightText}</span>
</DropdownMenuItem>
</>
{buildInfo?.deployment_id && (
<div className="flex items-center text-xs">
<Tooltip>
<TooltipTrigger asChild>
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
{buildInfo.deployment_id}
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
Deployment Identifier
</TooltipContent>
</Tooltip>
<CopyButton
text={buildInfo.deployment_id}
label="Copy deployment ID"
/>
</div>
)}
<div css={styles.footerText}>{Language.copyrightText}</div>
</Stack>
</div>
);
};
const styles = {
info: (theme) => [
theme.typography.body2 as CSSObject,
{
padding: 20,
},
],
userName: {
fontWeight: 600,
},
userEmail: (theme) => ({
color: theme.palette.text.secondary,
width: "100%",
textOverflow: "ellipsis",
overflow: "hidden",
}),
link: {
textDecoration: "none",
color: "inherit",
},
menuItem: (theme) => css`
gap: 20px;
padding: 8px 20px;
&:hover {
background-color: ${theme.palette.action.hover};
transition: background-color 0.3s ease;
}
`,
menuItemText: {
fontSize: 14,
},
footerText: (theme) => css`
font-size: 12px;
text-decoration: none;
color: ${theme.palette.text.secondary};
display: flex;
align-items: center;
gap: 4px;
& svg {
width: 12px;
height: 12px;
}
`,
buildInfo: (theme) => ({
color: theme.palette.text.primary,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -130,11 +130,19 @@ const TemplateRow: FC<TemplateRowProps> = ({
<TableCell css={styles.secondary}>
{showOrganizations ? (
<AvatarData
title={template.organization_display_name}
subtitle={`Used by ${Language.developerCount(template.active_user_count)}`}
avatar={<Avatar variant="icon" src={template.organization_icon} />}
/>
<Stack
spacing={0}
css={{
width: "100%",
}}
>
<span css={styles.cellPrimaryLine}>
{template.organization_display_name}
</span>
<span css={styles.cellSecondaryLine}>
Used by {Language.developerCount(template.active_user_count)}
</span>
</Stack>
) : (
Language.developerCount(template.active_user_count)
)}
+5 -4
View File
@@ -7,7 +7,6 @@ import {
Navigate,
Outlet,
Route,
ScrollRestoration,
} from "react-router";
import { Loader } from "./components/Loader/Loader";
import { RequireAuth } from "./contexts/auth/RequireAuth";
@@ -352,11 +351,10 @@ const AIBridgeRequestLogsPage = lazy(
() => import("./pages/AIBridgePage/RequestLogsPage/RequestLogsPage"),
);
const GlobalLayout = () => {
const RoutesWithSuspense = () => {
return (
<Suspense fallback={<Loader fullscreen />}>
<Outlet />
<ScrollRestoration />
</Suspense>
);
};
@@ -411,7 +409,10 @@ const groupsRouter = () => {
export const router = createBrowserRouter(
createRoutesFromChildren(
<Route element={<GlobalLayout />} errorElement={<GlobalErrorBoundary />}>
<Route
element={<RoutesWithSuspense />}
errorElement={<GlobalErrorBoundary />}
>
<Route path="login" element={<LoginPage />} />
<Route path="login/device" element={<LoginOAuthDevicePage />} />
<Route path="setup" element={<SetupPage />} />