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
|
idx = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("WS[%s]: connected", sessionNum)
|
||||||
|
|
||||||
session := s.console.getSession(idx)
|
session := s.console.getSession(idx)
|
||||||
|
|
||||||
// Wait for PTY session to be available (it gets set when a command is executed)
|
// 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()
|
session.mu.Unlock()
|
||||||
|
|
||||||
if ptySess == nil {
|
if ptySess == nil {
|
||||||
// No active PTY — send message and wait
|
log.Printf("WS[%s]: no PTY yet, polling...", sessionNum)
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Nema aktivne sesije. Pokrenite komandu.]\033[0m\r\n"))
|
conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Čekam pokretanje sesije...]\033[0m\r\n"))
|
||||||
|
|
||||||
// Poll for session to start
|
// Poll for session to start (up to 30s)
|
||||||
ticker := time.NewTicker(500 * time.Millisecond)
|
ticker := time.NewTicker(300 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
timeout := time.After(30 * time.Second)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
@ -65,25 +68,42 @@ func (s *Server) handleConsoleWS(c *gin.Context) {
|
|||||||
ptySess = session.ptySess
|
ptySess = session.ptySess
|
||||||
session.mu.Unlock()
|
session.mu.Unlock()
|
||||||
if ptySess != nil {
|
if ptySess != nil {
|
||||||
|
log.Printf("WS[%s]: PTY found after polling", sessionNum)
|
||||||
goto connected
|
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():
|
case <-c.Request.Context().Done():
|
||||||
|
log.Printf("WS[%s]: client disconnected while polling", sessionNum)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connected:
|
connected:
|
||||||
|
log.Printf("WS[%s]: subscribing to PTY", sessionNum)
|
||||||
subID := fmt.Sprintf("ws-%d", time.Now().UnixNano())
|
subID := fmt.Sprintf("ws-%d", time.Now().UnixNano())
|
||||||
outputCh := ptySess.Subscribe(subID)
|
outputCh := ptySess.Subscribe(subID)
|
||||||
defer ptySess.Unsubscribe(subID)
|
defer ptySess.Unsubscribe(subID)
|
||||||
|
|
||||||
// Send buffered output for reconnect
|
// Send buffered output for reconnect
|
||||||
buffered := ptySess.GetBuffer()
|
buffered := ptySess.GetBuffer()
|
||||||
|
log.Printf("WS[%s]: sending buffer (%d bytes)", sessionNum, len(buffered))
|
||||||
if len(buffered) > 0 {
|
if len(buffered) > 0 {
|
||||||
conn.WriteMessage(websocket.BinaryMessage, buffered)
|
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
|
// Serialized write channel
|
||||||
writeCh := make(chan []byte, 256)
|
writeCh := make(chan []byte, 256)
|
||||||
writeDone := make(chan struct{})
|
writeDone := make(chan struct{})
|
||||||
@ -109,11 +129,11 @@ connected:
|
|||||||
// Watch for process exit
|
// Watch for process exit
|
||||||
go func() {
|
go func() {
|
||||||
<-ptySess.Done()
|
<-ptySess.Done()
|
||||||
|
log.Printf("WS[%s]: PTY process exited", sessionNum)
|
||||||
select {
|
select {
|
||||||
case writeCh <- []byte("\r\n\033[33m[Sesija završena]\033[0m\r\n"):
|
case writeCh <- []byte("\r\n\033[33m[Sesija završena]\033[0m\r\n"):
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
// Give browser time to receive the message, then close
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
conn.WriteMessage(websocket.CloseMessage,
|
conn.WriteMessage(websocket.CloseMessage,
|
||||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "done"))
|
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "done"))
|
||||||
@ -124,7 +144,7 @@ connected:
|
|||||||
_, msg, err := conn.ReadMessage()
|
_, msg, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
|
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
|
break
|
||||||
}
|
}
|
||||||
@ -133,18 +153,19 @@ connected:
|
|||||||
var resize wsResizeMsg
|
var resize wsResizeMsg
|
||||||
if json.Unmarshal(msg, &resize) == nil && resize.Type == "resize" && resize.Cols > 0 && resize.Rows > 0 {
|
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 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular keyboard input → PTY
|
// Regular keyboard input → PTY
|
||||||
if _, err := ptySess.WriteInput(msg); err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close(writeCh)
|
close(writeCh)
|
||||||
<-writeDone
|
<-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>
|
<button class="btn btn-kill" id="kill-1" onclick="killSession(1)" style="display:none">Prekini</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="console-terminal" id="terminal-1"></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>
|
||||||
|
|
||||||
<div class="console-panel" id="panel-2" style="display:none">
|
<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>
|
<button class="btn btn-kill" id="kill-2" onclick="killSession(2)" style="display:none">Prekini</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="console-terminal" id="terminal-2"></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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -94,8 +86,6 @@ function getTermTheme() {
|
|||||||
|
|
||||||
// ── Session state ────────────────────────────────────
|
// ── Session state ────────────────────────────────────
|
||||||
var sessions = [{}, {}];
|
var sessions = [{}, {}];
|
||||||
var historyIdx = [0, 0];
|
|
||||||
var cmdHistory = [[], []];
|
|
||||||
|
|
||||||
function initTerminal(idx) {
|
function initTerminal(idx) {
|
||||||
var num = idx + 1;
|
var num = idx + 1;
|
||||||
@ -142,10 +132,7 @@ function initTerminal(idx) {
|
|||||||
sessions[idx].fitAddon = fitAddon;
|
sessions[idx].fitAddon = fitAddon;
|
||||||
sessions[idx].ws = null;
|
sessions[idx].ws = null;
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(function() { fitAddon.fit(); }, 50);
|
||||||
fitAddon.fit();
|
|
||||||
term.write('\x1b[33mSesija ' + num + ' — upiši komandu dole i pritisni Enter\x1b[0m\r\n');
|
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WebSocket connection ─────────────────────────────
|
// ── WebSocket connection ─────────────────────────────
|
||||||
@ -158,6 +145,10 @@ function connectWS(idx) {
|
|||||||
sess.ws = null;
|
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 proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
var url = proto + '//' + location.host + '/console/ws/' + num;
|
var url = proto + '//' + location.host + '/console/ws/' + num;
|
||||||
var ws = new WebSocket(url);
|
var ws = new WebSocket(url);
|
||||||
@ -165,7 +156,6 @@ function connectWS(idx) {
|
|||||||
|
|
||||||
ws.onopen = function() {
|
ws.onopen = function() {
|
||||||
setSessionUI(num, 'running');
|
setSessionUI(num, 'running');
|
||||||
// Send initial size
|
|
||||||
var term = sess.term;
|
var term = sess.term;
|
||||||
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
||||||
term.focus();
|
term.focus();
|
||||||
@ -192,67 +182,7 @@ function connectWS(idx) {
|
|||||||
sess.ws = ws;
|
sess.ws = ws;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Command handling ─────────────────────────────────
|
// ── UI helpers ───────────────────────────────────────
|
||||||
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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function killSession(session) {
|
function killSession(session) {
|
||||||
fetch('/console/kill/' + session, {method: 'POST'})
|
fetch('/console/kill/' + session, {method: 'POST'})
|
||||||
.then(function() {
|
.then(function() {
|
||||||
@ -278,7 +208,6 @@ function togglePanel2() {
|
|||||||
if (panel.style.display === 'none') {
|
if (panel.style.display === 'none') {
|
||||||
panel.style.display = 'flex';
|
panel.style.display = 'flex';
|
||||||
btn.textContent = '- Sesija 2';
|
btn.textContent = '- Sesija 2';
|
||||||
// Initialize terminal 2 if not yet done
|
|
||||||
if (!sessions[1].term) {
|
if (!sessions[1].term) {
|
||||||
initTerminal(1);
|
initTerminal(1);
|
||||||
} else {
|
} else {
|
||||||
@ -294,7 +223,6 @@ function togglePanel2() {
|
|||||||
var origSetTheme = window.setTheme;
|
var origSetTheme = window.setTheme;
|
||||||
window.setTheme = function(mode) {
|
window.setTheme = function(mode) {
|
||||||
if (origSetTheme) origSetTheme(mode);
|
if (origSetTheme) origSetTheme(mode);
|
||||||
// Update terminal themes after a tick
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
var theme = getTermTheme();
|
var theme = getTermTheme();
|
||||||
for (var i = 0; i < 2; i++) {
|
for (var i = 0; i < 2; i++) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user