diff --git a/code/internal/server/ws.go b/code/internal/server/ws.go index b56d5d8..299ba3d 100644 --- a/code/internal/server/ws.go +++ b/code/internal/server/ws.go @@ -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) } diff --git a/code/web/templates/console.html b/code/web/templates/console.html index 0b77342..c75cb9c 100644 --- a/code/web/templates/console.html +++ b/code/web/templates/console.html @@ -42,10 +42,6 @@
-
- - -
-
- - -
@@ -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++) {