mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-26 09:35:52 +00:00
153 lines
3.3 KiB
Go
153 lines
3.3 KiB
Go
package session
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
)
|
|
|
|
// StdinWatcher provides event-driven stdin handling like Node.js
|
|
type StdinWatcher struct {
|
|
stdinPath string
|
|
ptyFile *os.File
|
|
watcher *fsnotify.Watcher
|
|
stdinFile *os.File
|
|
buffer []byte
|
|
mu sync.Mutex
|
|
stopChan chan struct{}
|
|
stoppedChan chan struct{}
|
|
}
|
|
|
|
// NewStdinWatcher creates a new stdin watcher
|
|
func NewStdinWatcher(stdinPath string, ptyFile *os.File) (*StdinWatcher, error) {
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create fsnotify watcher: %w", err)
|
|
}
|
|
|
|
sw := &StdinWatcher{
|
|
stdinPath: stdinPath,
|
|
ptyFile: ptyFile,
|
|
watcher: watcher,
|
|
buffer: make([]byte, 4096),
|
|
stopChan: make(chan struct{}),
|
|
stoppedChan: make(chan struct{}),
|
|
}
|
|
|
|
// Open stdin pipe for reading
|
|
stdinFile, err := os.OpenFile(stdinPath, os.O_RDONLY|syscall.O_NONBLOCK, 0)
|
|
if err != nil {
|
|
watcher.Close()
|
|
return nil, fmt.Errorf("failed to open stdin pipe: %w", err)
|
|
}
|
|
sw.stdinFile = stdinFile
|
|
|
|
// Add stdin path to watcher
|
|
if err := watcher.Add(stdinPath); err != nil {
|
|
stdinFile.Close()
|
|
watcher.Close()
|
|
return nil, fmt.Errorf("failed to watch stdin pipe: %w", err)
|
|
}
|
|
|
|
return sw, nil
|
|
}
|
|
|
|
// Start begins watching for stdin input
|
|
func (sw *StdinWatcher) Start() {
|
|
go sw.watchLoop()
|
|
}
|
|
|
|
// Stop stops the watcher
|
|
func (sw *StdinWatcher) Stop() {
|
|
close(sw.stopChan)
|
|
<-sw.stoppedChan
|
|
sw.cleanup()
|
|
}
|
|
|
|
// watchLoop is the main event loop
|
|
func (sw *StdinWatcher) watchLoop() {
|
|
defer close(sw.stoppedChan)
|
|
|
|
for {
|
|
select {
|
|
case <-sw.stopChan:
|
|
debugLog("[DEBUG] StdinWatcher: Stopping watch loop")
|
|
return
|
|
|
|
case event, ok := <-sw.watcher.Events:
|
|
if !ok {
|
|
debugLog("[DEBUG] StdinWatcher: Watcher events channel closed")
|
|
return
|
|
}
|
|
|
|
// Handle write events (new data available)
|
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
|
sw.handleStdinData()
|
|
}
|
|
|
|
case err, ok := <-sw.watcher.Errors:
|
|
if !ok {
|
|
debugLog("[DEBUG] StdinWatcher: Watcher errors channel closed")
|
|
return
|
|
}
|
|
log.Printf("[ERROR] StdinWatcher: Watcher error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleStdinData reads available data and forwards it to the PTY
|
|
func (sw *StdinWatcher) handleStdinData() {
|
|
sw.mu.Lock()
|
|
defer sw.mu.Unlock()
|
|
|
|
for {
|
|
n, err := sw.stdinFile.Read(sw.buffer)
|
|
if n > 0 {
|
|
// Forward data to PTY immediately
|
|
if _, writeErr := sw.ptyFile.Write(sw.buffer[:n]); writeErr != nil {
|
|
log.Printf("[ERROR] StdinWatcher: Failed to write to PTY: %v", writeErr)
|
|
return
|
|
}
|
|
debugLog("[DEBUG] StdinWatcher: Forwarded %d bytes to PTY", n)
|
|
}
|
|
|
|
if err != nil {
|
|
if err == io.EOF || isEAGAIN(err) {
|
|
// No more data available right now
|
|
break
|
|
}
|
|
log.Printf("[ERROR] StdinWatcher: Failed to read from stdin: %v", err)
|
|
return
|
|
}
|
|
|
|
// If we read a full buffer, there might be more data
|
|
if n == len(sw.buffer) {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// cleanup releases resources
|
|
func (sw *StdinWatcher) cleanup() {
|
|
if sw.watcher != nil {
|
|
sw.watcher.Close()
|
|
}
|
|
if sw.stdinFile != nil {
|
|
sw.stdinFile.Close()
|
|
}
|
|
}
|
|
|
|
// isEAGAIN checks if the error is EAGAIN (resource temporarily unavailable)
|
|
func isEAGAIN(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
// Check for EAGAIN in the error string
|
|
return err.Error() == "resource temporarily unavailable"
|
|
}
|