From b1e8d5d5e09743d4b3cc477f736f895a5e50ce5f Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Mon, 30 Jun 2025 21:23:09 +0500 Subject: [PATCH 01/70] docs: remove beta label from Coder Desktop (#18651) Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: Edward Angert --- docs/manifest.json | 6 ++---- docs/user-guides/desktop/index.md | 6 +++--- docs/user-guides/workspace-access/remote-desktops.md | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index ef42dfbbce510..a139889baf68d 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -258,16 +258,14 @@ }, { "title": "Coder Desktop", - "description": "Use Coder Desktop to access your workspace like it's a local machine", + "description": "Transform remote workspaces into seamless local development environments with no port forwarding required", "path": "./user-guides/desktop/index.md", "icon_path": "./images/icons/computer-code.svg", - "state": ["beta"], "children": [ { "title": "Coder Desktop connect and sync", "description": "Use Coder Desktop to manage your workspace code and files locally", - "path": "./user-guides/desktop/desktop-connect-sync.md", - "state": ["beta"] + "path": "./user-guides/desktop/desktop-connect-sync.md" } ] }, diff --git a/docs/user-guides/desktop/index.md b/docs/user-guides/desktop/index.md index d56303f45dca9..6b50123f17048 100644 --- a/docs/user-guides/desktop/index.md +++ b/docs/user-guides/desktop/index.md @@ -1,7 +1,7 @@ -# Coder Desktop (Beta) +# Coder Desktop -Use Coder Desktop to work on your workspaces as though they're on your LAN, no -port-forwarding required. +Coder Desktop provides seamless access to your remote workspaces without the need to install a CLI or configure manual port forwarding. +Connect to workspace services using simple hostnames like `myworkspace.coder`, launch native applications with one click, and synchronize files between local and remote environments. > [!NOTE] > Coder Desktop requires a Coder deployment running [v2.20.0](https://github.com/coder/coder/releases/tag/v2.20.0) or later. diff --git a/docs/user-guides/workspace-access/remote-desktops.md b/docs/user-guides/workspace-access/remote-desktops.md index a60e943cea86a..56bd17bf8b8a9 100644 --- a/docs/user-guides/workspace-access/remote-desktops.md +++ b/docs/user-guides/workspace-access/remote-desktops.md @@ -42,7 +42,7 @@ coder port-forward --tcp 3399:3389 Then, connect to your workspace via RDP at `localhost:3399`. ![windows-rdp](../../images/ides/windows_rdp_client.png) -### RDP with Coder Desktop (Beta) +### RDP with Coder Desktop [Coder Desktop](../desktop/index.md)'s Coder Connect feature creates a connection to your workspaces in the background. There is no need for port forwarding when it is enabled. From 715c7b0c2402eae3763a77c9455bf7e5183306a1 Mon Sep 17 00:00:00 2001 From: Vladislav Rudskoy Date: Mon, 30 Jun 2025 19:46:00 +0200 Subject: [PATCH 02/70] chore: correct RD limitation comment (#18668) subj --- docs/user-guides/workspace-access/jetbrains/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guides/workspace-access/jetbrains/index.md b/docs/user-guides/workspace-access/jetbrains/index.md index 1c42273b42145..8189d1333ad3b 100644 --- a/docs/user-guides/workspace-access/jetbrains/index.md +++ b/docs/user-guides/workspace-access/jetbrains/index.md @@ -15,7 +15,7 @@ IDEs are supported for remote development: - [JetBrains Fleet](./fleet.md) > [!IMPORTANT] -> Remote development only works with paid versions of JetBrains IDEs. +> Remote development works with paid and non-commercial licenses of JetBrains IDEs From 9ccaf86099e3f0a84b81f5e9b889d136ef7f45a8 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 30 Jun 2025 20:56:39 +0300 Subject: [PATCH 03/70] fix(agent/agentcontainers): always derive devcontainer name from workspace folder (#18666) --- agent/agentcontainers/api.go | 28 ++++++++--- agent/agentcontainers/api_test.go | 79 +++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 3faa97c3e0511..ddf5ce438ce3d 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -211,6 +211,25 @@ func WithDevcontainers(devcontainers []codersdk.WorkspaceAgentDevcontainer, scri if dc.Status == "" { dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStarting } + logger := api.logger.With( + slog.F("devcontainer_id", dc.ID), + slog.F("devcontainer_name", dc.Name), + slog.F("workspace_folder", dc.WorkspaceFolder), + slog.F("config_path", dc.ConfigPath), + ) + + // Devcontainers have a name originating from Terraform, but + // we need to ensure that the name is unique. We will use + // the workspace folder name to generate a unique agent name, + // and if that fails, we will fall back to the devcontainers + // original name. + name, usingWorkspaceFolder := api.makeAgentName(dc.WorkspaceFolder, dc.Name) + if name != dc.Name { + logger = logger.With(slog.F("devcontainer_name", name)) + logger.Debug(api.ctx, "updating devcontainer name", slog.F("devcontainer_old_name", dc.Name)) + dc.Name = name + api.usingWorkspaceFolderName[dc.WorkspaceFolder] = usingWorkspaceFolder + } api.knownDevcontainers[dc.WorkspaceFolder] = dc api.devcontainerNames[dc.Name] = true @@ -223,12 +242,7 @@ func WithDevcontainers(devcontainers []codersdk.WorkspaceAgentDevcontainer, scri } } if api.devcontainerLogSourceIDs[dc.WorkspaceFolder] == uuid.Nil { - api.logger.Error(api.ctx, "devcontainer log source ID not found for devcontainer", - slog.F("devcontainer_id", dc.ID), - slog.F("devcontainer_name", dc.Name), - slog.F("workspace_folder", dc.WorkspaceFolder), - slog.F("config_path", dc.ConfigPath), - ) + logger.Error(api.ctx, "devcontainer log source ID not found for devcontainer") } } } @@ -872,7 +886,7 @@ func (api *API) getContainers() (codersdk.WorkspaceAgentListContainersResponse, devcontainers = append(devcontainers, dc) } slices.SortFunc(devcontainers, func(a, b codersdk.WorkspaceAgentDevcontainer) int { - return strings.Compare(a.Name, b.Name) + return strings.Compare(a.WorkspaceFolder, b.WorkspaceFolder) }) } diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index ab857ee59cb0b..2a742e3be8b2f 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -2596,3 +2596,82 @@ func fakeContainer(t *testing.T, mut ...func(*codersdk.WorkspaceAgentContainer)) } return ct } + +func TestWithDevcontainersNameGeneration(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip("Dev Container tests are not supported on Windows") + } + + devcontainers := []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.New(), + Name: "original-name", + WorkspaceFolder: "/home/coder/foo/project", + ConfigPath: "/home/coder/foo/project/.devcontainer/devcontainer.json", + }, + { + ID: uuid.New(), + Name: "another-name", + WorkspaceFolder: "/home/coder/bar/project", + ConfigPath: "/home/coder/bar/project/.devcontainer/devcontainer.json", + }, + } + + scripts := []codersdk.WorkspaceAgentScript{ + {ID: devcontainers[0].ID, LogSourceID: uuid.New()}, + {ID: devcontainers[1].ID, LogSourceID: uuid.New()}, + } + + logger := testutil.Logger(t) + + // This should trigger the WithDevcontainers code path where names are generated + api := agentcontainers.NewAPI(logger, + agentcontainers.WithDevcontainers(devcontainers, scripts), + agentcontainers.WithContainerCLI(&fakeContainerCLI{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{ + fakeContainer(t, func(c *codersdk.WorkspaceAgentContainer) { + c.ID = "some-container-id-1" + c.FriendlyName = "container-name-1" + c.Labels[agentcontainers.DevcontainerLocalFolderLabel] = "/home/coder/baz/project" + c.Labels[agentcontainers.DevcontainerConfigFileLabel] = "/home/coder/baz/project/.devcontainer/devcontainer.json" + }), + }, + }, + }), + agentcontainers.WithDevcontainerCLI(&fakeDevcontainerCLI{}), + agentcontainers.WithSubAgentClient(&fakeSubAgentClient{}), + agentcontainers.WithWatcher(watcher.NewNoop()), + ) + defer api.Close() + api.Start() + + r := chi.NewRouter() + r.Mount("/", api.Routes()) + + ctx := context.Background() + + err := api.RefreshContainers(ctx) + require.NoError(t, err, "RefreshContainers should not error") + + // Initial request returns the initial data. + req := httptest.NewRequest(http.MethodGet, "/", nil). + WithContext(ctx) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + var response codersdk.WorkspaceAgentListContainersResponse + err = json.NewDecoder(rec.Body).Decode(&response) + require.NoError(t, err) + + // Verify the devcontainers have the expected names. + require.Len(t, response.Devcontainers, 3, "should have two devcontainers") + assert.NotEqual(t, "original-name", response.Devcontainers[2].Name, "first devcontainer should not keep original name") + assert.Equal(t, "project", response.Devcontainers[2].Name, "first devcontainer should use the project folder name") + assert.NotEqual(t, "another-name", response.Devcontainers[0].Name, "second devcontainer should not keep original name") + assert.Equal(t, "bar-project", response.Devcontainers[0].Name, "second devcontainer should has a collision and uses the folder name with a prefix") + assert.Equal(t, "baz-project", response.Devcontainers[1].Name, "third devcontainer should use the folder name with a prefix since it collides with the first two") +} From b7cb275d7e79a051a85f854f82a432332acaec76 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Mon, 30 Jun 2025 22:06:05 +0400 Subject: [PATCH 04/70] fix: stop tearing down non-TTY processes on SSH session end (#18673) (possibly temporary) fix for #18519 Matches OpenSSH for non-tty sessions, where we don't actively terminate the process. Adds explicit tracking to the SSH server for these processes so that if we are shutting down we terminate them: this ensures that we can shut down quickly to allow shutdown scripts to run. It also ensures our tests don't leak system resources. --- agent/agentssh/agentssh.go | 42 +++++++++++++++++++++++++++++++++- agent/agentssh/exec_other.go | 10 ++++---- agent/agentssh/exec_windows.go | 20 ++++++++-------- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go index 6e3760c643cb3..18e647ef15c0b 100644 --- a/agent/agentssh/agentssh.go +++ b/agent/agentssh/agentssh.go @@ -129,6 +129,7 @@ type Server struct { listeners map[net.Listener]struct{} conns map[net.Conn]struct{} sessions map[ssh.Session]struct{} + processes map[*os.Process]struct{} closing chan struct{} // Wait for goroutines to exit, waited without // a lock on mu but protected by closing. @@ -188,6 +189,7 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom fs: fs, conns: make(map[net.Conn]struct{}), sessions: make(map[ssh.Session]struct{}), + processes: make(map[*os.Process]struct{}), logger: logger, config: config, @@ -606,7 +608,10 @@ func (s *Server) startNonPTYSession(logger slog.Logger, session ssh.Session, mag // otherwise context cancellation will not propagate properly // and SSH server close may be delayed. cmd.SysProcAttr = cmdSysProcAttr() - cmd.Cancel = cmdCancel(session.Context(), logger, cmd) + + // to match OpenSSH, we don't actually tear a non-TTY command down, even if the session ends. + // c.f. https://github.com/coder/coder/issues/18519#issuecomment-3019118271 + cmd.Cancel = nil cmd.Stdout = session cmd.Stderr = session.Stderr() @@ -629,6 +634,16 @@ func (s *Server) startNonPTYSession(logger slog.Logger, session ssh.Session, mag s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "start_command").Add(1) return xerrors.Errorf("start: %w", err) } + + // Since we don't cancel the process when the session stops, we still need to tear it down if we are closing. So + // track it here. + if !s.trackProcess(cmd.Process, true) { + // must be closing + err = cmdCancel(logger, cmd.Process) + return xerrors.Errorf("failed to track process: %w", err) + } + defer s.trackProcess(cmd.Process, false) + sigs := make(chan ssh.Signal, 1) session.Signals(sigs) defer func() { @@ -1089,6 +1104,27 @@ func (s *Server) trackSession(ss ssh.Session, add bool) (ok bool) { return true } +// trackCommand registers the process with the server. If the server is +// closing, the process is not registered and should be closed. +// +//nolint:revive +func (s *Server) trackProcess(p *os.Process, add bool) (ok bool) { + s.mu.Lock() + defer s.mu.Unlock() + if add { + if s.closing != nil { + // Server closed. + return false + } + s.wg.Add(1) + s.processes[p] = struct{}{} + return true + } + s.wg.Done() + delete(s.processes, p) + return true +} + // Close the server and all active connections. Server can be re-used // after Close is done. func (s *Server) Close() error { @@ -1128,6 +1164,10 @@ func (s *Server) Close() error { _ = c.Close() } + for p := range s.processes { + _ = cmdCancel(s.logger, p) + } + s.logger.Debug(ctx, "closing SSH server") err := s.srv.Close() diff --git a/agent/agentssh/exec_other.go b/agent/agentssh/exec_other.go index 54dfd50899412..aef496a1ef775 100644 --- a/agent/agentssh/exec_other.go +++ b/agent/agentssh/exec_other.go @@ -4,7 +4,7 @@ package agentssh import ( "context" - "os/exec" + "os" "syscall" "cdr.dev/slog" @@ -16,9 +16,7 @@ func cmdSysProcAttr() *syscall.SysProcAttr { } } -func cmdCancel(ctx context.Context, logger slog.Logger, cmd *exec.Cmd) func() error { - return func() error { - logger.Debug(ctx, "cmdCancel: sending SIGHUP to process and children", slog.F("pid", cmd.Process.Pid)) - return syscall.Kill(-cmd.Process.Pid, syscall.SIGHUP) - } +func cmdCancel(logger slog.Logger, p *os.Process) error { + logger.Debug(context.Background(), "cmdCancel: sending SIGHUP to process and children", slog.F("pid", p.Pid)) + return syscall.Kill(-p.Pid, syscall.SIGHUP) } diff --git a/agent/agentssh/exec_windows.go b/agent/agentssh/exec_windows.go index 39f0f97198479..0dafa67958a67 100644 --- a/agent/agentssh/exec_windows.go +++ b/agent/agentssh/exec_windows.go @@ -2,7 +2,7 @@ package agentssh import ( "context" - "os/exec" + "os" "syscall" "cdr.dev/slog" @@ -12,14 +12,12 @@ func cmdSysProcAttr() *syscall.SysProcAttr { return &syscall.SysProcAttr{} } -func cmdCancel(ctx context.Context, logger slog.Logger, cmd *exec.Cmd) func() error { - return func() error { - logger.Debug(ctx, "cmdCancel: killing process", slog.F("pid", cmd.Process.Pid)) - // Windows doesn't support sending signals to process groups, so we - // have to kill the process directly. In the future, we may want to - // implement a more sophisticated solution for process groups on - // Windows, but for now, this is a simple way to ensure that the - // process is terminated when the context is cancelled. - return cmd.Process.Kill() - } +func cmdCancel(logger slog.Logger, p *os.Process) error { + logger.Debug(context.Background(), "cmdCancel: killing process", slog.F("pid", p.Pid)) + // Windows doesn't support sending signals to process groups, so we + // have to kill the process directly. In the future, we may want to + // implement a more sophisticated solution for process groups on + // Windows, but for now, this is a simple way to ensure that the + // process is terminated when the context is cancelled. + return p.Kill() } From 22c5e84a7eb49fdd339ae97aa46ac405fb4a0649 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Mon, 30 Jun 2025 20:46:28 +0200 Subject: [PATCH 05/70] fix: handle health status when displaying task apps (#18675) Previously, we displayed apps in iframes on the task page without waiting for them to initialize. This would result in 502 errors shown to the user. This PR makes sure that we only display the app after it initializes. ### Before Screenshot 2025-06-30 at 14 59 07 (2) --- site/src/pages/TaskPage/TaskAppIframe.tsx | 34 +++++++++++++++----- site/src/pages/TaskPage/TaskApps.tsx | 8 +++++ site/src/pages/TaskPage/TaskPage.stories.tsx | 27 ++++++++++++++++ 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/site/src/pages/TaskPage/TaskAppIframe.tsx b/site/src/pages/TaskPage/TaskAppIframe.tsx index b995dfec771b6..ce0223e802fd9 100644 --- a/site/src/pages/TaskPage/TaskAppIframe.tsx +++ b/site/src/pages/TaskPage/TaskAppIframe.tsx @@ -6,6 +6,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "components/DropdownMenu/DropdownMenu"; +import { Spinner } from "components/Spinner/Spinner"; import { EllipsisVertical, ExternalLinkIcon, HouseIcon } from "lucide-react"; import { useAppLink } from "modules/apps/useAppLink"; import type { Task } from "modules/tasks/tasks"; @@ -97,14 +98,31 @@ export const TaskAppIFrame: FC = ({ )} -