Konzola: sklonjen command input, detaljno WS logovanje
- Uklonjena input polja iz konzole — rad samo kroz Pusti dugme - Detaljno logovanje WS: connect, poll, subscribe, buffer, disconnect - WS timeout 30s ako nema PTY sesije - Provera da li je PTY already done pre nego subscribe Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fa8aa59b29
commit
d27eb900b1
@ -44,6 +44,8 @@ func (s *Server) handleConsoleWS(c *gin.Context) {
|
||||
idx = 1
|
||||
}
|
||||
|
||||
log.Printf("WS[%s]: connected", sessionNum)
|
||||
|
||||
session := s.console.getSession(idx)
|
||||
|
||||
// Wait for PTY session to be available (it gets set when a command is executed)
|
||||
@ -52,12 +54,13 @@ func (s *Server) handleConsoleWS(c *gin.Context) {
|
||||
session.mu.Unlock()
|
||||
|
||||
if ptySess == nil {
|
||||
// No active PTY — send message and wait
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Nema aktivne sesije. Pokrenite komandu.]\033[0m\r\n"))
|
||||
log.Printf("WS[%s]: no PTY yet, polling...", sessionNum)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Čekam pokretanje sesije...]\033[0m\r\n"))
|
||||
|
||||
// Poll for session to start
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
// Poll for session to start (up to 30s)
|
||||
ticker := time.NewTicker(300 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
timeout := time.After(30 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
@ -65,25 +68,42 @@ func (s *Server) handleConsoleWS(c *gin.Context) {
|
||||
ptySess = session.ptySess
|
||||
session.mu.Unlock()
|
||||
if ptySess != nil {
|
||||
log.Printf("WS[%s]: PTY found after polling", sessionNum)
|
||||
goto connected
|
||||
}
|
||||
case <-timeout:
|
||||
log.Printf("WS[%s]: timeout waiting for PTY", sessionNum)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[31m[Timeout — sesija nije pokrenuta]\033[0m\r\n"))
|
||||
return
|
||||
case <-c.Request.Context().Done():
|
||||
log.Printf("WS[%s]: client disconnected while polling", sessionNum)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connected:
|
||||
log.Printf("WS[%s]: subscribing to PTY", sessionNum)
|
||||
subID := fmt.Sprintf("ws-%d", time.Now().UnixNano())
|
||||
outputCh := ptySess.Subscribe(subID)
|
||||
defer ptySess.Unsubscribe(subID)
|
||||
|
||||
// Send buffered output for reconnect
|
||||
buffered := ptySess.GetBuffer()
|
||||
log.Printf("WS[%s]: sending buffer (%d bytes)", sessionNum, len(buffered))
|
||||
if len(buffered) > 0 {
|
||||
conn.WriteMessage(websocket.BinaryMessage, buffered)
|
||||
}
|
||||
|
||||
// Check if already done
|
||||
select {
|
||||
case <-ptySess.Done():
|
||||
log.Printf("WS[%s]: PTY already done, sending buffer only", sessionNum)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Sesija završena]\033[0m\r\n"))
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Serialized write channel
|
||||
writeCh := make(chan []byte, 256)
|
||||
writeDone := make(chan struct{})
|
||||
@ -109,11 +129,11 @@ connected:
|
||||
// Watch for process exit
|
||||
go func() {
|
||||
<-ptySess.Done()
|
||||
log.Printf("WS[%s]: PTY process exited", sessionNum)
|
||||
select {
|
||||
case writeCh <- []byte("\r\n\033[33m[Sesija završena]\033[0m\r\n"):
|
||||
default:
|
||||
}
|
||||
// Give browser time to receive the message, then close
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
conn.WriteMessage(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "done"))
|
||||
@ -124,7 +144,7 @@ connected:
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
|
||||
log.Printf("WebSocket read error: %v", err)
|
||||
log.Printf("WS[%s]: read error: %v", sessionNum, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -133,18 +153,19 @@ connected:
|
||||
var resize wsResizeMsg
|
||||
if json.Unmarshal(msg, &resize) == nil && resize.Type == "resize" && resize.Cols > 0 && resize.Rows > 0 {
|
||||
if err := ptySess.Resize(resize.Rows, resize.Cols); err != nil {
|
||||
log.Printf("PTY resize error: %v", err)
|
||||
log.Printf("WS[%s]: resize error: %v", sessionNum, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Regular keyboard input → PTY
|
||||
if _, err := ptySess.WriteInput(msg); err != nil {
|
||||
log.Printf("PTY write error: %v", err)
|
||||
log.Printf("WS[%s]: PTY write error: %v", sessionNum, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
close(writeCh)
|
||||
<-writeDone
|
||||
log.Printf("WS[%s]: disconnected", sessionNum)
|
||||
}
|
||||
|
||||
@ -42,10 +42,6 @@
|
||||
<button class="btn btn-kill" id="kill-1" onclick="killSession(1)" style="display:none">Prekini</button>
|
||||
</div>
|
||||
<div class="console-terminal" id="terminal-1"></div>
|
||||
<div class="console-input-row">
|
||||
<input type="text" id="input-1" class="console-input" placeholder="Komanda za claude..." onkeydown="handleKey(event, 1)" autocomplete="off">
|
||||
<button class="btn btn-move" onclick="sendCommand(1)">⏎</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="console-panel" id="panel-2" style="display:none">
|
||||
@ -55,10 +51,6 @@
|
||||
<button class="btn btn-kill" id="kill-2" onclick="killSession(2)" style="display:none">Prekini</button>
|
||||
</div>
|
||||
<div class="console-terminal" id="terminal-2"></div>
|
||||
<div class="console-input-row">
|
||||
<input type="text" id="input-2" class="console-input" placeholder="Komanda za claude..." onkeydown="handleKey(event, 2)" autocomplete="off">
|
||||
<button class="btn btn-move" onclick="sendCommand(2)">⏎</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,8 +86,6 @@ function getTermTheme() {
|
||||
|
||||
// ── Session state ────────────────────────────────────
|
||||
var sessions = [{}, {}];
|
||||
var historyIdx = [0, 0];
|
||||
var cmdHistory = [[], []];
|
||||
|
||||
function initTerminal(idx) {
|
||||
var num = idx + 1;
|
||||
@ -142,10 +132,7 @@ function initTerminal(idx) {
|
||||
sessions[idx].fitAddon = fitAddon;
|
||||
sessions[idx].ws = null;
|
||||
|
||||
setTimeout(function() {
|
||||
fitAddon.fit();
|
||||
term.write('\x1b[33mSesija ' + num + ' — upiši komandu dole i pritisni Enter\x1b[0m\r\n');
|
||||
}, 50);
|
||||
setTimeout(function() { fitAddon.fit(); }, 50);
|
||||
}
|
||||
|
||||
// ── WebSocket connection ─────────────────────────────
|
||||
@ -158,6 +145,10 @@ function connectWS(idx) {
|
||||
sess.ws = null;
|
||||
}
|
||||
|
||||
// Clear terminal for new session
|
||||
sess.term.clear();
|
||||
sess.term.write('\x1b[33mPovezivanje na sesiju ' + num + '...\x1b[0m\r\n');
|
||||
|
||||
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
var url = proto + '//' + location.host + '/console/ws/' + num;
|
||||
var ws = new WebSocket(url);
|
||||
@ -165,7 +156,6 @@ function connectWS(idx) {
|
||||
|
||||
ws.onopen = function() {
|
||||
setSessionUI(num, 'running');
|
||||
// Send initial size
|
||||
var term = sess.term;
|
||||
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
||||
term.focus();
|
||||
@ -192,67 +182,7 @@ function connectWS(idx) {
|
||||
sess.ws = ws;
|
||||
}
|
||||
|
||||
// ── Command handling ─────────────────────────────────
|
||||
function handleKey(e, session) {
|
||||
if (e.key === 'Enter') {
|
||||
sendCommand(session);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
var idx = session - 1;
|
||||
if (historyIdx[idx] > 0) {
|
||||
historyIdx[idx]--;
|
||||
document.getElementById('input-' + session).value = cmdHistory[idx][historyIdx[idx]] || '';
|
||||
}
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
var idx = session - 1;
|
||||
if (historyIdx[idx] < cmdHistory[idx].length) {
|
||||
historyIdx[idx]++;
|
||||
document.getElementById('input-' + session).value = cmdHistory[idx][historyIdx[idx]] || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendCommand(session) {
|
||||
var input = document.getElementById('input-' + session);
|
||||
var cmd = input.value.trim();
|
||||
if (!cmd) return;
|
||||
|
||||
var idx = session - 1;
|
||||
cmdHistory[idx].push(cmd);
|
||||
historyIdx[idx] = cmdHistory[idx].length;
|
||||
input.value = '';
|
||||
|
||||
// Clear terminal for new command
|
||||
sessions[idx].term.clear();
|
||||
|
||||
setSessionUI(session, 'running');
|
||||
|
||||
fetch('/console/exec', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({cmd: cmd, session: session})
|
||||
})
|
||||
.then(function(resp) {
|
||||
if (!resp.ok) {
|
||||
return resp.json().then(function(data) {
|
||||
sessions[idx].term.write('\r\n\x1b[31m' + data.error + '\x1b[0m\r\n');
|
||||
setSessionUI(session, 'idle');
|
||||
throw new Error(data.error);
|
||||
});
|
||||
}
|
||||
return resp.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (!data) return;
|
||||
// Connect WebSocket to the PTY session
|
||||
connectWS(idx);
|
||||
})
|
||||
.catch(function(err) {
|
||||
setSessionUI(session, 'idle');
|
||||
});
|
||||
}
|
||||
|
||||
// ── UI helpers ───────────────────────────────────────
|
||||
function killSession(session) {
|
||||
fetch('/console/kill/' + session, {method: 'POST'})
|
||||
.then(function() {
|
||||
@ -278,7 +208,6 @@ function togglePanel2() {
|
||||
if (panel.style.display === 'none') {
|
||||
panel.style.display = 'flex';
|
||||
btn.textContent = '- Sesija 2';
|
||||
// Initialize terminal 2 if not yet done
|
||||
if (!sessions[1].term) {
|
||||
initTerminal(1);
|
||||
} else {
|
||||
@ -294,7 +223,6 @@ function togglePanel2() {
|
||||
var origSetTheme = window.setTheme;
|
||||
window.setTheme = function(mode) {
|
||||
if (origSetTheme) origSetTheme(mode);
|
||||
// Update terminal themes after a tick
|
||||
setTimeout(function() {
|
||||
var theme = getTermTheme();
|
||||
for (var i = 0; i < 2; i++) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user