Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Browser Terminal #1673

Closed
wants to merge 2 commits into from
Closed
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
1 change: 1 addition & 0 deletions cmd/daytona/config/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func GetIdeList() []Ide {
{"codium", "VSCodium"},
{"codium-insiders", "VSCodium Insiders"},
{"ssh", "Terminal SSH"},
{"browser-tty", "Browser Terminal"},
{"jupyter", "Jupyter"},
{"fleet", "Fleet"},
{"positron", "Positron"},
Expand Down
2 changes: 1 addition & 1 deletion docs/daytona_code.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ daytona code [WORKSPACE] [flags]
### Options

```
-i, --ide string Specify the IDE (vscode, code-insiders, browser, cursor, codium, codium-insiders, ssh, jupyter, fleet, positron, zed, windsurf, clion, goland, intellij, phpstorm, pycharm, rider, rubymine, webstorm)
-i, --ide string Specify the IDE (vscode, code-insiders, browser, cursor, codium, codium-insiders, ssh, browser-tty, jupyter, fleet, positron, zed, windsurf, clion, goland, intellij, phpstorm, pycharm, rider, rubymine, webstorm)
-y, --yes Automatically confirm any prompts
```

Expand Down
2 changes: 1 addition & 1 deletion docs/daytona_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ daytona create [REPOSITORY_URL | WORKSPACE_CONFIG_NAME]... [flags]
--devcontainer-path string Automatically assign the devcontainer builder with the path passed as the flag value
--env stringArray Specify environment variables (e.g. --env 'KEY1=VALUE1' --env 'KEY2=VALUE2' ...')
--git-provider-config string Specify the Git provider configuration ID or alias
-i, --ide string Specify the IDE (vscode, code-insiders, browser, cursor, codium, codium-insiders, ssh, jupyter, fleet, positron, zed, windsurf, clion, goland, intellij, phpstorm, pycharm, rider, rubymine, webstorm)
-i, --ide string Specify the IDE (vscode, code-insiders, browser, cursor, codium, codium-insiders, ssh, browser-tty, jupyter, fleet, positron, zed, windsurf, clion, goland, intellij, phpstorm, pycharm, rider, rubymine, webstorm)
--label stringArray Specify labels (e.g. --label 'label.key1=VALUE1' --label 'label.key2=VALUE2' ...)
--manual Manually enter the Git repository
--multi-workspace Target with multiple workspaces/repos
Expand Down
2 changes: 1 addition & 1 deletion hack/docs/daytona_code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ options:
- name: ide
shorthand: i
usage: |
Specify the IDE (vscode, code-insiders, browser, cursor, codium, codium-insiders, ssh, jupyter, fleet, positron, zed, windsurf, clion, goland, intellij, phpstorm, pycharm, rider, rubymine, webstorm)
Specify the IDE (vscode, code-insiders, browser, cursor, codium, codium-insiders, ssh, browser-tty, jupyter, fleet, positron, zed, windsurf, clion, goland, intellij, phpstorm, pycharm, rider, rubymine, webstorm)
- name: "yes"
shorthand: "y"
default_value: "false"
Expand Down
2 changes: 1 addition & 1 deletion hack/docs/daytona_create.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ options:
- name: ide
shorthand: i
usage: |
Specify the IDE (vscode, code-insiders, browser, cursor, codium, codium-insiders, ssh, jupyter, fleet, positron, zed, windsurf, clion, goland, intellij, phpstorm, pycharm, rider, rubymine, webstorm)
Specify the IDE (vscode, code-insiders, browser, cursor, codium, codium-insiders, ssh, browser-tty, jupyter, fleet, positron, zed, windsurf, clion, goland, intellij, phpstorm, pycharm, rider, rubymine, webstorm)
- name: label
default_value: '[]'
usage: |
Expand Down
53 changes: 53 additions & 0 deletions hack/get-ttyd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/bash
# Copyright 2024 Daytona Platforms Inc.
# SPDX-License-Identifier: Apache-2.0

RELEASE_TAG="1.7.7"
RELEASE_ORG="tsl0922"
TTYD_ROOT="$HOME/ttyd"

# Check if ttyd is already installed
if [ -d "$TTYD_ROOT" ]; then
echo "Terminal Server is already installed. Skipping installation."
exit 0
fi

# Ensure the RELEASE_TAG is set
if [ -z "$RELEASE_TAG" ]; then
echo "The RELEASE_TAG build arg must be set." >&2
exit 1
fi

# Determine system architecture
arch=$(uname -m)
if [ "$arch" = "x86_64" ]; then
arch="x86_64"
elif [ "$arch" = "aarch64" ]; then
arch="aarch64"
elif [ "$arch" = "armv7l" ]; then
arch="armhf"
else
echo "Unsupported architecture: $arch"
exit 1
fi

# Define the download URL and target file
download_url="https://github.com/$RELEASE_ORG/ttyd/releases/download/$RELEASE_TAG/ttyd.$arch"
target_file="$HOME/ttyd-$arch"

# Download the file using wget or curl
if command -v wget &>/dev/null; then
wget -O "$target_file" "$download_url"
elif command -v curl &>/dev/null; then
curl -fsSL -o "$target_file" "$download_url"
else
echo "Neither wget nor curl is available. Please install one of them."
exit 1
fi

# Make the binary executable
chmod +x "$target_file"

# Move ttyd to the installation directory
mkdir -p "$TTYD_ROOT/bin"
mv "$target_file" "$TTYD_ROOT/bin/ttyd"
2 changes: 2 additions & 0 deletions pkg/cmd/common/open_ide.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func OpenIDE(ideId string, activeProfile config.Profile, workspaceId string, wor
err = ide.OpenTerminalSsh(activeProfile, workspaceId, gpgKey, nil)
case "browser":
err = ide.OpenBrowserIDE(activeProfile, workspaceId, workspaceProviderMetadata, gpgKey)
case "browser-tty":
err = ide.OpenBrowserTerminal(activeProfile, workspaceId, gpgKey)
case "codium":
err = ide.OpenVScodium(activeProfile, workspaceId, workspaceProviderMetadata, gpgKey)
case "codium-insiders":
Expand Down
137 changes: 137 additions & 0 deletions pkg/ide/browser-terminal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright 2024 Daytona Platforms Inc.
// SPDX-License-Identifier: Apache-2.0

package ide

import (
"context"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"sync"
"syscall"
"time"

"github.com/daytonaio/daytona/cmd/daytona/config"
"github.com/daytonaio/daytona/internal/cmd/tailscale"
"github.com/daytonaio/daytona/internal/util"
"github.com/daytonaio/daytona/pkg/ports"
"github.com/daytonaio/daytona/pkg/views"

"github.com/pkg/browser"
log "github.com/sirupsen/logrus"
)

const startCommand = "$HOME/ttyd/bin/ttyd --port 63777 --writable --cwd"

// OpenBrowserTerminal starts a browser-based terminal and opens it in the browser
func OpenBrowserTerminal(activeProfile config.Profile, workspaceId string, gpgKey *string) error {
// Create a cancellation context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Ensure the context is canceled on exit

// Capture OS interrupt signals for graceful exit
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)

// WaitGroup to wait for all goroutines to finish
var wg sync.WaitGroup

// Ensure SSH config exists
err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, gpgKey)
if err != nil {
return err
}

views.RenderInfoMessageBold("Downloading Terminal Server...")
workspaceHostname := config.GetHostname(activeProfile.Id, workspaceId)

// Download and install ttyd
installServerCommand := exec.Command("ssh", workspaceHostname, "curl -fsSL https://download.daytona.io/daytona/tools/get-ttyd.sh | sh")
installServerCommand.Stdout = io.Writer(&util.DebugLogWriter{})
installServerCommand.Stderr = io.Writer(&util.DebugLogWriter{})

err = installServerCommand.Run()
if err != nil {
return err
}

workspaceDir, err := util.GetWorkspaceDir(activeProfile, workspaceId, gpgKey)
if err != nil {
return err
}

views.RenderInfoMessageBold("Starting Terminal Server...")

// Start the terminal server in a goroutine
wg.Add(1)
go func() {
defer wg.Done()

startServerCommand := exec.CommandContext(ctx, "ssh", workspaceHostname, fmt.Sprintf("%s %s bash", startCommand, workspaceDir))
startServerCommand.Stdout = io.Writer(&util.DebugLogWriter{})
startServerCommand.Stderr = io.Writer(&util.DebugLogWriter{})

err = startServerCommand.Run()
if err != nil && ctx.Err() == nil { // Ignore errors if context was canceled
log.Error(err)
}
}()

// Forward ttyd (Terminal server) port
browserPort, errChan := tailscale.ForwardPort(workspaceId, 63777, activeProfile)
if browserPort == nil {
if err := <-errChan; err != nil {
return err
}
}

ideURL := fmt.Sprintf("http://localhost:%d", *browserPort)
// Wait for the port to be ready
for {
if ports.IsPortReady(*browserPort) {
break
}
time.Sleep(100 * time.Millisecond)
}

views.RenderInfoMessageBold(fmt.Sprintf("Forwarded %s Terminal port to %s.\nOpening browser...\n", workspaceId, ideURL))

err = browser.OpenURL(ideURL)
if err != nil {
log.Error("Error opening URL: " + err.Error())
}

// Handle errors from the port-forwarding goroutine
wg.Add(1)
go func() {
defer wg.Done()

for {
select {
case <-ctx.Done():
return
case err := <-errChan:
if err != nil {
// Log errors in debug mode
// Connection errors to the forwarded port should not exit the process
log.Debug(err)
}
}
}
}()

// Wait for a termination signal
<-signalChan
log.Info("Received termination signal. Shutting down gracefully...")

// Cancel the context to stop all goroutines
cancel()

// Wait for all goroutines to complete
wg.Wait()
log.Info("All tasks stopped. Exiting.")
return nil
}
Loading