orztv
update
37635f4
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>
);
}