forked from wrenn/wrenn
- Add multi-session Terminal tab with xterm.js (session tabs, close, reconnect) - Keep terminal mounted across tab switches to preserve sessions - Persist active tab in URL (?tab=terminal) so refresh stays on terminal - Buffer keystrokes (50ms) to reduce per-character RPC overhead - Add WebSocket auth via ?token= query param for browser WS connections - Enable ws:true in Vite dev proxy for WebSocket support envd fixes (pre-existing bugs exposed by multi-session terminals): - Fix getProcess tag Range: inverted return values caused early stop when multiple tagged processes existed, making SendInput fail with "not found" - Fix multiplexer deadlock: blocking send to cancelled fork's unbuffered channel prevented process cleanup. Now uses buffered channels (cap 64) with non-blocking fallback
80 lines
1.2 KiB
Go
80 lines
1.2 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package handler
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
type MultiplexedChannel[T any] struct {
|
|
Source chan T
|
|
channels []chan T
|
|
mu sync.RWMutex
|
|
exited atomic.Bool
|
|
}
|
|
|
|
func NewMultiplexedChannel[T any](buffer int) *MultiplexedChannel[T] {
|
|
c := &MultiplexedChannel[T]{
|
|
channels: nil,
|
|
Source: make(chan T, buffer),
|
|
}
|
|
|
|
go func() {
|
|
for v := range c.Source {
|
|
c.mu.RLock()
|
|
|
|
for _, cons := range c.channels {
|
|
select {
|
|
case cons <- v:
|
|
default:
|
|
// Consumer not reading — skip to prevent deadlock
|
|
}
|
|
}
|
|
|
|
c.mu.RUnlock()
|
|
}
|
|
|
|
c.exited.Store(true)
|
|
|
|
for _, cons := range c.channels {
|
|
close(cons)
|
|
}
|
|
}()
|
|
|
|
return c
|
|
}
|
|
|
|
func (m *MultiplexedChannel[T]) Fork() (chan T, func()) {
|
|
if m.exited.Load() {
|
|
ch := make(chan T)
|
|
close(ch)
|
|
|
|
return ch, func() {}
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
consumer := make(chan T, 64)
|
|
|
|
m.channels = append(m.channels, consumer)
|
|
|
|
return consumer, func() {
|
|
m.remove(consumer)
|
|
}
|
|
}
|
|
|
|
func (m *MultiplexedChannel[T]) remove(consumer chan T) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
for i, ch := range m.channels {
|
|
if ch == consumer {
|
|
m.channels = append(m.channels[:i], m.channels[i+1:]...)
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|