Files
drip/internal/client/cli/attach.go
zhiqing 307cf8e6cc feat: Add Bearer Token authentication support and optimize code structure
- Add Bearer Token authentication, supporting tunnel access control via the --auth-bearer parameter
- Refactor large modules into smaller, more focused components to improve code maintainability
- Update dependency versions, including golang.org/x/crypto, golang.org/x/net, etc.
- Add SilenceUsage and SilenceErrors configuration for all CLI commands
- Modify connector configuration structure to support the new authentication method
- Update recent change log in README with new feature descriptions

BREAKING CHANGE: Authentication via Bearer Token is now supported, requiring the new --auth-bearer parameter
2026-01-29 14:40:53 +08:00

231 lines
5.6 KiB
Go

package cli
import (
"bufio"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"drip/internal/shared/ui"
"github.com/spf13/cobra"
)
var attachCmd = &cobra.Command{
Use: "attach [type] [port]",
Short: "Attach to a running background tunnel",
Long: `Attach to a running background tunnel to view its logs in real-time.
Examples:
drip attach List running tunnels and select one
drip attach http 3000 Attach to HTTP tunnel on port 3000
drip attach https 8443 Attach to HTTPS tunnel on port 8443
drip attach tcp 5432 Attach to TCP tunnel on port 5432
Press Ctrl+C to detach (tunnel will continue running).`,
Aliases: []string{"logs", "tail"},
Args: cobra.MaximumNArgs(2),
RunE: runAttach,
SilenceUsage: true,
SilenceErrors: true,
}
func init() {
rootCmd.AddCommand(attachCmd)
}
func runAttach(_ *cobra.Command, args []string) error {
CleanupStaleDaemons()
daemons, err := ListAllDaemons()
if err != nil {
return fmt.Errorf("failed to list daemons: %w", err)
}
if len(daemons) == 0 {
fmt.Println(ui.Info(
"No Running Tunnels",
"",
ui.Muted("Start a tunnel in background with:"),
ui.Cyan(" drip http 3000 -d"),
ui.Cyan(" drip tcp 5432 -d"),
))
return nil
}
var selectedDaemon *DaemonInfo
if len(args) == 2 {
tunnelType := args[0]
if tunnelType != "http" && tunnelType != "https" && tunnelType != "tcp" {
return fmt.Errorf("invalid tunnel type: %s (must be 'http', 'https', or 'tcp')", tunnelType)
}
port, err := strconv.Atoi(args[1])
if err != nil || port < 1 || port > 65535 {
return fmt.Errorf("invalid port number: %s", args[1])
}
for _, d := range daemons {
if d.Type == tunnelType && d.Port == port {
if !IsProcessRunning(d.PID) {
RemoveDaemonInfo(d.Type, d.Port)
return fmt.Errorf("tunnel is not running (cleaned up stale entry)")
}
selectedDaemon = d
break
}
}
if selectedDaemon == nil {
return fmt.Errorf("no %s tunnel running on port %d", tunnelType, port)
}
} else if len(args) == 0 {
selectedDaemon, err = selectDaemonInteractive(daemons)
if err != nil {
return err
}
if selectedDaemon == nil {
return nil
}
} else {
return fmt.Errorf("usage: drip attach [type port]")
}
return attachToDaemon(selectedDaemon)
}
func selectDaemonInteractive(daemons []*DaemonInfo) (*DaemonInfo, error) {
var runningDaemons []*DaemonInfo
for _, d := range daemons {
if IsProcessRunning(d.PID) {
runningDaemons = append(runningDaemons, d)
} else {
RemoveDaemonInfo(d.Type, d.Port)
}
}
if len(runningDaemons) == 0 {
fmt.Println(ui.Muted("No running tunnels."))
return nil, nil
}
table := ui.NewTable([]string{"#", "TYPE", "PORT", "URL", "UPTIME"}).
WithTitle("Select a tunnel to attach")
for i, d := range runningDaemons {
uptime := time.Since(d.StartTime)
var typeStr string
switch d.Type {
case "http":
typeStr = ui.Highlight("HTTP")
case "https":
typeStr = ui.Highlight("HTTPS")
default:
typeStr = ui.Cyan("TCP")
}
table.AddRow([]string{
ui.Highlight(fmt.Sprintf("%d", i+1)),
typeStr,
fmt.Sprintf("%d", d.Port),
ui.URL(d.URL),
FormatDuration(uptime),
})
}
fmt.Print(table.Render())
fmt.Printf("Enter number (1-%d) or 'q' to quit: ", len(runningDaemons))
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("failed to read input: %w", err)
}
input = strings.TrimSpace(input)
if input == "q" || input == "Q" {
return nil, nil
}
selection, err := strconv.Atoi(input)
if err != nil || selection < 1 || selection > len(runningDaemons) {
return nil, fmt.Errorf("invalid selection: %s", input)
}
return runningDaemons[selection-1], nil
}
func attachToDaemon(daemon *DaemonInfo) error {
logPath := filepath.Join(getDaemonDir(), fmt.Sprintf("%s_%d.log", daemon.Type, daemon.Port))
if _, err := os.Stat(logPath); os.IsNotExist(err) {
return fmt.Errorf("log file not found: %s", logPath)
}
uptime := time.Since(daemon.StartTime)
fmt.Println(ui.Info(
fmt.Sprintf("Attached to %s tunnel on port %d", strings.ToUpper(daemon.Type), daemon.Port),
"",
ui.KeyValue("URL", ui.URL(daemon.URL)),
ui.KeyValue("PID", fmt.Sprintf("%d", daemon.PID)),
ui.KeyValue("Uptime", FormatDuration(uptime)),
ui.KeyValue("Log", truncatePath(logPath, 48)),
"",
ui.Warning("Press Ctrl+C to detach (tunnel will continue running)"),
))
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
tailCmd := exec.Command("tail", "-f", logPath)
tailCmd.Stdout = os.Stdout
tailCmd.Stderr = os.Stderr
if err := tailCmd.Start(); err != nil {
return fmt.Errorf("failed to start tail: %w", err)
}
done := make(chan error, 1)
go func() {
done <- tailCmd.Wait()
}()
select {
case <-sigCh:
if tailCmd.Process != nil {
_ = tailCmd.Process.Kill()
}
fmt.Println()
fmt.Println(ui.Warning("Detached from tunnel (tunnel is still running)"))
fmt.Println(ui.Muted(fmt.Sprintf("Use '%s' to reattach", ui.Cyan(fmt.Sprintf("drip attach %s %d", daemon.Type, daemon.Port)))))
fmt.Println(ui.Muted(fmt.Sprintf("Use '%s' to stop the tunnel", ui.Cyan(fmt.Sprintf("drip stop %s %d", daemon.Type, daemon.Port)))))
return nil
case err := <-done:
if err != nil {
return fmt.Errorf("tail process exited: %w", err)
}
return nil
}
}
func truncatePath(path string, maxLen int) string {
if len(path) <= maxLen {
return path
}
filename := filepath.Base(path)
if len(filename) >= maxLen-3 {
return "..." + filename[len(filename)-(maxLen-3):]
}
dirLen := maxLen - len(filename) - 3
return path[:dirLen] + "..." + filename
}