mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-09 09:16:20 +00:00
fix(terminal): handle completed scripts without websocket error
This commit is contained in:
@@ -50,12 +50,14 @@ interface ScriptTerminalModalProps {
|
|||||||
description: string
|
description: string
|
||||||
scriptName?: string
|
scriptName?: string
|
||||||
params?: Record<string, string>
|
params?: Record<string, string>
|
||||||
|
completedSuccessfullyMessage?: string
|
||||||
|
completedWithErrorMessage?: (exitCode: number) => string
|
||||||
// Optional callback fired when the script's WebSocket closes
|
// Optional callback fired when the script's WebSocket closes
|
||||||
// (script_runner sends an exit code and then closes). Lets the
|
// (script_runner sends an exit code and then closes). Lets the
|
||||||
// parent auto-dismiss the modal — used by host-backup's Restore
|
// parent auto-dismiss the modal — used by host-backup's Restore
|
||||||
// flow so "Press Enter to close" in the bash script actually
|
// flow so "Press Enter to close" in the bash script actually
|
||||||
// closes the modal without an extra click. Other callers ignore.
|
// closes the modal without an extra click. Other callers ignore.
|
||||||
onComplete?: () => void
|
onComplete?: (exitCode?: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScriptTerminalModal({
|
export function ScriptTerminalModal({
|
||||||
@@ -65,6 +67,8 @@ export function ScriptTerminalModal({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
params = { EXECUTION_MODE: "web" },
|
params = { EXECUTION_MODE: "web" },
|
||||||
|
completedSuccessfullyMessage,
|
||||||
|
completedWithErrorMessage,
|
||||||
onComplete,
|
onComplete,
|
||||||
}: ScriptTerminalModalProps) {
|
}: ScriptTerminalModalProps) {
|
||||||
const t = useT()
|
const t = useT()
|
||||||
@@ -85,6 +89,7 @@ export function ScriptTerminalModal({
|
|||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
const reconnectAttemptsRef = useRef(0)
|
const reconnectAttemptsRef = useRef(0)
|
||||||
const keepAliveIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
const keepAliveIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const completionReceivedRef = useRef(false)
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [isTablet, setIsTablet] = useState(false)
|
const [isTablet, setIsTablet] = useState(false)
|
||||||
|
|
||||||
@@ -96,6 +101,14 @@ export function ScriptTerminalModal({
|
|||||||
const resizeBarRef = useRef<HTMLDivElement>(null)
|
const resizeBarRef = useRef<HTMLDivElement>(null)
|
||||||
const modalHeightRef = useRef(600)
|
const modalHeightRef = useRef(600)
|
||||||
|
|
||||||
|
const getCompletionMessage = useCallback(
|
||||||
|
(exitCode: number) =>
|
||||||
|
exitCode === 0
|
||||||
|
? (completedSuccessfullyMessage ?? t("scriptTerminal.completedSuccessfully"))
|
||||||
|
: (completedWithErrorMessage?.(exitCode) ?? t("scriptTerminal.completedWithError", { code: exitCode })),
|
||||||
|
[completedSuccessfullyMessage, completedWithErrorMessage, t],
|
||||||
|
)
|
||||||
|
|
||||||
const terminalContainerRef = useRef<HTMLDivElement>(null)
|
const terminalContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const paramsRef = useRef(params)
|
const paramsRef = useRef(params)
|
||||||
|
|
||||||
@@ -106,7 +119,7 @@ export function ScriptTerminalModal({
|
|||||||
|
|
||||||
// Same trick for onComplete — we want the latest callback inside
|
// Same trick for onComplete — we want the latest callback inside
|
||||||
// the ws.onclose handler without re-running the connection effect.
|
// the ws.onclose handler without re-running the connection effect.
|
||||||
const onCompleteRef = useRef<(() => void) | undefined>(undefined)
|
const onCompleteRef = useRef<((exitCode?: number) => void) | undefined>(undefined)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onCompleteRef.current = onComplete
|
onCompleteRef.current = onComplete
|
||||||
}, [onComplete])
|
}, [onComplete])
|
||||||
@@ -168,6 +181,23 @@ const initMessage = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The PTY worker always emits this final line. Treat it as a
|
||||||
|
// completion fallback because some WebSocket servers tear down the
|
||||||
|
// connection before the following structured message is flushed.
|
||||||
|
const exitMatch = typeof event.data === "string"
|
||||||
|
? event.data.match(/\[Script exited with code (-?\d+)\]/)
|
||||||
|
: null
|
||||||
|
if (exitMatch) {
|
||||||
|
const exitCode = Number(exitMatch[1])
|
||||||
|
termRef.current?.write(event.data)
|
||||||
|
completionReceivedRef.current = true
|
||||||
|
setIsComplete(true)
|
||||||
|
termRef.current?.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
|
||||||
|
onCompleteRef.current?.(exitCode)
|
||||||
|
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data)
|
const msg = JSON.parse(event.data)
|
||||||
if (msg.type === "web_interaction" && msg.interaction) {
|
if (msg.type === "web_interaction" && msg.interaction) {
|
||||||
@@ -189,6 +219,15 @@ const initMessage = {
|
|||||||
termRef.current?.writeln(`\x1b[31m${msg.message}\x1b[0m`)
|
termRef.current?.writeln(`\x1b[31m${msg.message}\x1b[0m`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (msg.type === "script_complete") {
|
||||||
|
const exitCode = Number(msg.exit_code ?? 1)
|
||||||
|
completionReceivedRef.current = true
|
||||||
|
setIsComplete(true)
|
||||||
|
termRef.current?.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
|
||||||
|
onCompleteRef.current?.(exitCode)
|
||||||
|
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
termRef.current?.write(event.data)
|
termRef.current?.write(event.data)
|
||||||
setIsWaitingNextInteraction(false)
|
setIsWaitingNextInteraction(false)
|
||||||
@@ -199,6 +238,9 @@ const initMessage = {
|
|||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
setConnectionStatus("offline")
|
setConnectionStatus("offline")
|
||||||
|
if (!completionReceivedRef.current) {
|
||||||
|
termRef.current?.writeln(`\x1b[31m${t("scriptTerminal.websocketError")}\x1b[0m`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
ws.onclose = (event) => {
|
||||||
@@ -207,16 +249,19 @@ const initMessage = {
|
|||||||
clearInterval(keepAliveIntervalRef.current)
|
clearInterval(keepAliveIntervalRef.current)
|
||||||
keepAliveIntervalRef.current = null
|
keepAliveIntervalRef.current = null
|
||||||
}
|
}
|
||||||
|
if (completionReceivedRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!isComplete && reconnectAttemptsRef.current < 3) {
|
if (!isComplete && reconnectAttemptsRef.current < 3) {
|
||||||
reconnectTimeoutRef.current = setTimeout(attemptReconnect, 2000)
|
reconnectTimeoutRef.current = setTimeout(attemptReconnect, 2000)
|
||||||
} else {
|
} else {
|
||||||
setIsComplete(true)
|
setIsComplete(true)
|
||||||
onCompleteRef.current?.()
|
onCompleteRef.current?.(-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}, [isOpen, isComplete, scriptPath])
|
}, [isOpen, isComplete, scriptPath, getCompletionMessage, t])
|
||||||
|
|
||||||
const sendKey = useCallback((key: string) => {
|
const sendKey = useCallback((key: string) => {
|
||||||
if (!termRef.current) return
|
if (!termRef.current) return
|
||||||
@@ -353,6 +398,23 @@ const initMessage = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See the reconnect handler above. The exit line is guaranteed to be
|
||||||
|
// sent with the PTY output and is therefore a robust fallback when a
|
||||||
|
// final JSON frame is lost during server-side socket teardown.
|
||||||
|
const exitMatch = typeof event.data === "string"
|
||||||
|
? event.data.match(/\[Script exited with code (-?\d+)\]/)
|
||||||
|
: null
|
||||||
|
if (exitMatch) {
|
||||||
|
const exitCode = Number(exitMatch[1])
|
||||||
|
term.write(event.data)
|
||||||
|
completionReceivedRef.current = true
|
||||||
|
setIsComplete(true)
|
||||||
|
term.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
|
||||||
|
onCompleteRef.current?.(exitCode)
|
||||||
|
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data)
|
const msg = JSON.parse(event.data)
|
||||||
|
|
||||||
@@ -376,6 +438,15 @@ const initMessage = {
|
|||||||
term.writeln(`\x1b[31m${msg.message}\x1b[0m`)
|
term.writeln(`\x1b[31m${msg.message}\x1b[0m`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (msg.type === "script_complete") {
|
||||||
|
const exitCode = Number(msg.exit_code ?? 1)
|
||||||
|
completionReceivedRef.current = true
|
||||||
|
setIsComplete(true)
|
||||||
|
term.writeln(`\x1b[${exitCode === 0 ? "32" : "31"}m${getCompletionMessage(exitCode)}\x1b[0m`)
|
||||||
|
onCompleteRef.current?.(exitCode)
|
||||||
|
if (ws.readyState === WebSocket.OPEN) ws.close(1000, "script complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Not JSON, es output normal de terminal
|
// Not JSON, es output normal de terminal
|
||||||
}
|
}
|
||||||
@@ -390,21 +461,25 @@ const initMessage = {
|
|||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
setConnectionStatus("offline")
|
setConnectionStatus("offline")
|
||||||
|
if (!completionReceivedRef.current) {
|
||||||
term.writeln(`\x1b[31m${t("scriptTerminal.websocketError")}\x1b[0m`)
|
term.writeln(`\x1b[31m${t("scriptTerminal.websocketError")}\x1b[0m`)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
ws.onclose = (event) => {
|
||||||
setConnectionStatus("offline")
|
setConnectionStatus("offline")
|
||||||
|
if (!completionReceivedRef.current) {
|
||||||
term.writeln(`\x1b[33m${t("scriptTerminal.connectionClosed")}\x1b[0m`)
|
term.writeln(`\x1b[33m${t("scriptTerminal.connectionClosed")}\x1b[0m`)
|
||||||
|
}
|
||||||
|
|
||||||
if (keepAliveIntervalRef.current) {
|
if (keepAliveIntervalRef.current) {
|
||||||
clearInterval(keepAliveIntervalRef.current)
|
clearInterval(keepAliveIntervalRef.current)
|
||||||
keepAliveIntervalRef.current = null
|
keepAliveIntervalRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isComplete) {
|
if (!completionReceivedRef.current && !isComplete) {
|
||||||
setIsComplete(true)
|
setIsComplete(true)
|
||||||
onCompleteRef.current?.()
|
onCompleteRef.current?.(-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,6 +566,7 @@ const initMessage = {
|
|||||||
|
|
||||||
sessionIdRef.current = Math.random().toString(36).substring(2, 8)
|
sessionIdRef.current = Math.random().toString(36).substring(2, 8)
|
||||||
reconnectAttemptsRef.current = 0
|
reconnectAttemptsRef.current = 0
|
||||||
|
completionReceivedRef.current = false
|
||||||
setIsComplete(false)
|
setIsComplete(false)
|
||||||
setInteractionInput("")
|
setInteractionInput("")
|
||||||
setCurrentInteraction(null)
|
setCurrentInteraction(null)
|
||||||
|
|||||||
@@ -1464,7 +1464,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
setApplyStartedAt(Date.now())
|
setApplyStartedAt(Date.now())
|
||||||
setApplyOpen(true)
|
setApplyOpen(true)
|
||||||
}
|
}
|
||||||
const handleApplyComplete = async () => {
|
const handleApplyComplete = async (exitCode = 0) => {
|
||||||
if (applyVmid == null) return
|
if (applyVmid == null) return
|
||||||
const duration = Math.max(0, Math.round((Date.now() - applyStartedAt) / 1000))
|
const duration = Math.max(0, Math.round((Date.now() - applyStartedAt) / 1000))
|
||||||
// The modal fires onComplete on any WS close (success or user cancel);
|
// The modal fires onComplete on any WS close (success or user cancel);
|
||||||
@@ -1477,7 +1477,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
success: true,
|
success: exitCode === 0,
|
||||||
target: applyTarget,
|
target: applyTarget,
|
||||||
duration_seconds: duration,
|
duration_seconds: duration,
|
||||||
ct_name: selectedVM?.name || `CT-${applyVmid}`,
|
ct_name: selectedVM?.name || `CT-${applyVmid}`,
|
||||||
@@ -4967,13 +4967,17 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
onComplete={handleApplyComplete}
|
onComplete={handleApplyComplete}
|
||||||
scriptPath="/usr/local/share/proxmenux/scripts/lxc/apply_updates.sh"
|
scriptPath="/usr/local/share/proxmenux/scripts/lxc/apply_updates.sh"
|
||||||
scriptName="lxc_apply_updates"
|
scriptName="lxc_apply_updates"
|
||||||
title={`Apply updates — CT ${applyVmid}`}
|
title={t("vmLxc.updates.terminalTitle", { vmid: applyVmid })}
|
||||||
|
completedSuccessfullyMessage={t("vmLxc.updates.terminalCompletedSuccessfully")}
|
||||||
|
completedWithErrorMessage={(exitCode) =>
|
||||||
|
t("vmLxc.updates.terminalCompletedWithError", { code: exitCode })
|
||||||
|
}
|
||||||
description={
|
description={
|
||||||
applyTarget === "os"
|
applyTarget === "os"
|
||||||
? "Applying OS package updates inside the container..."
|
? t("vmLxc.updates.terminalDescriptionOs")
|
||||||
: applyTarget === "app"
|
: applyTarget === "app"
|
||||||
? "Running the application updater inside the container..."
|
? t("vmLxc.updates.terminalDescriptionApp")
|
||||||
: "Applying OS package + application updates inside the container..."
|
: t("vmLxc.updates.terminalDescriptionBoth")
|
||||||
}
|
}
|
||||||
params={{
|
params={{
|
||||||
VMID: String(applyVmid),
|
VMID: String(applyVmid),
|
||||||
|
|||||||
@@ -1201,6 +1201,12 @@
|
|||||||
"applyUpdate": "Apply update",
|
"applyUpdate": "Apply update",
|
||||||
"upToDate": "Up to date",
|
"upToDate": "Up to date",
|
||||||
"runUpdater": "Run updater",
|
"runUpdater": "Run updater",
|
||||||
|
"terminalTitle": "Apply updates — CT {vmid}",
|
||||||
|
"terminalDescriptionOs": "Applying OS package updates inside the container...",
|
||||||
|
"terminalDescriptionApp": "Running the application updater inside the container...",
|
||||||
|
"terminalDescriptionBoth": "Applying OS package and application updates inside the container...",
|
||||||
|
"terminalCompletedSuccessfully": "Updates completed successfully",
|
||||||
|
"terminalCompletedWithError": "Updates finished with error code {code}",
|
||||||
"applyOsPlusApp": "Apply OS + {appName} updates",
|
"applyOsPlusApp": "Apply OS + {appName} updates",
|
||||||
"applyOsPlusApps": "Apply OS + Apps updates",
|
"applyOsPlusApps": "Apply OS + Apps updates",
|
||||||
"osPlusApp": "OS + {appName} updates",
|
"osPlusApp": "OS + {appName} updates",
|
||||||
@@ -4649,6 +4655,8 @@
|
|||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"websocketError": "WebSocket error occurred",
|
"websocketError": "WebSocket error occurred",
|
||||||
"connectionClosed": "Connection closed",
|
"connectionClosed": "Connection closed",
|
||||||
|
"completedSuccessfully": "Script completed successfully",
|
||||||
|
"completedWithError": "Script finished with error code {code}",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"yourInput": "Your input:",
|
"yourInput": "Your input:",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
|
|||||||
@@ -1201,6 +1201,12 @@
|
|||||||
"applyUpdate": "Aktualizovať",
|
"applyUpdate": "Aktualizovať",
|
||||||
"upToDate": "Aktuálne",
|
"upToDate": "Aktuálne",
|
||||||
"runUpdater": "Spustiť aktualizáciu",
|
"runUpdater": "Spustiť aktualizáciu",
|
||||||
|
"terminalTitle": "Aktualizácia — CT {vmid}",
|
||||||
|
"terminalDescriptionOs": "Aktualizujem balíky systému v kontajneri...",
|
||||||
|
"terminalDescriptionApp": "Spúšťam aktualizáciu aplikácie v kontajneri...",
|
||||||
|
"terminalDescriptionBoth": "Aktualizujem systém aj aplikácie v kontajneri...",
|
||||||
|
"terminalCompletedSuccessfully": "Aktualizácia bola úspešne dokončená",
|
||||||
|
"terminalCompletedWithError": "Aktualizácia skončila s chybovým kódom {code}",
|
||||||
"applyOsPlusApp": "Aktualizovať systém + {appName}",
|
"applyOsPlusApp": "Aktualizovať systém + {appName}",
|
||||||
"applyOsPlusApps": "Aktualizovať systém + aplikácie",
|
"applyOsPlusApps": "Aktualizovať systém + aplikácie",
|
||||||
"osPlusApp": "Aktualizácie systému + {appName}",
|
"osPlusApp": "Aktualizácie systému + {appName}",
|
||||||
@@ -4155,6 +4161,8 @@
|
|||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"websocketError": "Chyba WebSocket spojenia",
|
"websocketError": "Chyba WebSocket spojenia",
|
||||||
"connectionClosed": "Spojenie bolo zatvorené",
|
"connectionClosed": "Spojenie bolo zatvorené",
|
||||||
|
"completedSuccessfully": "Skript sa úspešne dokončil",
|
||||||
|
"completedWithError": "Skript skončil s chybovým kódom {code}",
|
||||||
"yes": "Áno",
|
"yes": "Áno",
|
||||||
"yourInput": "Tvoj vstup:",
|
"yourInput": "Tvoj vstup:",
|
||||||
"submit": "Odoslať",
|
"submit": "Odoslať",
|
||||||
|
|||||||
@@ -479,6 +479,15 @@ def script_websocket(ws, session_id):
|
|||||||
env=env
|
env=env
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# The child inherited the slave side of the PTY. Keeping the parent's
|
||||||
|
# duplicate open can prevent the reader from seeing EOF after the script
|
||||||
|
# exits, which in turn hides the final script_complete message.
|
||||||
|
try:
|
||||||
|
os.close(slave_fd)
|
||||||
|
slave_fd = None
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Set non-blocking mode for master_fd
|
# Set non-blocking mode for master_fd
|
||||||
flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
|
flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
|
||||||
fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||||
@@ -573,6 +582,13 @@ def script_websocket(ws, session_id):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
ws.send(f'\r\n[Script exited with code {exit_code}]\r\n')
|
ws.send(f'\r\n[Script exited with code {exit_code}]\r\n')
|
||||||
|
# Send an explicit terminal result before the connection is
|
||||||
|
# closed. The browser previously saw the worker disappear as a
|
||||||
|
# generic WebSocket failure even when the script exited with 0.
|
||||||
|
ws.send(json.dumps({
|
||||||
|
'type': 'script_complete',
|
||||||
|
'exit_code': exit_code,
|
||||||
|
}))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -581,10 +597,16 @@ def script_websocket(ws, session_id):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
data = ws.receive(timeout=None)
|
data = ws.receive(timeout=0.25)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
|
if script_process.poll() is not None:
|
||||||
|
# The output worker owns the final PTY drain and emits
|
||||||
|
# both `[Script exited with code N]` and
|
||||||
|
# `script_complete`. Wait briefly for it before cleanup.
|
||||||
|
output_thread.join(timeout=2.0)
|
||||||
break
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = json.loads(data)
|
msg = json.loads(data)
|
||||||
@@ -625,6 +647,14 @@ def script_websocket(ws, session_id):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if script_process.poll() is not None:
|
if script_process.poll() is not None:
|
||||||
|
# The output worker owns the final PTY drain and emits both
|
||||||
|
# `[Script exited with code N]` and `script_complete`. A
|
||||||
|
# resize/ping arriving just after process exit used to make
|
||||||
|
# this receive loop enter cleanup immediately, closing the
|
||||||
|
# socket before those final frames were sent. Wait briefly
|
||||||
|
# for the worker so a normal script exit is delivered before
|
||||||
|
# teardown.
|
||||||
|
output_thread.join(timeout=2.0)
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -644,6 +674,7 @@ def script_websocket(ws, session_id):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if slave_fd is not None:
|
||||||
try:
|
try:
|
||||||
os.close(slave_fd)
|
os.close(slave_fd)
|
||||||
except:
|
except:
|
||||||
|
|||||||
Reference in New Issue
Block a user