Spaces:
Running
Running
import { useState, useEffect, useCallback } from 'react'; | |
import { json, type ActionFunction, type LoaderFunction } from '@remix-run/node'; | |
import { useLoaderData, useSubmit, useNavigation } from '@remix-run/react'; | |
import { spawn, type ChildProcess } from 'child_process'; | |
type SshxStatus = 'running' | 'stopped'; | |
interface LoaderData { | |
status: SshxStatus; | |
output: string; | |
link: string | null; | |
shell: string | null; | |
} | |
let sshxProcess: ChildProcess | null = null; | |
let sshxOutput = ''; | |
const extractFromOutput = (output: string, regex: RegExp): string | null => { | |
const match = output.match(regex); | |
return match ? match[1] : null; | |
}; | |
const handleProcessOutput = (data: Buffer) => { | |
sshxOutput += data.toString(); | |
}; | |
const handleProcessClose = () => { | |
sshxProcess = null; | |
sshxOutput += '\nSSHX进程已结束。'; | |
}; | |
export const loader: LoaderFunction = async () => { | |
const status: SshxStatus = sshxProcess ? 'running' : 'stopped'; | |
const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/); | |
const shell = extractFromOutput(sshxOutput, /Shell:\s+([^\n]+)/); | |
return json<LoaderData>({ status, output: sshxOutput, link, shell }); | |
}; | |
export const action: ActionFunction = async ({ request }) => { | |
const formData = await request.formData(); | |
const action = formData.get('action'); | |
if (action === 'start' && !sshxProcess) { | |
sshxProcess = spawn('/home/pn/sshx/sshx', ['-q']); // 修改这里,添加 '-q' 参数 | |
sshxProcess.stdout?.on('data', handleProcessOutput); | |
sshxProcess.stderr?.on('data', handleProcessOutput); | |
sshxProcess.on('close', handleProcessClose); | |
return json({ status: 'started' }); | |
} else if (action === 'stop' && sshxProcess) { | |
sshxProcess.kill(); | |
handleProcessClose(); | |
return json({ status: 'stopped' }); | |
} | |
return json({ error: '无效操作' }, { status: 400 }); | |
}; | |
export default function Sshx() { | |
const { status, output, link, shell } = useLoaderData<LoaderData>(); | |
const submit = useSubmit(); | |
const navigation = useNavigation(); | |
const [localOutput, setLocalOutput] = useState(output); | |
const [startTime, setStartTime] = useState<number | null>(null); | |
const refreshData = useCallback(() => { | |
submit(null, { method: 'get', replace: true }); | |
}, [submit]); | |
const stopSshx = useCallback(() => { | |
submit({ action: 'stop' }, { method: 'post' }); | |
setStartTime(null); | |
}, [submit]); | |
useEffect(() => { | |
let interval: NodeJS.Timeout | null = null; | |
let timer: NodeJS.Timeout | null = null; | |
if (status === 'running') { | |
interval = setInterval(refreshData, 60000); // 每60秒更新一次 | |
if (startTime === null) { | |
setStartTime(Date.now()); | |
} else { | |
const elapsedTime = Date.now() - startTime; | |
const remainingTime = 600000 - elapsedTime; // 10分钟 = 600000毫秒 | |
if (remainingTime > 0) { | |
timer = setTimeout(stopSshx, remainingTime); | |
} else { | |
stopSshx(); | |
} | |
} | |
} else { | |
setStartTime(null); | |
} | |
return () => { | |
if (interval) clearInterval(interval); | |
if (timer) clearTimeout(timer); | |
}; | |
}, [refreshData, status, startTime, stopSshx]); | |
useEffect(() => { | |
setLocalOutput(output); | |
}, [output]); | |
const isLoading = navigation.state === 'submitting' || navigation.state === 'loading'; | |
const handleAction = (action: 'start' | 'stop') => { | |
if (action === 'start') { | |
setStartTime(Date.now()); | |
} else { | |
setStartTime(null); | |
} | |
submit({ action }, { method: 'post' }); | |
setTimeout(refreshData, 100); | |
}; | |
// 计算剩余时间 | |
const remainingTime = startTime ? Math.max(0, 600 - Math.floor((Date.now() - startTime) / 1000)) : 0; | |
return ( | |
<div className="container mx-auto px-4 py-8 max-w-3xl"> | |
<h1 className="text-3xl font-bold text-primary mb-6 pb-2 border-b-2 border-primary">SSHX控制面板</h1> | |
<div className="mb-6"> | |
<p className="text-lg font-semibold"> | |
状态: <span className={`${status === 'running' ? 'text-green-600' : 'text-red-600'} font-bold`}> | |
{status === 'running' ? '运行中' : '已停止'} | |
</span> | |
</p> | |
</div> | |
<div className="flex space-x-4 mb-6"> | |
<button | |
onClick={() => handleAction('start')} | |
className={`px-4 py-2 rounded-md text-white transition-colors duration-300 ${ | |
status === 'running' || isLoading | |
? 'bg-gray-400 cursor-not-allowed' | |
: 'bg-green-500 hover:bg-green-600' | |
}`} | |
disabled={status === 'running' || isLoading} | |
> | |
启动SSHX | |
</button> | |
<button | |
onClick={() => handleAction('stop')} | |
className={`px-4 py-2 rounded-md text-white transition-colors duration-300 ${ | |
status === 'stopped' || isLoading | |
? 'bg-gray-400 cursor-not-allowed' | |
: 'bg-red-500 hover:bg-red-600' | |
}`} | |
disabled={status === 'stopped' || isLoading} | |
> | |
停止SSHX | |
</button> | |
</div> | |
{status === 'running' && ( | |
<div className="bg-blue-100 border border-blue-300 rounded-md p-4 mb-6"> | |
<p className="mb-2 text-blue-800"> | |
<strong className="font-semibold">链接:</strong>{' '} | |
{link ? ( | |
<a href={link} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline font-medium"> | |
{link} | |
</a> | |
) : ( | |
<span className="text-gray-600">暂不可用</span> | |
)} | |
</p> | |
<p className="text-blue-800"> | |
<strong className="font-semibold">Shell:</strong> <span className="font-medium">{shell || '暂不可用'}</span> | |
</p> | |
<p className="text-blue-800"> | |
<strong className="font-semibold">剩余时间:</strong> <span className="font-medium">{remainingTime} 秒</span> | |
</p> | |
</div> | |
)} | |
<h2 className="text-2xl font-semibold text-secondary mb-4">输出:</h2> | |
<pre className="bg-gray-800 text-blue-600 p-4 rounded-md border border-gray-600 font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-96 overflow-y-auto"> | |
{localOutput} | |
</pre> | |
</div> | |
); | |
} | |