mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-12-14 08:06:22 +00:00
Update AppImage
This commit is contained in:
@@ -1,68 +0,0 @@
|
|||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const requestUrl = new URL(request.url)
|
|
||||||
const API_PORT = "8008"
|
|
||||||
|
|
||||||
// Check if request comes through proxy (standard HTTP/HTTPS ports)
|
|
||||||
const isProxied = requestUrl.port === "" || requestUrl.port === "80" || requestUrl.port === "443"
|
|
||||||
|
|
||||||
let flaskUrl: string
|
|
||||||
|
|
||||||
if (isProxied) {
|
|
||||||
// Behind proxy - use same host but different path
|
|
||||||
flaskUrl = `${requestUrl.protocol}//${requestUrl.host}/api/scripts/execute`
|
|
||||||
} else {
|
|
||||||
// Direct access - use Flask port
|
|
||||||
flaskUrl = `${requestUrl.protocol}//${requestUrl.hostname}:${API_PORT}/api/scripts/execute`
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[v0] Starting NVIDIA driver installation")
|
|
||||||
console.log("[v0] Request URL:", requestUrl.href)
|
|
||||||
console.log("[v0] Flask URL:", flaskUrl)
|
|
||||||
|
|
||||||
const response = await fetch(flaskUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
script_relative_path: "gpu_tpu/nvidia_installer.sh",
|
|
||||||
params: {
|
|
||||||
EXECUTION_MODE: "web",
|
|
||||||
WEB_LOG: "/tmp/nvidia_web_install.log",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
console.error("[v0] Flask API error:", response.status, errorText)
|
|
||||||
throw new Error(`Flask API error: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!data.success) {
|
|
||||||
throw new Error(data.error || "Failed to start installation")
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[v0] NVIDIA installation started, session_id:", data.session_id)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
session_id: data.session_id,
|
|
||||||
message: "NVIDIA installation started",
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("[v0] NVIDIA installation error:", error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error.message || "Failed to start NVIDIA driver installation. Please try manually.",
|
|
||||||
},
|
|
||||||
{ status: 500 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
fetcher as swrFetcher,
|
fetcher as swrFetcher,
|
||||||
} from "../types/hardware"
|
} from "../types/hardware"
|
||||||
import { fetchApi } from "@/lib/api-config"
|
import { fetchApi } from "@/lib/api-config"
|
||||||
import { HybridScriptMonitor } from "./hybrid-script-monitor"
|
import { ScriptTerminalModal } from "./script-terminal-modal"
|
||||||
|
|
||||||
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
||||||
if (!sizeStr) return 0
|
if (!sizeStr) return 0
|
||||||
@@ -239,7 +239,7 @@ export default function Hardware() {
|
|||||||
const [selectedDisk, setSelectedDisk] = useState<StorageDevice | null>(null)
|
const [selectedDisk, setSelectedDisk] = useState<StorageDevice | null>(null)
|
||||||
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
|
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
|
||||||
const [selectedUPS, setSelectedUPS] = useState<any>(null)
|
const [selectedUPS, setSelectedUPS] = useState<any>(null)
|
||||||
const [nvidiaSessionId, setNvidiaSessionId] = useState<string | null>(null)
|
const [showNvidiaInstaller, setShowNvidiaInstaller] = useState(false)
|
||||||
const [installingNvidiaDriver, setInstallingNvidiaDriver] = useState(false)
|
const [installingNvidiaDriver, setInstallingNvidiaDriver] = useState(false)
|
||||||
|
|
||||||
const fetcher = async (url: string) => {
|
const fetcher = async (url: string) => {
|
||||||
@@ -257,39 +257,9 @@ export default function Hardware() {
|
|||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleInstallNvidiaDriver = async () => {
|
const handleInstallNvidiaDriver = () => {
|
||||||
console.log("[v0] NVIDIA installation button clicked")
|
console.log("[v0] Opening NVIDIA installer terminal")
|
||||||
|
setShowNvidiaInstaller(true)
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
script_name: "nvidia_installer",
|
|
||||||
script_relative_path: "gpu_tpu/nvidia_installer.sh",
|
|
||||||
params: {
|
|
||||||
EXECUTION_MODE: "web",
|
|
||||||
WEB_LOG: "/tmp/nvidia_web_install.log",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[v0] Calling Flask to execute script:", payload)
|
|
||||||
|
|
||||||
const data = await fetchApi("/api/scripts/execute", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log("[v0] Flask response:", data)
|
|
||||||
|
|
||||||
if (data.success && data.session_id) {
|
|
||||||
console.log("[v0] Installation started with session ID:", data.session_id)
|
|
||||||
setNvidiaSessionId(data.session_id)
|
|
||||||
} else {
|
|
||||||
console.error("[v0] Installation failed:", data.error)
|
|
||||||
alert(`Failed to install NVIDIA drivers: ${data.error}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[v0] Exception during installation:", error)
|
|
||||||
alert("Failed to start NVIDIA driver installation. Check console for details.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1130,18 +1100,11 @@ export default function Hardware() {
|
|||||||
{getMonitoringToolRecommendation(selectedGPU.vendor)}
|
{getMonitoringToolRecommendation(selectedGPU.vendor)}
|
||||||
</p>
|
</p>
|
||||||
{selectedGPU.vendor.toLowerCase().includes("nvidia") && (
|
{selectedGPU.vendor.toLowerCase().includes("nvidia") && (
|
||||||
<Button onClick={handleInstallNvidiaDriver} disabled={!!nvidiaSessionId} className="w-full">
|
<Button onClick={handleInstallNvidiaDriver} className="w-full">
|
||||||
{nvidiaSessionId ? (
|
<>
|
||||||
<>
|
<Download className="mr-2 h-4 w-4" />
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
Install NVIDIA Drivers
|
||||||
Installing...
|
</>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
|
||||||
Install NVIDIA Drivers
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2057,23 +2020,35 @@ export default function Hardware() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* NVIDIA Installation Monitor */}
|
{/* NVIDIA Installation Monitor */}
|
||||||
{nvidiaSessionId && (
|
{/* <HybridScriptMonitor
|
||||||
<HybridScriptMonitor
|
sessionId={nvidiaSessionId}
|
||||||
sessionId={nvidiaSessionId}
|
title="NVIDIA Driver Installation"
|
||||||
title="NVIDIA Driver Installation"
|
description="Installing NVIDIA proprietary drivers for GPU monitoring..."
|
||||||
description="Installing NVIDIA proprietary drivers for GPU monitoring..."
|
onClose={() => {
|
||||||
onClose={() => {
|
setNvidiaSessionId(null)
|
||||||
setNvidiaSessionId(null)
|
mutateHardware()
|
||||||
|
}}
|
||||||
|
onComplete={(success) => {
|
||||||
|
console.log("[v0] NVIDIA installation completed:", success ? "success" : "failed")
|
||||||
|
if (success) {
|
||||||
mutateHardware()
|
mutateHardware()
|
||||||
}}
|
}
|
||||||
onComplete={(success) => {
|
}}
|
||||||
console.log("[v0] NVIDIA installation completed:", success ? "success" : "failed")
|
/> */}
|
||||||
if (success) {
|
<ScriptTerminalModal
|
||||||
mutateHardware()
|
open={showNvidiaInstaller}
|
||||||
}
|
onClose={() => {
|
||||||
}}
|
setShowNvidiaInstaller(false)
|
||||||
/>
|
mutateHardware()
|
||||||
)}
|
}}
|
||||||
|
scriptPath="/usr/local/share/proxmenux/scripts/gpu_tpu/nvidia_installer.sh"
|
||||||
|
scriptName="nvidia_installer"
|
||||||
|
params={{
|
||||||
|
EXECUTION_MODE: "web",
|
||||||
|
}}
|
||||||
|
title="NVIDIA Driver Installation"
|
||||||
|
description="Installing NVIDIA proprietary drivers for GPU monitoring..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,428 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from "react"
|
|
||||||
import { fetchApi, getApiUrl } from "@/lib/api-config"
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
||||||
import { Loader2, CheckCircle2, XCircle, TerminalIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import type { Terminal } from "xterm"
|
|
||||||
import type { FitAddon } from "xterm-addon-fit"
|
|
||||||
|
|
||||||
interface HybridScriptMonitorProps {
|
|
||||||
sessionId: string | null
|
|
||||||
title?: string
|
|
||||||
description?: string
|
|
||||||
onClose: () => void
|
|
||||||
onComplete?: (success: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScriptInteraction {
|
|
||||||
type: "msgbox" | "yesno" | "inputbox" | "menu"
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
text: string
|
|
||||||
data?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HybridScriptMonitor({
|
|
||||||
sessionId,
|
|
||||||
title = "Script Execution",
|
|
||||||
description = "Monitoring script execution...",
|
|
||||||
onClose,
|
|
||||||
onComplete,
|
|
||||||
}: HybridScriptMonitorProps) {
|
|
||||||
const [interaction, setInteraction] = useState<ScriptInteraction | null>(null)
|
|
||||||
const [status, setStatus] = useState<"running" | "completed" | "failed">("running")
|
|
||||||
const [inputValue, setInputValue] = useState("")
|
|
||||||
const [selectedMenuItem, setSelectedMenuItem] = useState<string>("")
|
|
||||||
const [isResponding, setIsResponding] = useState(false)
|
|
||||||
|
|
||||||
const terminalRef = useRef<HTMLDivElement>(null)
|
|
||||||
const xtermRef = useRef<Terminal | null>(null)
|
|
||||||
const fitAddonRef = useRef<FitAddon | null>(null)
|
|
||||||
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
|
||||||
|
|
||||||
const decodeBase64 = (str: string): string => {
|
|
||||||
try {
|
|
||||||
return atob(str)
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[v0] Failed to decode base64:", str, e)
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!terminalRef.current || xtermRef.current || typeof window === "undefined") return
|
|
||||||
|
|
||||||
const loadTerminal = async () => {
|
|
||||||
const { Terminal } = await import("xterm")
|
|
||||||
const { FitAddon } = await import("xterm-addon-fit")
|
|
||||||
await import("xterm/css/xterm.css")
|
|
||||||
|
|
||||||
const term = new Terminal({
|
|
||||||
cursorBlink: false,
|
|
||||||
fontSize: 13,
|
|
||||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
||||||
theme: {
|
|
||||||
background: "#1e1e1e",
|
|
||||||
foreground: "#d4d4d4",
|
|
||||||
cursor: "#d4d4d4",
|
|
||||||
black: "#000000",
|
|
||||||
red: "#cd3131",
|
|
||||||
green: "#0dbc79",
|
|
||||||
yellow: "#e5e510",
|
|
||||||
blue: "#2472c8",
|
|
||||||
magenta: "#bc3fbc",
|
|
||||||
cyan: "#11a8cd",
|
|
||||||
white: "#e5e5e5",
|
|
||||||
brightBlack: "#666666",
|
|
||||||
brightRed: "#f14c4c",
|
|
||||||
brightGreen: "#23d18b",
|
|
||||||
brightYellow: "#f5f543",
|
|
||||||
brightBlue: "#3b8eea",
|
|
||||||
brightMagenta: "#d670d6",
|
|
||||||
brightCyan: "#29b8db",
|
|
||||||
brightWhite: "#ffffff",
|
|
||||||
},
|
|
||||||
convertEol: true,
|
|
||||||
disableStdin: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const fitAddon = new FitAddon()
|
|
||||||
term.loadAddon(fitAddon)
|
|
||||||
term.open(terminalRef.current!)
|
|
||||||
fitAddon.fit()
|
|
||||||
|
|
||||||
xtermRef.current = term
|
|
||||||
fitAddonRef.current = fitAddon
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
fitAddon.fit()
|
|
||||||
})
|
|
||||||
resizeObserver.observe(terminalRef.current!)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
resizeObserver.disconnect()
|
|
||||||
term.dispose()
|
|
||||||
xtermRef.current = null
|
|
||||||
fitAddonRef.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTerminal()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!sessionId || !xtermRef.current) return
|
|
||||||
|
|
||||||
const term = xtermRef.current
|
|
||||||
term.writeln("\x1b[32m[INFO] Conectando al stream de logs...\x1b[0m")
|
|
||||||
|
|
||||||
const eventSourceUrl = getApiUrl(`/api/scripts/logs/${sessionId}`)
|
|
||||||
const eventSource = new EventSource(eventSourceUrl)
|
|
||||||
|
|
||||||
eventSource.onopen = () => {
|
|
||||||
term.writeln("\x1b[32m[INFO] Conexión establecida con el servidor\x1b[0m")
|
|
||||||
}
|
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data)
|
|
||||||
|
|
||||||
if (data.type === "init") {
|
|
||||||
term.writeln(`\x1b[36m[INICIO] Ejecutando: ${data.script}\x1b[0m`)
|
|
||||||
term.writeln(`\x1b[36m[INICIO] Session ID: ${data.session_id}\x1b[0m`)
|
|
||||||
term.writeln("")
|
|
||||||
} else if (data.type === "raw") {
|
|
||||||
const message = data.message
|
|
||||||
|
|
||||||
// Detectar WEB_INTERACTION y mostrar modal, pero NO escribir en terminal
|
|
||||||
if (message.includes("WEB_INTERACTION:")) {
|
|
||||||
const interactionPart = message.split("WEB_INTERACTION:")[1]
|
|
||||||
|
|
||||||
if (interactionPart) {
|
|
||||||
const parts = interactionPart.split(":")
|
|
||||||
|
|
||||||
if (parts.length >= 4) {
|
|
||||||
const [type, id, titleB64, textB64, ...dataParts] = parts
|
|
||||||
const dataB64 = dataParts.join(":")
|
|
||||||
|
|
||||||
setInteraction({
|
|
||||||
type: type as ScriptInteraction["type"],
|
|
||||||
id,
|
|
||||||
title: decodeBase64(titleB64),
|
|
||||||
text: decodeBase64(textB64),
|
|
||||||
data: dataB64 ? decodeBase64(dataB64) : undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
term.writeln(message)
|
|
||||||
}
|
|
||||||
} else if (data.type === "error") {
|
|
||||||
term.writeln(`\x1b[31m[ERROR] ${data.message}\x1b[0m`)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
term.writeln(`\x1b[31m[PARSE ERROR] ${event.data.substring(0, 100)}\x1b[0m`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventSource.onerror = () => {
|
|
||||||
term.writeln("\x1b[31m[ERROR] Conexión perdida, reintentando...\x1b[0m")
|
|
||||||
}
|
|
||||||
|
|
||||||
const pollStatus = async () => {
|
|
||||||
try {
|
|
||||||
const statusData = await fetchApi(`/api/scripts/status/${sessionId}`)
|
|
||||||
|
|
||||||
if (statusData.status === "completed" || statusData.exit_code === 0) {
|
|
||||||
term.writeln("")
|
|
||||||
term.writeln("\x1b[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m")
|
|
||||||
term.writeln("\x1b[32m✓ Script completado exitosamente\x1b[0m")
|
|
||||||
term.writeln("\x1b[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m")
|
|
||||||
setStatus("completed")
|
|
||||||
eventSource.close()
|
|
||||||
if (pollingIntervalRef.current) {
|
|
||||||
clearInterval(pollingIntervalRef.current)
|
|
||||||
}
|
|
||||||
onComplete?.(true)
|
|
||||||
} else if (statusData.status === "failed" || (statusData.exit_code !== null && statusData.exit_code !== 0)) {
|
|
||||||
term.writeln("")
|
|
||||||
term.writeln("\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m")
|
|
||||||
term.writeln(`\x1b[31m✗ Script falló con código de salida: ${statusData.exit_code}\x1b[0m`)
|
|
||||||
term.writeln("\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m")
|
|
||||||
setStatus("failed")
|
|
||||||
eventSource.close()
|
|
||||||
if (pollingIntervalRef.current) {
|
|
||||||
clearInterval(pollingIntervalRef.current)
|
|
||||||
}
|
|
||||||
onComplete?.(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detectar interacciones pendientes desde el status
|
|
||||||
if (statusData.pending_interaction) {
|
|
||||||
const parts = statusData.pending_interaction.split(":")
|
|
||||||
if (parts.length >= 4) {
|
|
||||||
const [type, id, titleB64, textB64, ...dataParts] = parts
|
|
||||||
const dataB64 = dataParts.join(":")
|
|
||||||
|
|
||||||
setInteraction({
|
|
||||||
type: type as ScriptInteraction["type"],
|
|
||||||
id,
|
|
||||||
title: decodeBase64(titleB64),
|
|
||||||
text: decodeBase64(textB64),
|
|
||||||
data: dataB64 ? decodeBase64(dataB64) : undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[v0] Error polling status:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pollStatus()
|
|
||||||
pollingIntervalRef.current = setInterval(pollStatus, 2000)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
eventSource.close()
|
|
||||||
if (pollingIntervalRef.current) {
|
|
||||||
clearInterval(pollingIntervalRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [sessionId, onComplete])
|
|
||||||
|
|
||||||
const handleInteractionResponse = async (response: string) => {
|
|
||||||
if (!interaction || !sessionId || !xtermRef.current) return
|
|
||||||
|
|
||||||
const term = xtermRef.current
|
|
||||||
setIsResponding(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
term.writeln(`\x1b[33m[USUARIO] Respuesta: ${response}\x1b[0m`)
|
|
||||||
|
|
||||||
await fetchApi("/api/scripts/respond", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
session_id: sessionId,
|
|
||||||
interaction_id: interaction.id,
|
|
||||||
value: response,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
setInteraction(null)
|
|
||||||
setInputValue("")
|
|
||||||
setSelectedMenuItem("")
|
|
||||||
} catch (error) {
|
|
||||||
term.writeln(`\x1b[31m[ERROR] Error enviando respuesta: ${error}\x1b[0m`)
|
|
||||||
} finally {
|
|
||||||
setIsResponding(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderInteractionModal = () => {
|
|
||||||
if (!interaction) return null
|
|
||||||
|
|
||||||
switch (interaction.type) {
|
|
||||||
case "msgbox":
|
|
||||||
return (
|
|
||||||
<Dialog open={true} onOpenChange={() => {}}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{interaction.title}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogDescription className="py-6 text-base whitespace-pre-wrap">{interaction.text}</DialogDescription>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button onClick={() => handleInteractionResponse("ok")} disabled={isResponding}>
|
|
||||||
{isResponding ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
||||||
OK
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
|
|
||||||
case "yesno":
|
|
||||||
return (
|
|
||||||
<Dialog open={true} onOpenChange={() => {}}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{interaction.title}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogDescription className="py-6 text-base whitespace-pre-wrap">{interaction.text}</DialogDescription>
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button variant="outline" onClick={() => handleInteractionResponse("no")} disabled={isResponding}>
|
|
||||||
No
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => handleInteractionResponse("yes")} disabled={isResponding}>
|
|
||||||
{isResponding ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
||||||
Yes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
|
|
||||||
case "inputbox":
|
|
||||||
return (
|
|
||||||
<Dialog open={true} onOpenChange={() => {}}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{interaction.title}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogDescription className="py-4 text-base whitespace-pre-wrap">{interaction.text}</DialogDescription>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="input-value">Valor</Label>
|
|
||||||
<Input
|
|
||||||
id="input-value"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
|
||||||
placeholder={interaction.data || "Introduce el valor..."}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-3">
|
|
||||||
<Button variant="outline" onClick={() => handleInteractionResponse("")} disabled={isResponding}>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => handleInteractionResponse(inputValue)} disabled={isResponding || !inputValue}>
|
|
||||||
{isResponding ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
||||||
OK
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
|
|
||||||
case "menu":
|
|
||||||
const menuItems = interaction.data?.split("|").filter(Boolean) || []
|
|
||||||
return (
|
|
||||||
<Dialog open={true} onOpenChange={() => {}}>
|
|
||||||
<DialogContent className="sm:max-w-2xl max-h-[80vh]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{interaction.title}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogDescription className="py-4 text-base whitespace-pre-wrap">{interaction.text}</DialogDescription>
|
|
||||||
<ScrollArea className="max-h-96 pr-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{menuItems.map((item, index) => {
|
|
||||||
const [value, label] = item.includes(":") ? item.split(":") : [item, item]
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={index}
|
|
||||||
variant={selectedMenuItem === value ? "default" : "outline"}
|
|
||||||
className="w-full justify-start text-left h-auto py-3 px-4"
|
|
||||||
onClick={() => setSelectedMenuItem(value)}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
<div className="flex justify-end gap-3 mt-4 pt-4 border-t">
|
|
||||||
<Button variant="outline" onClick={() => handleInteractionResponse("")} disabled={isResponding}>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleInteractionResponse(selectedMenuItem)}
|
|
||||||
disabled={isResponding || !selectedMenuItem}
|
|
||||||
>
|
|
||||||
{isResponding ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
||||||
Seleccionar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionId) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Dialog open={true} onOpenChange={status !== "running" ? onClose : undefined}>
|
|
||||||
<DialogContent className="max-w-5xl max-h-[85vh]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="flex items-center gap-2">
|
|
||||||
{status === "running" && <Loader2 className="h-5 w-5 animate-spin" />}
|
|
||||||
{status === "completed" && <CheckCircle2 className="h-5 w-5 text-green-500" />}
|
|
||||||
{status === "failed" && <XCircle className="h-5 w-5 text-red-500" />}
|
|
||||||
<TerminalIcon className="h-5 w-5" />
|
|
||||||
{title}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>{description}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="border rounded-lg overflow-hidden bg-[#1e1e1e]">
|
|
||||||
<div ref={terminalRef} className="h-[500px] p-2" style={{ width: "100%", height: "500px" }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-4 border-t">
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Session ID: <span className="font-mono">{sessionId}</span>
|
|
||||||
</div>
|
|
||||||
{status !== "running" && (
|
|
||||||
<Button onClick={onClose} size="lg">
|
|
||||||
Cerrar
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{renderInteractionModal()}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
162
AppImage/components/script-terminal-modal.tsx
Normal file
162
AppImage/components/script-terminal-modal.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { AlertCircle, CheckCircle2 } from "lucide-react"
|
||||||
|
import { io, type Socket } from "socket.io-client"
|
||||||
|
|
||||||
|
interface ScriptTerminalModalProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
scriptPath: string
|
||||||
|
scriptName: string
|
||||||
|
params?: Record<string, string>
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebInteraction {
|
||||||
|
type: "yesno" | "menu" | "msgbox" | "input"
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
text: string
|
||||||
|
options?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScriptTerminalModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
scriptPath,
|
||||||
|
scriptName,
|
||||||
|
params = {},
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: ScriptTerminalModalProps) {
|
||||||
|
const [socket, setSocket] = useState<Socket | null>(null)
|
||||||
|
const [sessionId] = useState(() => Math.random().toString(36).substring(7))
|
||||||
|
const [terminalOutput, setTerminalOutput] = useState<string>("")
|
||||||
|
const [isRunning, setIsRunning] = useState(false)
|
||||||
|
const [isComplete, setIsComplete] = useState(false)
|
||||||
|
const [exitCode, setExitCode] = useState<number | null>(null)
|
||||||
|
const [interaction, setInteraction] = useState<WebInteraction | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
|
||||||
|
const newSocket = io("http://localhost:8008", {
|
||||||
|
transports: ["websocket"],
|
||||||
|
reconnection: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
newSocket.on("connect", () => {
|
||||||
|
console.log("[v0] WebSocket connected")
|
||||||
|
// Ejecutar el script
|
||||||
|
newSocket.emit("execute_script", {
|
||||||
|
session_id: sessionId,
|
||||||
|
script_path: scriptPath,
|
||||||
|
script_name: scriptName,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
setIsRunning(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
newSocket.on("script_output", (data: { line: string }) => {
|
||||||
|
setTerminalOutput((prev) => prev + data.line + "\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
newSocket.on("web_interaction", (data: WebInteraction) => {
|
||||||
|
console.log("[v0] Web interaction received:", data)
|
||||||
|
setInteraction(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
newSocket.on("script_complete", (data: { exit_code: number }) => {
|
||||||
|
console.log("[v0] Script complete, exit code:", data.exit_code)
|
||||||
|
setIsRunning(false)
|
||||||
|
setIsComplete(true)
|
||||||
|
setExitCode(data.exit_code)
|
||||||
|
})
|
||||||
|
|
||||||
|
setSocket(newSocket)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
newSocket.disconnect()
|
||||||
|
}
|
||||||
|
}, [open, sessionId, scriptPath, scriptName, params])
|
||||||
|
|
||||||
|
const handleInteractionResponse = (value: string) => {
|
||||||
|
if (!socket || !interaction) return
|
||||||
|
|
||||||
|
console.log("[v0] Sending interaction response:", value)
|
||||||
|
socket.emit("interaction_response", {
|
||||||
|
session_id: sessionId,
|
||||||
|
interaction_id: interaction.id,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Escribir la respuesta en la terminal
|
||||||
|
setTerminalOutput((prev) => prev + `\n> ${value}\n`)
|
||||||
|
setInteraction(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-4xl h-[80vh] flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
{isRunning && <span className="animate-spin">⚙️</span>}
|
||||||
|
{isComplete && exitCode === 0 && <CheckCircle2 className="w-5 h-5 text-green-500" />}
|
||||||
|
{isComplete && exitCode !== 0 && <AlertCircle className="w-5 h-5 text-red-500" />}
|
||||||
|
{title}
|
||||||
|
</DialogTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 bg-black rounded-md p-4 overflow-auto font-mono text-sm text-green-400">
|
||||||
|
<pre className="whitespace-pre-wrap">{terminalOutput}</pre>
|
||||||
|
{isRunning && <span className="animate-pulse">_</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isComplete && (
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded-md ${exitCode === 0 ? "bg-green-500/10 text-green-500" : "bg-red-500/10 text-red-500"}`}
|
||||||
|
>
|
||||||
|
{exitCode === 0 ? "✓ Script completed successfully" : `✗ Script failed with exit code ${exitCode}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">Session ID: {sessionId}</span>
|
||||||
|
<Button onClick={onClose} disabled={isRunning}>
|
||||||
|
{isRunning ? "Running..." : "Close"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{interaction && (
|
||||||
|
<Dialog open={true} onOpenChange={() => setInteraction(null)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{interaction.title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<p className="text-sm">{interaction.text}</p>
|
||||||
|
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
{interaction.type === "yesno" && (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => handleInteractionResponse("no")}>
|
||||||
|
No
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => handleInteractionResponse("yes")}>Yes</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{interaction.type === "msgbox" && <Button onClick={() => handleInteractionResponse("ok")}>OK</Button>}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -91,36 +91,44 @@ class ScriptRunner:
|
|||||||
|
|
||||||
lines_read = [0] # Lista para compartir entre threads
|
lines_read = [0] # Lista para compartir entre threads
|
||||||
|
|
||||||
# Monitor output for interactions
|
|
||||||
def monitor_output():
|
def monitor_output():
|
||||||
print(f"[DEBUG] monitor_output thread started for session {session_id}", file=sys.stderr, flush=True)
|
print(f"[DEBUG] monitor_output thread started for session {session_id}", file=sys.stderr, flush=True)
|
||||||
|
print(f"[DEBUG] Will monitor log file: {log_file}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(log_file, 'a') as log_f:
|
# Read log file in real-time (similar to tail -f)
|
||||||
while True:
|
last_position = 0
|
||||||
line = process.stdout.readline()
|
|
||||||
if not line:
|
# Wait a moment for script to start writing
|
||||||
print(f"[DEBUG] No more lines from stdout (EOF reached)", file=sys.stderr, flush=True)
|
time.sleep(0.5)
|
||||||
break
|
|
||||||
|
while process.poll() is None or last_position < os.path.getsize(log_file):
|
||||||
decoded_line = line.decode('utf-8', errors='replace').rstrip()
|
try:
|
||||||
lines_read[0] += 1
|
if os.path.exists(log_file):
|
||||||
|
with open(log_file, 'r') as log_f:
|
||||||
print(f"[DEBUG] Read line {lines_read[0]}: {decoded_line[:100]}...", file=sys.stderr, flush=True)
|
log_f.seek(last_position)
|
||||||
|
new_lines = log_f.readlines()
|
||||||
log_f.write(decoded_line + '\n')
|
|
||||||
log_f.flush()
|
for line in new_lines:
|
||||||
|
decoded_line = line.rstrip()
|
||||||
# Check for interaction requests
|
if decoded_line: # Skip empty lines
|
||||||
try:
|
lines_read[0] += 1
|
||||||
if decoded_line.strip().startswith('{'):
|
print(f"[DEBUG] Read line {lines_read[0]} from log: {decoded_line[:100]}...", file=sys.stderr, flush=True)
|
||||||
data = json.loads(decoded_line.strip())
|
|
||||||
if data.get('type') == 'interaction_request':
|
# Check for interaction requests in the line
|
||||||
session['pending_interaction'] = data
|
if 'WEB_INTERACTION:' in decoded_line:
|
||||||
print(f"[DEBUG] Detected interaction request: {data}", file=sys.stderr, flush=True)
|
print(f"[DEBUG] Detected WEB_INTERACTION line: {decoded_line}", file=sys.stderr, flush=True)
|
||||||
except json.JSONDecodeError:
|
session['pending_interaction'] = decoded_line
|
||||||
pass
|
|
||||||
|
last_position = log_f.tell()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DEBUG ERROR] Error reading log file: {e}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
time.sleep(0.1) # Poll every 100ms
|
||||||
|
|
||||||
print(f"[DEBUG] monitor_output thread finished. Total lines read: {lines_read[0]}", file=sys.stderr, flush=True)
|
print(f"[DEBUG] monitor_output thread finished. Total lines read: {lines_read[0]}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DEBUG ERROR] Exception in monitor_output: {e}", file=sys.stderr, flush=True)
|
print(f"[DEBUG ERROR] Exception in monitor_output: {e}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|||||||
@@ -236,6 +236,169 @@ def terminal_websocket(ws):
|
|||||||
if session_id in active_sessions:
|
if session_id in active_sessions:
|
||||||
del active_sessions[session_id]
|
del active_sessions[session_id]
|
||||||
|
|
||||||
|
@sock.route('/ws/script/<session_id>')
|
||||||
|
def script_websocket(ws, session_id):
|
||||||
|
"""WebSocket endpoint for executing scripts with hybrid web mode"""
|
||||||
|
|
||||||
|
# Receive initial script execution request
|
||||||
|
try:
|
||||||
|
init_data = ws.receive(timeout=5)
|
||||||
|
if not init_data:
|
||||||
|
ws.send(json.dumps({'type': 'error', 'message': 'No script data received'}))
|
||||||
|
return
|
||||||
|
|
||||||
|
script_data = json.loads(init_data)
|
||||||
|
script_path = script_data.get('script_path')
|
||||||
|
params = script_data.get('params', {})
|
||||||
|
|
||||||
|
if not script_path:
|
||||||
|
ws.send(json.dumps({'type': 'error', 'message': 'No script_path provided'}))
|
||||||
|
return
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
ws.send(json.dumps({'type': 'error', 'message': f'Invalid init data: {str(e)}'}))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create pseudo-terminal for script execution
|
||||||
|
master_fd, slave_fd = pty.openpty()
|
||||||
|
|
||||||
|
# Build environment variables from params
|
||||||
|
env = os.environ.copy()
|
||||||
|
for key, value in params.items():
|
||||||
|
env[key] = str(value)
|
||||||
|
|
||||||
|
# Start script process with PTY
|
||||||
|
script_process = subprocess.Popen(
|
||||||
|
['/bin/bash', script_path],
|
||||||
|
stdin=slave_fd,
|
||||||
|
stdout=slave_fd,
|
||||||
|
stderr=slave_fd,
|
||||||
|
preexec_fn=os.setsid,
|
||||||
|
env=env
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set non-blocking mode for master_fd
|
||||||
|
flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
|
||||||
|
fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||||
|
|
||||||
|
# Set terminal size
|
||||||
|
set_winsize(master_fd, 30, 120)
|
||||||
|
|
||||||
|
# Thread to read script output and forward to WebSocket
|
||||||
|
def read_script_output():
|
||||||
|
buffer = ""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
r, _, _ = select.select([master_fd], [], [], 0.01)
|
||||||
|
if master_fd in r:
|
||||||
|
try:
|
||||||
|
data = os.read(master_fd, 4096)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
|
||||||
|
text = data.decode('utf-8', errors='ignore')
|
||||||
|
buffer += text
|
||||||
|
|
||||||
|
# Process line by line to detect WEB_INTERACTION
|
||||||
|
lines = buffer.split('\n')
|
||||||
|
buffer = lines[-1] # Keep incomplete line in buffer
|
||||||
|
|
||||||
|
for line in lines[:-1]:
|
||||||
|
# Detect WEB_INTERACTION lines
|
||||||
|
if line.strip().startswith('WEB_INTERACTION:'):
|
||||||
|
# Parse interaction: WEB_INTERACTION:type:id:title_b64:text_b64:data_b64
|
||||||
|
try:
|
||||||
|
parts = line.strip().split(':', 6)
|
||||||
|
if len(parts) >= 5:
|
||||||
|
interaction = {
|
||||||
|
'type': 'interaction',
|
||||||
|
'interaction_type': parts[1],
|
||||||
|
'interaction_id': parts[2],
|
||||||
|
'title': parts[3],
|
||||||
|
'text': parts[4],
|
||||||
|
'data': parts[5] if len(parts) > 5 else ''
|
||||||
|
}
|
||||||
|
ws.send(json.dumps(interaction))
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing WEB_INTERACTION: {e}")
|
||||||
|
|
||||||
|
# Regular output line
|
||||||
|
ws.send(json.dumps({
|
||||||
|
'type': 'output',
|
||||||
|
'data': line + '\n'
|
||||||
|
}))
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading script output: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Send completion message
|
||||||
|
exit_code = script_process.poll()
|
||||||
|
ws.send(json.dumps({
|
||||||
|
'type': 'exit',
|
||||||
|
'code': exit_code if exit_code is not None else 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
output_thread = threading.Thread(target=read_script_output, daemon=True)
|
||||||
|
output_thread.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# Receive user input or interaction responses
|
||||||
|
data = ws.receive(timeout=None)
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg = json.loads(data)
|
||||||
|
|
||||||
|
# Handle resize
|
||||||
|
if msg.get('type') == 'resize':
|
||||||
|
cols = int(msg.get('cols', 120))
|
||||||
|
rows = int(msg.get('rows', 30))
|
||||||
|
set_winsize(master_fd, rows, cols)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle interaction response
|
||||||
|
if msg.get('type') == 'response':
|
||||||
|
response_value = msg.get('value', '')
|
||||||
|
# Write response to script's stdin
|
||||||
|
os.write(master_fd, (str(response_value) + '\n').encode('utf-8'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Raw text input, send to script
|
||||||
|
os.write(master_fd, data.encode('utf-8'))
|
||||||
|
|
||||||
|
# Check if process is still alive
|
||||||
|
if script_process.poll() is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Script session error: {e}")
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
try:
|
||||||
|
script_process.terminate()
|
||||||
|
script_process.wait(timeout=1)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
script_process.kill()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.close(master_fd)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.close(slave_fd)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def init_terminal_routes(app):
|
def init_terminal_routes(app):
|
||||||
"""Initialize terminal routes with Flask app"""
|
"""Initialize terminal routes with Flask app"""
|
||||||
|
|||||||
Reference in New Issue
Block a user