orztv commited on
Commit
615586d
·
1 Parent(s): 6507fab
Files changed (1) hide show
  1. src/sshx.tsx +113 -111
src/sshx.tsx CHANGED
@@ -6,139 +6,141 @@ import { spawn, type ChildProcess } from 'child_process';
6
  type SshxStatus = 'running' | 'stopped';
7
 
8
  interface LoaderData {
9
- status: SshxStatus;
10
- output: string;
11
- link: string | null;
12
- shell: string | null;
13
  }
14
 
15
  let sshxProcess: ChildProcess | null = null;
16
  let sshxOutput = '';
17
 
18
  const extractFromOutput = (output: string, regex: RegExp): string | null => {
19
- const match = output.match(regex);
20
- return match ? match[1] : null;
21
  };
22
 
23
  const handleProcessOutput = (data: Buffer) => {
24
- sshxOutput += data.toString();
25
  };
26
 
27
  const handleProcessClose = () => {
28
- sshxProcess = null;
29
- sshxOutput += '\nSSHX进程已结束。';
30
  };
31
 
32
  export const loader: LoaderFunction = async () => {
33
- const status: SshxStatus = sshxProcess ? 'running' : 'stopped';
34
- const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/);
35
- const shell = extractFromOutput(sshxOutput, /Shell:\s+([^\n]+)/);
36
- return json<LoaderData>({ status, output: sshxOutput, link, shell });
37
  };
38
 
39
  export const action: ActionFunction = async ({ request }) => {
40
- const formData = await request.formData();
41
- const action = formData.get('action');
42
-
43
- if (action === 'start' && !sshxProcess) {
44
- sshxProcess = spawn('/cloudide/workspace/hf-ssh/sshx/sshx', []);
45
- sshxProcess.stdout?.on('data', handleProcessOutput);
46
- sshxProcess.stderr?.on('data', handleProcessOutput);
47
- sshxProcess.on('close', handleProcessClose);
48
- return json({ status: 'started' });
49
- } else if (action === 'stop' && sshxProcess) {
50
- sshxProcess.kill();
51
- handleProcessClose();
52
- return json({ status: 'stopped' });
53
- }
54
-
55
- return json({ error: '无效操作' }, { status: 400 });
56
  };
57
 
58
  export default function Sshx() {
59
- const { status, output, link, shell } = useLoaderData<LoaderData>();
60
- const submit = useSubmit();
61
- const navigation = useNavigation();
62
- const [localOutput, setLocalOutput] = useState(output);
63
-
64
- const refreshData = useCallback(() => {
65
- submit(null, { method: 'get', replace: true });
66
- }, [submit]);
67
-
68
- useEffect(() => {
69
- let interval: NodeJS.Timeout | null = null;
70
- if (status === 'running') {
71
- interval = setInterval(refreshData, 60000); // 每60秒更新一次
72
- }
73
- return () => {
74
- if (interval) clearInterval(interval);
75
- };
76
- }, [refreshData, status]);
77
-
78
- useEffect(() => {
79
- setLocalOutput(output);
80
- }, [output]);
81
-
82
- const isLoading = navigation.state === 'submitting' || navigation.state === 'loading';
83
-
84
- const handleAction = (action: 'start' | 'stop') => {
85
- submit({ action }, { method: 'post' });
86
- setTimeout(refreshData, 100);
87
  };
88
-
89
- return (
90
- <div className="container mx-auto px-4 py-8 max-w-3xl">
91
- <h1 className="text-3xl font-bold text-primary mb-6 pb-2 border-b-2 border-primary">SSHX控制面板</h1>
92
- <div className="mb-6">
93
- <p className="text-lg font-semibold">
94
- 状态: <span className={`${status === 'running' ? 'text-green-600' : 'text-red-600'}`}>
95
- {status === 'running' ? '运行中' : '已停止'}
96
- </span>
97
- </p>
98
- </div>
99
- <div className="flex space-x-4 mb-6">
100
- <button
101
- onClick={() => handleAction('start')}
102
- className={`px-4 py-2 rounded-md text-white transition-colors duration-300 ${status === 'running' || isLoading
103
- ? 'bg-gray-400 cursor-not-allowed'
104
- : 'bg-green-500 hover:bg-green-600'
105
- }`}
106
- disabled={status === 'running' || isLoading}
107
- >
108
- 启动SSHX
109
- </button>
110
- <button
111
- onClick={() => handleAction('stop')}
112
- className={`px-4 py-2 rounded-md text-white transition-colors duration-300 ${status === 'stopped' || isLoading
113
- ? 'bg-gray-400 cursor-not-allowed'
114
- : 'bg-red-500 hover:bg-red-600'
115
- }`}
116
- disabled={status === 'stopped' || isLoading}
117
- >
118
- 停止SSHX
119
- </button>
120
- </div>
121
- {status === 'running' && (
122
- <div className="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
123
- <p className="mb-2">
124
- <strong className="font-semibold">链接:</strong>{' '}
125
- {link ? (
126
- <a href={link} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
127
- {link}
128
- </a>
129
- ) : (
130
- '暂不可用'
131
- )}
132
- </p>
133
- <p>
134
- <strong className="font-semibold">Shell:</strong> {shell || '暂不可用'}
135
- </p>
136
- </div>
 
 
 
 
 
 
 
 
137
  )}
138
- <h2 className="text-2xl font-semibold text-secondary mb-4">输出:</h2>
139
- <pre className="bg-gray-100 p-4 rounded-md border border-gray-300 font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-96 overflow-y-auto">
140
- {localOutput}
141
- </pre>
142
  </div>
143
- );
 
 
 
 
 
 
144
  }
 
6
  type SshxStatus = 'running' | 'stopped';
7
 
8
  interface LoaderData {
9
+ status: SshxStatus;
10
+ output: string;
11
+ link: string | null;
12
+ shell: string | null;
13
  }
14
 
15
  let sshxProcess: ChildProcess | null = null;
16
  let sshxOutput = '';
17
 
18
  const extractFromOutput = (output: string, regex: RegExp): string | null => {
19
+ const match = output.match(regex);
20
+ return match ? match[1] : null;
21
  };
22
 
23
  const handleProcessOutput = (data: Buffer) => {
24
+ sshxOutput += data.toString();
25
  };
26
 
27
  const handleProcessClose = () => {
28
+ sshxProcess = null;
29
+ sshxOutput += '\nSSHX进程已结束。';
30
  };
31
 
32
  export const loader: LoaderFunction = async () => {
33
+ const status: SshxStatus = sshxProcess ? 'running' : 'stopped';
34
+ const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/);
35
+ const shell = extractFromOutput(sshxOutput, /Shell:\s+([^\n]+)/);
36
+ return json<LoaderData>({ status, output: sshxOutput, link, shell });
37
  };
38
 
39
  export const action: ActionFunction = async ({ request }) => {
40
+ const formData = await request.formData();
41
+ const action = formData.get('action');
42
+
43
+ if (action === 'start' && !sshxProcess) {
44
+ sshxProcess = spawn('/home/pn/sshx/sshx', []);
45
+ sshxProcess.stdout?.on('data', handleProcessOutput);
46
+ sshxProcess.stderr?.on('data', handleProcessOutput);
47
+ sshxProcess.on('close', handleProcessClose);
48
+ return json({ status: 'started' });
49
+ } else if (action === 'stop' && sshxProcess) {
50
+ sshxProcess.kill();
51
+ handleProcessClose();
52
+ return json({ status: 'stopped' });
53
+ }
54
+
55
+ return json({ error: '无效操作' }, { status: 400 });
56
  };
57
 
58
  export default function Sshx() {
59
+ const { status, output, link, shell } = useLoaderData<LoaderData>();
60
+ const submit = useSubmit();
61
+ const navigation = useNavigation();
62
+ const [localOutput, setLocalOutput] = useState(output);
63
+
64
+ const refreshData = useCallback(() => {
65
+ submit(null, { method: 'get', replace: true });
66
+ }, [submit]);
67
+
68
+ useEffect(() => {
69
+ let interval: NodeJS.Timeout | null = null;
70
+ if (status === 'running') {
71
+ interval = setInterval(refreshData, 60000); // 每60秒更新一次
72
+ }
73
+ return () => {
74
+ if (interval) clearInterval(interval);
 
 
 
 
 
 
 
 
 
 
 
 
75
  };
76
+ }, [refreshData, status]);
77
+
78
+ useEffect(() => {
79
+ setLocalOutput(output);
80
+ }, [output]);
81
+
82
+ const isLoading = navigation.state === 'submitting' || navigation.state === 'loading';
83
+
84
+ const handleAction = (action: 'start' | 'stop') => {
85
+ submit({ action }, { method: 'post' });
86
+ setTimeout(refreshData, 100);
87
+ };
88
+
89
+ return (
90
+ <div className="container mx-auto px-4 py-8 max-w-3xl">
91
+ <h1 className="text-3xl font-bold text-primary mb-6 pb-2 border-b-2 border-primary">SSHX控制面板</h1>
92
+ <div className="mb-6">
93
+ <p className="text-lg font-semibold">
94
+ 状态: <span className={`${status === 'running' ? 'text-green-600' : 'text-red-600'}`}>
95
+ {status === 'running' ? '运行中' : '已停止'}
96
+ </span>
97
+ </p>
98
+ </div>
99
+ <div className="flex space-x-4 mb-6">
100
+ <button
101
+ onClick={() => handleAction('start')}
102
+ className={`px-4 py-2 rounded-md text-white transition-colors duration-300 ${
103
+ status === 'running' || isLoading
104
+ ? 'bg-gray-400 cursor-not-allowed'
105
+ : 'bg-green-500 hover:bg-green-600'
106
+ }`}
107
+ disabled={status === 'running' || isLoading}
108
+ >
109
+ 启动SSHX
110
+ </button>
111
+ <button
112
+ onClick={() => handleAction('stop')}
113
+ className={`px-4 py-2 rounded-md text-white transition-colors duration-300 ${
114
+ status === 'stopped' || isLoading
115
+ ? 'bg-gray-400 cursor-not-allowed'
116
+ : 'bg-red-500 hover:bg-red-600'
117
+ }`}
118
+ disabled={status === 'stopped' || isLoading}
119
+ >
120
+ 停止SSHX
121
+ </button>
122
+ </div>
123
+ {status === 'running' && (
124
+ <div className="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
125
+ <p className="mb-2">
126
+ <strong className="font-semibold">链接:</strong>{' '}
127
+ {link ? (
128
+ <a href={link} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
129
+ {link}
130
+ </a>
131
+ ) : (
132
+ '暂不可用'
133
  )}
134
+ </p>
135
+ <p>
136
+ <strong className="font-semibold">Shell:</strong> {shell || '暂不可用'}
137
+ </p>
138
  </div>
139
+ )}
140
+ <h2 className="text-2xl font-semibold text-secondary mb-4">输出:</h2>
141
+ <pre className="bg-gray-100 p-4 rounded-md border border-gray-300 font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-96 overflow-y-auto">
142
+ {localOutput}
143
+ </pre>
144
+ </div>
145
+ );
146
  }