mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
* Add Linux implementation of VibeTunnel This commit introduces a complete Linux port of VibeTunnel, providing feature parity with the macOS version. The implementation includes: - Full Go-based server with identical REST API and WebSocket endpoints - Terminal session management using PTY (pseudo-terminal) handling - Asciinema recording format for session playback - Compatible CLI interface matching the macOS `vt` command - Support for all VibeTunnel features: password protection, network modes, ngrok integration - Comprehensive build system with Makefile supporting various installation methods - Systemd service integration for running as a system daemon The Linux version maintains 100% compatibility with the existing web UI and can be used as a drop-in replacement for the macOS app on Linux systems. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add comprehensive ngrok integration to Linux VibeTunnel Implements full ngrok tunnel support for the Go/Linux version to match the macOS Swift implementation, enabling secure public access to local VibeTunnel instances. - **ngrok Service**: Complete lifecycle management with status tracking - **HTTP API**: RESTful endpoints matching macOS version - **CLI Support**: Command-line ngrok flags and integration - **Auto-forwarding**: Built-in HTTP request forwarding to local server - `POST /api/ngrok/start` - Start tunnel with auth token - `POST /api/ngrok/stop` - Stop active tunnel - `GET /api/ngrok/status` - Get current tunnel status - Uses `golang.ngrok.com/ngrok` SDK for native Go integration - Thread-safe service with mutex protection - Comprehensive error handling and logging - Real-time status updates (disconnected/connecting/connected/error) - Proper context cancellation for graceful shutdown ```bash vibetunnel --serve --ngrok --ngrok-token "your_token" vibetunnel --serve --port 4030 --ngrok --ngrok-token "your_token" ``` - Added golang.ngrok.com/ngrok v1.13.0 - Updated web packages (security fixes for puppeteer) Maintains full API compatibility with macOS VibeTunnel for seamless cross-platform operation and consistent web frontend integration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * up * Fix SSE streaming performance with byte-based approach Addresses @badlogic's review feedback to prevent performance issues with line-based file reading in processNewContent(). ## Changes Made ### Performance Fix - **Byte-based seeking**: Replace line counting with file position tracking - **Efficient reads**: Only read new content since last position using file.Seek() - **Memory optimization**: Avoid reading entire file on each update - **Incomplete line handling**: Properly handle partial lines at file end ### Technical Details - Changed lastLineCount *int → seenBytes *int64 - Use file.Seek(seenBytes, 0) to jump to last read position - Read only new content with currentSize - seenBytes - Handle incomplete lines by adjusting seek position - Maintains same functionality with better performance ### Benefits - **Scalability**: No longer reads entire file for each update - **Performance**: O(new_content) instead of O(total_content) - **Memory**: Constant memory usage regardless of file size - **Reliability**: Handles concurrent writes and partial lines correctly This prevents the SSE streaming from exploding in our faces as @badlogic warned, especially for long-running sessions with large output files. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Optimize streaming performance to reduce 1+ second delays Implements multiple optimizations to address user-reported 1+ second delay between typing and stream rendering: ## PTY Reading Optimizations - **Reduced sleep times**: 100ms → 1ms for EOF checks - **Faster polling**: 10ms → 1ms for zero-byte reads - **FIFO optimization**: 1s → 100ms for stdin EOF polling ## UTF-8 Buffering Improvements - **Timeout-based flushing**: 5ms timer for incomplete UTF-8 sequences - **Real-time streaming**: Don't wait for complete sequences in interactive mode - **Smart buffering**: Balance correctness with responsiveness ## File I/O Optimizations - **Immediate sync**: Call file.Sync() after each write for instant fsnotify - **Reduced SSE timeout**: 1s → 100ms for session alive checks - **Better responsiveness**: Ensure file changes trigger immediately ## Technical Changes - Added StreamWriter.scheduleFlush() with 5ms timeout - Enhanced writeEvent() with conditional file syncing - Optimized PTY read/write loop timing - Improved SSE streaming frequency These changes target the main bottlenecks identified in the PTY → file → fsnotify → SSE → browser pipeline. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix critical stdin polling delay causing 1+ second input lag - Reduced FIFO EOF polling from 100ms to 1ms - Reduced EAGAIN polling from 1ms to 100µs - Added immediate continue after successful writes - This eliminates the major input delay bottleneck 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix critical performance issues causing resource leaks and CPU burns Performance optimizations based on code review feedback: 1. **Fix SSE goroutine leaks**: - Added client disconnect detection to SSE streams - Propagate write errors to detect when clients close connections - Prevents memory leaks from abandoned streaming goroutines 2. **Fix PTY busy-loop CPU burn**: - Increased sleep from 1ms to 10ms in idle scenarios - Reduces CPU wake-ups from 1000/s to 100/s (10x improvement) - Significantly reduces CPU usage when PTY is idle 3. **Multi-stream disconnect detection**: - Added error checking to multi-stream write operations - Prevents goroutine leaks in multi-session streaming These fixes address the "thing of the things" - performance\! 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Standardize session creation API response format to match Rust server Changes: - Updated Go server session creation response to include success/message/error fields - Now returns: {"success": true, "message": "Session created successfully", "error": null, "sessionId": "..."} - Maintains backward compatibility with existing sessionId field - Go server already supported both input formats (cmdline/command, cwd/workingDir) This achieves protocol compatibility between Go and Rust implementations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix delete endpoint to return 200 OK with JSON response - Changed handleKillSession to return 200 OK instead of 204 No Content - Added JSON response with success/message fields for consistency - Fixes benchmark tool compatibility expecting 200 response 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Update Go server API to match Rust format exactly - Use 'command' array instead of 'cmdline' - Use 'workingDir' instead of 'cwd' - Remove compatibility shims for cleaner API - Better error messages matching Rust server 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Major performance optimizations for Go server - Remove 100ms artificial delay in session creation (-100ms per session) - Optimize PTY I/O handling with reduced polling intervals - Implement persistent stdin pipes to avoid repeated open/close - Batch file sync operations to reduce I/O overhead (5ms batching) - Remove blocking status updates from API handlers - Increase SSE session check interval from 100ms to 1s Target: Match Rust performance (60ms avg latency, 16+ ops/sec) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix O_NONBLOCK compilation issue * Add comprehensive TLS/HTTPS support with Caddy integration Features: - Optional TLS support via CLI flags (defaults to HTTP like Rust) - Self-signed certificate generation for localhost development - Let's Encrypt automatic certificate management for domains - Custom certificate support for production environments - HTTP to HTTPS redirect capability - Maintains 100% backward compatibility with Rust version Usage examples: - Default HTTP: ./vibetunnel --serve (same as Rust) - HTTPS with self-signed: ./vibetunnel --serve --tls - HTTPS with domain: ./vibetunnel --serve --tls --tls-domain example.com - HTTPS with custom certs: ./vibetunnel --serve --tls --tls-cert cert.pem --tls-key key.pem 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix terminal sizing issues and implement dynamic resize support Backend changes: - Add handleResizeSession API endpoint for dynamic terminal resizing - Implement Session.Resize() and PTY.Resize() methods with proper validation - Add session registry in Manager to track running sessions with PTY access - Fix stdin error handling to prevent session crashes on EAGAIN errors - Write resize events to asciinema stream for frontend synchronization - Update default terminal dimensions from 80x24 to 120x30 Frontend changes: - Add width/height parameters to SessionCreateData interface - Calculate appropriate terminal dimensions when creating sessions - Implement automatic resize API calls when terminal dimensions change - Add terminal-resize event dispatch for backend synchronization - Ensure resize events bubble properly for session management Fixes nvim being stuck at 80x24 by implementing proper terminal dimension management and dynamic resizing capabilities. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add client-side resize caching and Hack Nerd Font support - Implement resize request caching to prevent redundant API calls - Add debouncing to terminal resize events (250ms delay) - Replace ResizeObserver with window.resize events only to eliminate pixel-level jitter - Add Hack Nerd Font Mono as primary terminal font with Fira Code fallback - Update session creation to use conservative 120x30 defaults - Fix terminal dimension calculation in normal mode 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add comprehensive XTerm color and rendering enhancements - Complete 256-color palette support with CSS variables (0-255) - Enhanced XTerm configuration with proper terminal options - True xterm-compatible 16-color theme - Text attribute support: bold, italic, underline, dim, strikethrough, inverse, invisible - Cursor blinking with CSS animation - Font rendering optimizations (disabled ligatures, antialiasing) - Terminal-specific CSS styling for better rendering - Mac option key as meta, alt-click cursor movement - Selection colors and inactive selection support 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
366 lines
No EOL
10 KiB
Go
366 lines
No EOL
10 KiB
Go
package session
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/creack/pty"
|
|
"github.com/vibetunnel/linux/pkg/protocol"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
type PTY struct {
|
|
session *Session
|
|
cmd *exec.Cmd
|
|
pty *os.File
|
|
oldState *term.State
|
|
streamWriter *protocol.StreamWriter
|
|
stdinPipe *os.File
|
|
resizeMutex sync.Mutex
|
|
}
|
|
|
|
func NewPTY(session *Session) (*PTY, error) {
|
|
log.Printf("[DEBUG] NewPTY: Starting PTY creation for session %s", session.ID[:8])
|
|
|
|
shell := os.Getenv("SHELL")
|
|
if shell == "" {
|
|
shell = "/bin/bash"
|
|
}
|
|
|
|
cmdline := session.info.Args
|
|
if len(cmdline) == 0 {
|
|
cmdline = []string{shell}
|
|
}
|
|
|
|
log.Printf("[DEBUG] NewPTY: Initial cmdline: %v", cmdline)
|
|
|
|
// For shells, force interactive mode to prevent immediate exit
|
|
if len(cmdline) == 1 && (strings.HasSuffix(cmdline[0], "bash") || strings.HasSuffix(cmdline[0], "zsh") || strings.HasSuffix(cmdline[0], "sh")) {
|
|
cmdline = append(cmdline, "-i")
|
|
// Update session info to reflect the actual command being run
|
|
session.info.Args = cmdline
|
|
session.info.Cmdline = strings.Join(cmdline, " ")
|
|
log.Printf("[DEBUG] NewPTY: Added -i flag, cmdline now: %v", cmdline)
|
|
}
|
|
|
|
cmd := exec.Command(cmdline[0], cmdline[1:]...)
|
|
|
|
// Set working directory, ensuring it's valid
|
|
if session.info.Cwd != "" {
|
|
// Verify the directory exists and is accessible
|
|
if _, err := os.Stat(session.info.Cwd); err != nil {
|
|
log.Printf("[ERROR] NewPTY: Working directory '%s' not accessible: %v", session.info.Cwd, err)
|
|
return nil, fmt.Errorf("working directory '%s' not accessible: %w", session.info.Cwd, err)
|
|
}
|
|
cmd.Dir = session.info.Cwd
|
|
log.Printf("[DEBUG] NewPTY: Set working directory to: %s", session.info.Cwd)
|
|
}
|
|
|
|
// Set up environment with proper terminal settings
|
|
env := os.Environ()
|
|
env = append(env, "TERM="+session.info.Term)
|
|
env = append(env, "SHELL="+cmdline[0])
|
|
cmd.Env = env
|
|
|
|
ptmx, err := pty.Start(cmd)
|
|
if err != nil {
|
|
log.Printf("[ERROR] NewPTY: Failed to start PTY: %v", err)
|
|
return nil, fmt.Errorf("failed to start PTY: %w", err)
|
|
}
|
|
|
|
log.Printf("[DEBUG] NewPTY: PTY started successfully, PID: %d", cmd.Process.Pid)
|
|
|
|
if err := pty.Setsize(ptmx, &pty.Winsize{
|
|
Rows: uint16(session.info.Height),
|
|
Cols: uint16(session.info.Width),
|
|
}); err != nil {
|
|
log.Printf("[ERROR] NewPTY: Failed to set PTY size: %v", err)
|
|
ptmx.Close()
|
|
cmd.Process.Kill()
|
|
return nil, fmt.Errorf("failed to set PTY size: %w", err)
|
|
}
|
|
|
|
streamOut, err := os.Create(session.StreamOutPath())
|
|
if err != nil {
|
|
log.Printf("[ERROR] NewPTY: Failed to create stream-out: %v", err)
|
|
ptmx.Close()
|
|
cmd.Process.Kill()
|
|
return nil, fmt.Errorf("failed to create stream-out: %w", err)
|
|
}
|
|
|
|
streamWriter := protocol.NewStreamWriter(streamOut, &protocol.AsciinemaHeader{
|
|
Version: 2,
|
|
Width: uint32(session.info.Width),
|
|
Height: uint32(session.info.Height),
|
|
Command: strings.Join(cmdline, " "),
|
|
Env: session.info.Env,
|
|
})
|
|
|
|
if err := streamWriter.WriteHeader(); err != nil {
|
|
log.Printf("[ERROR] NewPTY: Failed to write stream header: %v", err)
|
|
streamOut.Close()
|
|
ptmx.Close()
|
|
cmd.Process.Kill()
|
|
return nil, fmt.Errorf("failed to write stream header: %w", err)
|
|
}
|
|
|
|
stdinPath := session.StdinPath()
|
|
log.Printf("[DEBUG] NewPTY: Creating stdin FIFO at: %s", stdinPath)
|
|
if err := syscall.Mkfifo(stdinPath, 0600); err != nil {
|
|
log.Printf("[ERROR] NewPTY: Failed to create stdin pipe: %v", err)
|
|
streamOut.Close()
|
|
ptmx.Close()
|
|
cmd.Process.Kill()
|
|
return nil, fmt.Errorf("failed to create stdin pipe: %w", err)
|
|
}
|
|
|
|
return &PTY{
|
|
session: session,
|
|
cmd: cmd,
|
|
pty: ptmx,
|
|
streamWriter: streamWriter,
|
|
}, nil
|
|
}
|
|
|
|
func (p *PTY) Pid() int {
|
|
if p.cmd.Process != nil {
|
|
return p.cmd.Process.Pid
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (p *PTY) Run() error {
|
|
defer p.Close()
|
|
|
|
log.Printf("[DEBUG] PTY.Run: Starting PTY run for session %s, PID %d", p.session.ID[:8], p.cmd.Process.Pid)
|
|
|
|
stdinPipe, err := os.OpenFile(p.session.StdinPath(), os.O_RDONLY|syscall.O_NONBLOCK, 0)
|
|
if err != nil {
|
|
log.Printf("[ERROR] PTY.Run: Failed to open stdin pipe: %v", err)
|
|
return fmt.Errorf("failed to open stdin pipe: %w", err)
|
|
}
|
|
defer stdinPipe.Close()
|
|
p.stdinPipe = stdinPipe
|
|
|
|
log.Printf("[DEBUG] PTY.Run: Stdin pipe opened successfully")
|
|
|
|
errCh := make(chan error, 3)
|
|
|
|
go func() {
|
|
log.Printf("[DEBUG] PTY.Run: Starting output reading goroutine")
|
|
buf := make([]byte, 32*1024)
|
|
|
|
for {
|
|
// Use a timeout-based approach for cross-platform compatibility
|
|
// This avoids the complexity of non-blocking I/O syscalls
|
|
n, err := p.pty.Read(buf)
|
|
if n > 0 {
|
|
log.Printf("[DEBUG] PTY.Run: Read %d bytes of output from PTY", n)
|
|
if err := p.streamWriter.WriteOutput(buf[:n]); err != nil {
|
|
log.Printf("[ERROR] PTY.Run: Failed to write output: %v", err)
|
|
errCh <- fmt.Errorf("failed to write output: %w", err)
|
|
return
|
|
}
|
|
// Continue reading immediately if we got data
|
|
continue
|
|
}
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
// For blocking reads, EOF typically means the process exited
|
|
log.Printf("[DEBUG] PTY.Run: PTY reached EOF, process likely exited")
|
|
return
|
|
}
|
|
// For other errors, this is a problem
|
|
log.Printf("[ERROR] PTY.Run: OUTPUT GOROUTINE sending error to errCh: %v", err)
|
|
errCh <- fmt.Errorf("PTY read error: %w", err)
|
|
return
|
|
}
|
|
// If we get here, n == 0 and err == nil, which is unusual for blocking reads
|
|
// Give a very brief pause to prevent tight loop
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
log.Printf("[DEBUG] PTY.Run: Starting stdin reading goroutine")
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := stdinPipe.Read(buf)
|
|
if n > 0 {
|
|
log.Printf("[DEBUG] PTY.Run: Read %d bytes from stdin, writing to PTY", n)
|
|
if _, err := p.pty.Write(buf[:n]); err != nil {
|
|
log.Printf("[ERROR] PTY.Run: Failed to write to PTY: %v", err)
|
|
// Only exit if the PTY is really broken, not on temporary errors
|
|
if err != syscall.EPIPE && err != syscall.ECONNRESET {
|
|
errCh <- fmt.Errorf("failed to write to PTY: %w", err)
|
|
return
|
|
}
|
|
// For broken pipe, just continue - the PTY might be closing
|
|
log.Printf("[DEBUG] PTY.Run: PTY write failed with pipe error, continuing...")
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
// Continue immediately after successful write
|
|
continue
|
|
}
|
|
if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK {
|
|
// No data available, brief pause to prevent CPU spinning
|
|
time.Sleep(100 * time.Microsecond)
|
|
continue
|
|
}
|
|
if err == io.EOF {
|
|
// No writers to the FIFO yet, brief pause before retry
|
|
time.Sleep(500 * time.Microsecond)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
// Log other errors but don't crash the session - stdin issues shouldn't kill the PTY
|
|
log.Printf("[WARN] PTY.Run: Stdin read error (non-fatal): %v", err)
|
|
time.Sleep(1 * time.Millisecond)
|
|
continue
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
log.Printf("[DEBUG] PTY.Run: Starting process wait goroutine for PID %d", p.cmd.Process.Pid)
|
|
err := p.cmd.Wait()
|
|
log.Printf("[DEBUG] PTY.Run: Process wait completed for PID %d, error: %v", p.cmd.Process.Pid, err)
|
|
|
|
if err != nil {
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
|
exitCode := status.ExitStatus()
|
|
p.session.info.ExitCode = &exitCode
|
|
log.Printf("[DEBUG] PTY.Run: Process exited with code %d", exitCode)
|
|
}
|
|
} else {
|
|
log.Printf("[DEBUG] PTY.Run: Process exited with non-exit error: %v", err)
|
|
}
|
|
} else {
|
|
exitCode := 0
|
|
p.session.info.ExitCode = &exitCode
|
|
log.Printf("[DEBUG] PTY.Run: Process exited normally (code 0)")
|
|
}
|
|
p.session.info.Status = string(StatusExited)
|
|
p.session.info.Save(p.session.Path())
|
|
log.Printf("[DEBUG] PTY.Run: PROCESS WAIT GOROUTINE sending completion to errCh")
|
|
errCh <- err
|
|
}()
|
|
|
|
log.Printf("[DEBUG] PTY.Run: Waiting for first error from goroutines...")
|
|
result := <-errCh
|
|
log.Printf("[DEBUG] PTY.Run: Received error from goroutine: %v", result)
|
|
log.Printf("[DEBUG] PTY.Run: Process PID %d status after error: alive=%v", p.cmd.Process.Pid, p.session.IsAlive())
|
|
return result
|
|
}
|
|
|
|
func (p *PTY) Attach() error {
|
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
return fmt.Errorf("not a terminal")
|
|
}
|
|
|
|
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set raw mode: %w", err)
|
|
}
|
|
p.oldState = oldState
|
|
|
|
defer func() {
|
|
term.Restore(int(os.Stdin.Fd()), oldState)
|
|
}()
|
|
|
|
ch := make(chan os.Signal, 1)
|
|
signal.Notify(ch, syscall.SIGWINCH)
|
|
go func() {
|
|
for range ch {
|
|
p.updateSize()
|
|
}
|
|
}()
|
|
defer signal.Stop(ch)
|
|
|
|
p.updateSize()
|
|
|
|
errCh := make(chan error, 2)
|
|
|
|
go func() {
|
|
_, err := io.Copy(p.pty, os.Stdin)
|
|
errCh <- err
|
|
}()
|
|
|
|
go func() {
|
|
_, err := io.Copy(os.Stdout, p.pty)
|
|
errCh <- err
|
|
}()
|
|
|
|
return <-errCh
|
|
}
|
|
|
|
func (p *PTY) updateSize() error {
|
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
return nil
|
|
}
|
|
|
|
width, height, err := term.GetSize(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return pty.Setsize(p.pty, &pty.Winsize{
|
|
Rows: uint16(height),
|
|
Cols: uint16(width),
|
|
})
|
|
}
|
|
|
|
func (p *PTY) Resize(width, height int) error {
|
|
if p.pty == nil {
|
|
return fmt.Errorf("PTY not initialized")
|
|
}
|
|
|
|
p.resizeMutex.Lock()
|
|
defer p.resizeMutex.Unlock()
|
|
|
|
log.Printf("[DEBUG] PTY.Resize: Resizing PTY to %dx%d for session %s", width, height, p.session.ID[:8])
|
|
|
|
// Resize the actual PTY
|
|
err := pty.Setsize(p.pty, &pty.Winsize{
|
|
Rows: uint16(height),
|
|
Cols: uint16(width),
|
|
})
|
|
|
|
if err != nil {
|
|
log.Printf("[ERROR] PTY.Resize: Failed to resize PTY: %v", err)
|
|
return fmt.Errorf("failed to resize PTY: %w", err)
|
|
}
|
|
|
|
// Write resize event to stream if streamWriter is available
|
|
if p.streamWriter != nil {
|
|
if err := p.streamWriter.WriteResize(uint32(width), uint32(height)); err != nil {
|
|
log.Printf("[ERROR] PTY.Resize: Failed to write resize event: %v", err)
|
|
// Don't fail the resize operation if we can't write the event
|
|
}
|
|
}
|
|
|
|
log.Printf("[DEBUG] PTY.Resize: Successfully resized PTY to %dx%d", width, height)
|
|
return nil
|
|
}
|
|
|
|
func (p *PTY) Close() error {
|
|
if p.streamWriter != nil {
|
|
p.streamWriter.Close()
|
|
}
|
|
if p.pty != nil {
|
|
p.pty.Close()
|
|
}
|
|
if p.oldState != nil {
|
|
term.Restore(int(os.Stdin.Fd()), p.oldState)
|
|
}
|
|
return nil
|
|
} |