Spaces:
Running
Running
File size: 6,328 Bytes
14fc0ec 0b02a00 584434c 0b02a00 615586d 0b02a00 ddbcf92 584434c 615586d 584434c 615586d 584434c 615586d 584434c 0b02a00 615586d ddbcf92 615586d 37635f4 615586d ddbcf92 615586d 37635f4 615586d 37635f4 615586d 37635f4 615586d 37635f4 615586d 37635f4 615586d 37635f4 584434c 37635f4 615586d 37635f4 615586d 37635f4 615586d 37635f4 615586d 37635f4 615586d 37635f4 615586d 37635f4 0b02a00 615586d 37635f4 615586d ddbcf92 615586d 37635f4 615586d 37635f4 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
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>
);
}
|