From a4f0b9e6e64d58a400bf940cb9da6949589f91e6 Mon Sep 17 00:00:00 2001 From: hunnywar Date: Wed, 8 Jan 2025 18:36:11 +0530 Subject: [PATCH] fix merge Signed-off-by: hunnywar --- cmd/daytona/config/const.go | 1 + docs/daytona_code.md | 2 +- docs/daytona_create.md | 2 +- hack/docs/daytona_code.yaml | 2 +- hack/docs/daytona_create.yaml | 2 +- hack/get-ttyd.sh | 53 +++++++++++++ pkg/cmd/workspace/code.go | 2 + pkg/ide/browser-terminal.go | 137 ++++++++++++++++++++++++++++++++++ 8 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 hack/get-ttyd.sh create mode 100644 pkg/ide/browser-terminal.go diff --git a/cmd/daytona/config/const.go b/cmd/daytona/config/const.go index 243a32683b..ca9743c313 100644 --- a/cmd/daytona/config/const.go +++ b/cmd/daytona/config/const.go @@ -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"}, diff --git a/docs/daytona_code.md b/docs/daytona_code.md index a37f6d179b..3cd12db800 100644 --- a/docs/daytona_code.md +++ b/docs/daytona_code.md @@ -9,7 +9,7 @@ daytona code [WORKSPACE] [PROJECT] [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 ``` diff --git a/docs/daytona_create.md b/docs/daytona_create.md index 03f852d4df..d3bdaabaff 100644 --- a/docs/daytona_create.md +++ b/docs/daytona_create.md @@ -17,7 +17,7 @@ daytona create [REPOSITORY_URL | PROJECT_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) --manual Manually enter the Git repository --multi-project Workspace with multiple projects/repos --name string Specify the workspace name diff --git a/hack/docs/daytona_code.yaml b/hack/docs/daytona_code.yaml index 049123a7d7..f2cd996d17 100644 --- a/hack/docs/daytona_code.yaml +++ b/hack/docs/daytona_code.yaml @@ -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" diff --git a/hack/docs/daytona_create.yaml b/hack/docs/daytona_create.yaml index d8afd7b6ab..325c301f34 100644 --- a/hack/docs/daytona_create.yaml +++ b/hack/docs/daytona_create.yaml @@ -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: manual default_value: "false" usage: Manually enter the Git repository diff --git a/hack/get-ttyd.sh b/hack/get-ttyd.sh new file mode 100644 index 0000000000..6bb7dfc2f4 --- /dev/null +++ b/hack/get-ttyd.sh @@ -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" \ No newline at end of file diff --git a/pkg/cmd/workspace/code.go b/pkg/cmd/workspace/code.go index 74e0fb4df3..db6e55a985 100644 --- a/pkg/cmd/workspace/code.go +++ b/pkg/cmd/workspace/code.go @@ -187,6 +187,8 @@ func openIDE(ideId string, activeProfile config.Profile, workspaceId string, pro return ide.OpenTerminalSsh(activeProfile, workspaceId, projectName, gpgKey, nil) case "browser": return ide.OpenBrowserIDE(activeProfile, workspaceId, projectName, projectProviderMetadata, gpgKey) + case "browser-tty": + return ide.OpenBrowserTerminal(activeProfile, workspaceId, projectName, gpgKey) case "codium": return ide.OpenVScodium(activeProfile, workspaceId, projectName, projectProviderMetadata, gpgKey) case "codium-insiders": diff --git a/pkg/ide/browser-terminal.go b/pkg/ide/browser-terminal.go new file mode 100644 index 0000000000..8c19b41ad0 --- /dev/null +++ b/pkg/ide/browser-terminal.go @@ -0,0 +1,137 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package ide + +import ( + "context" + "fmt" + "io" + "os" + "os/signal" + "os/exec" + "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, projectName 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, projectName, gpgKey) + if err != nil { + return err + } + + views.RenderInfoMessageBold("Downloading Terminal Server...") + projectHostname := config.GetProjectHostname(activeProfile.Id, workspaceId, projectName) + + // Download and install ttyd + installServerCommand := exec.Command("ssh", projectHostname, "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 + } + + projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName, 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", projectHostname, fmt.Sprintf("%s %s bash", startCommand, projectDir)) + 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, projectName, 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", projectName, 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 +} \ No newline at end of file