Files
drip/internal/client/cli/tunnel_runner.go
Gouryella aead68bb62 feat: Add HTTP streaming, compression support, and Docker deployment
enhancements

  - Add adaptive HTTP response handling with automatic streaming for large
  responses (>1MB)
  - Implement zero-copy streaming using buffer pools for better performance
  - Add compression module for reduced bandwidth usage
  - Add GitHub Container Registry workflow for automated Docker builds
  - Add production-optimized Dockerfile and docker-compose configuration
  - Simplify background mode with -d flag and improved daemon management
  - Update documentation with new command syntax and deployment guides
  - Clean up unused code and improve error handling
  - Fix lipgloss style usage (remove unnecessary .Copy() calls)
2025-12-05 22:09:07 +08:00

206 lines
4.7 KiB
Go

package cli
import (
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"drip/internal/client/cli/ui"
"drip/internal/client/tcp"
"drip/internal/shared/utils"
"go.uber.org/zap"
)
// runTunnelWithUI runs a tunnel with the new UI
func runTunnelWithUI(connConfig *tcp.ConnectorConfig, daemonInfo *DaemonInfo) error {
if err := utils.InitLogger(verbose); err != nil {
return fmt.Errorf("failed to initialize logger: %w", err)
}
defer utils.Sync()
logger := utils.GetLogger()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
reconnectAttempts := 0
for {
connector := tcp.NewConnector(connConfig, logger)
fmt.Println(ui.RenderConnecting(connConfig.ServerAddr, reconnectAttempts, maxReconnectAttempts))
if err := connector.Connect(); err != nil {
if isNonRetryableError(err) {
return fmt.Errorf("failed to connect: %w", err)
}
reconnectAttempts++
if reconnectAttempts >= maxReconnectAttempts {
return fmt.Errorf("failed to connect after %d attempts: %w", maxReconnectAttempts, err)
}
fmt.Println(ui.RenderConnectionFailed(err))
fmt.Println(ui.RenderRetrying(reconnectInterval))
select {
case <-quit:
fmt.Println(ui.RenderShuttingDown())
return nil
case <-time.After(reconnectInterval):
continue
}
}
reconnectAttempts = 0
if daemonInfo != nil {
daemonInfo.URL = connector.GetURL()
if err := SaveDaemonInfo(daemonInfo); err != nil {
logger.Warn("Failed to save daemon info", zap.Error(err))
}
}
displayAddr := connConfig.LocalHost
if displayAddr == "127.0.0.1" {
displayAddr = "localhost"
}
status := &ui.TunnelStatus{
Type: string(connConfig.TunnelType),
URL: connector.GetURL(),
LocalAddr: fmt.Sprintf("%s:%d", displayAddr, connConfig.LocalPort),
}
fmt.Print(ui.RenderTunnelConnected(status))
latencyCh := make(chan time.Duration, 1)
connector.SetLatencyCallback(func(latency time.Duration) {
select {
case latencyCh <- latency:
default:
}
})
stopDisplay := make(chan struct{})
disconnected := make(chan struct{})
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
var lastLatency time.Duration
lastRenderedLines := 0
for {
select {
case latency := <-latencyCh:
lastLatency = latency
case <-ticker.C:
stats := connector.GetStats()
if stats != nil {
stats.UpdateSpeed()
snapshot := stats.GetSnapshot()
status.Latency = lastLatency
status.BytesIn = snapshot.TotalBytesIn
status.BytesOut = snapshot.TotalBytesOut
status.SpeedIn = float64(snapshot.SpeedIn)
status.SpeedOut = float64(snapshot.SpeedOut)
status.TotalRequest = snapshot.TotalRequests
statsView := ui.RenderTunnelStats(status)
if lastRenderedLines > 0 {
fmt.Print(clearLines(lastRenderedLines))
}
fmt.Print(statsView)
lastRenderedLines = countRenderedLines(statsView)
}
case <-stopDisplay:
return
}
}
}()
go func() {
connector.Wait()
close(disconnected)
}()
select {
case <-quit:
close(stopDisplay)
fmt.Println()
fmt.Println(ui.RenderShuttingDown())
// Close with timeout (wait for ongoing requests to complete)
done := make(chan struct{})
go func() {
connector.Close()
close(done)
}()
select {
case <-done:
// Closed successfully
case <-time.After(5 * time.Second):
fmt.Println(ui.Warning("Force closing (timeout)..."))
}
if daemonInfo != nil {
RemoveDaemonInfo(daemonInfo.Type, daemonInfo.Port)
}
fmt.Println(ui.Success("Tunnel closed"))
return nil
case <-disconnected:
close(stopDisplay)
fmt.Println()
fmt.Println(ui.RenderConnectionLost())
reconnectAttempts++
if reconnectAttempts >= maxReconnectAttempts {
return fmt.Errorf("connection lost after %d reconnect attempts", maxReconnectAttempts)
}
fmt.Println(ui.RenderRetrying(reconnectInterval))
select {
case <-quit:
fmt.Println(ui.RenderShuttingDown())
return nil
case <-time.After(reconnectInterval):
continue
}
}
}
}
func clearLines(lines int) string {
if lines <= 0 {
return ""
}
return fmt.Sprintf("\033[%dA\033[J", lines)
}
func countRenderedLines(block string) int {
if block == "" {
return 0
}
lines := strings.Count(block, "\n")
if !strings.HasSuffix(block, "\n") {
lines++
}
return lines
}
func isNonRetryableError(err error) bool {
errStr := err.Error()
return strings.Contains(errStr, "subdomain is already taken") ||
strings.Contains(errStr, "subdomain is reserved") ||
strings.Contains(errStr, "invalid subdomain") ||
strings.Contains(errStr, "authentication") ||
strings.Contains(errStr, "Invalid authentication token")
}