orztv commited on
Commit
14fc0ec
·
1 Parent(s): 0b02a00
Files changed (1) hide show
  1. src/sshx.tsx +213 -124
src/sshx.tsx CHANGED
@@ -1,154 +1,243 @@
1
- /// <reference types="node" />
2
-
3
- import React, { useState, useEffect, useCallback } from 'react';
4
  import { json, type ActionFunction, type LoaderFunction } from '@remix-run/node';
5
  import { useLoaderData, Form, useSubmit, useNavigation } from '@remix-run/react';
6
  import { spawn, type ChildProcess } from 'child_process';
7
 
8
- // 定义类型
9
  type SshxStatus = 'running' | 'stopped';
10
 
11
  interface LoaderData {
12
- status: SshxStatus;
13
- output: string;
14
- link: string | null;
15
- shell: string | null;
16
  }
17
 
18
- // 服务器端状态
19
  let sshxProcess: ChildProcess | null = null;
20
  let sshxOutput = '';
21
 
22
  export const loader: LoaderFunction = async () => {
23
- const status: SshxStatus = sshxProcess ? 'running' : 'stopped';
24
- const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/);
25
- const shell = extractFromOutput(sshxOutput, /Shell:\s+([^\n]+)/);
26
- return json<LoaderData>({ status, output: sshxOutput, link, shell });
27
  };
28
 
29
  export const action: ActionFunction = async ({ request }) => {
30
- const formData = await request.formData();
31
- const action = formData.get('action');
32
-
33
- if (action === 'start' && !sshxProcess) {
34
- sshxProcess = spawn('/home/pn/sshx/sshx', []);
35
- sshxProcess.stdout?.on('data', handleProcessOutput);
36
- sshxProcess.stderr?.on('data', handleProcessOutput);
37
- sshxProcess.on('close', handleProcessClose);
38
- return json({ status: 'started' });
39
- } else if (action === 'stop' && sshxProcess) {
40
- sshxProcess.kill();
41
- handleProcessClose();
42
- return json({ status: 'stopped' });
43
- }
44
-
45
- return json({ error: 'Invalid action' }, { status: 400 });
46
  };
47
 
48
  function handleProcessOutput(data: Buffer) {
49
- sshxOutput += data.toString();
50
  }
51
 
52
  function handleProcessClose() {
53
- sshxProcess = null;
54
- sshxOutput += '\nSSHX process has ended.';
55
  }
56
 
57
  function extractFromOutput(output: string, regex: RegExp): string | null {
58
- const match = output.match(regex);
59
- return match ? match[1] : null;
60
  }
61
 
62
  export default function Sshx() {
63
- const { status, output, link, shell } = useLoaderData<LoaderData>();
64
- const submit = useSubmit();
65
- const navigation = useNavigation();
66
- const [localOutput, setLocalOutput] = useState(output);
67
-
68
- const refreshData = useCallback(() => {
69
- submit(null, { method: 'get', replace: true });
70
- }, [submit]);
71
-
72
- useEffect(() => {
73
- const interval = setInterval(refreshData, 1000);
74
- return () => clearInterval(interval);
75
- }, [refreshData]);
76
-
77
- useEffect(() => {
78
- setLocalOutput(output);
79
- }, [output]);
80
-
81
- const isLoading = navigation.state === 'submitting' || navigation.state === 'loading';
82
-
83
- return (
84
- <div style={{ fontFamily: 'Arial, sans-serif', maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
85
- <h1 style={{ color: '#333', borderBottom: '2px solid #333', paddingBottom: '10px' }}>SSHX Control</h1>
86
- <p style={{ fontSize: '18px', fontWeight: 'bold' }}>
87
- Status: <span style={{ color: status === 'running' ? 'green' : 'red' }}>{status}</span>
88
- </p>
89
- <div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
90
- <Form method="post">
91
- <button
92
- type="submit"
93
- name="action"
94
- value="start"
95
- style={{
96
- padding: '10px 20px',
97
- fontSize: '16px',
98
- backgroundColor: '#4CAF50',
99
- color: 'white',
100
- border: 'none',
101
- borderRadius: '5px',
102
- cursor: 'pointer',
103
- opacity: status === 'running' || isLoading ? 0.5 : 1
104
- }}
105
- disabled={status === 'running' || isLoading}
106
- >
107
- Start SSHX
108
- </button>
109
- </Form>
110
- <Form method="post">
111
- <button
112
- type="submit"
113
- name="action"
114
- value="stop"
115
- style={{
116
- padding: '10px 20px',
117
- fontSize: '16px',
118
- backgroundColor: '#f44336',
119
- color: 'white',
120
- border: 'none',
121
- borderRadius: '5px',
122
- cursor: 'pointer',
123
- opacity: status === 'stopped' || isLoading ? 0.5 : 1
124
- }}
125
- disabled={status === 'stopped' || isLoading}
126
- >
127
- Stop SSHX
128
- </button>
129
- </Form>
130
- </div>
131
- {status === 'running' && (
132
- <div style={{ backgroundColor: '#f0f0f0', padding: '15px', borderRadius: '5px', marginBottom: '20px' }}>
133
- <p style={{ margin: '5px 0' }}>
134
- <strong>Link:</strong>{' '}
135
- {link ? (
136
- <a href={link} target="_blank" rel="noopener noreferrer" style={{ color: '#1a0dab' }}>
137
- {link}
138
- </a>
139
- ) : (
140
- 'Not available'
141
  )}
142
- </p>
143
- <p style={{ margin: '5px 0' }}>
144
- <strong>Shell:</strong> {shell || 'Not available'}
145
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  </div>
147
- )}
148
- <h2 style={{ color: '#333', borderBottom: '1px solid #333', paddingBottom: '5px' }}>Output:</h2>
149
- <pre style={{ backgroundColor: '#f0f0f0', padding: '15px', borderRadius: '5px', whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
150
- {localOutput}
151
- </pre>
152
- </div>
153
- );
154
- }
 
1
+ import { useState, useEffect, useCallback } from 'react';
 
 
2
  import { json, type ActionFunction, type LoaderFunction } from '@remix-run/node';
3
  import { useLoaderData, Form, useSubmit, useNavigation } from '@remix-run/react';
4
  import { spawn, type ChildProcess } from 'child_process';
5
 
 
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
  export const loader: LoaderFunction = async () => {
19
+ const status: SshxStatus = sshxProcess ? 'running' : 'stopped';
20
+ const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/);
21
+ const shell = extractFromOutput(sshxOutput, /Shell:\s+([^\n]+)/);
22
+ return json<LoaderData>({ status, output: sshxOutput, link, shell });
23
  };
24
 
25
  export const action: ActionFunction = async ({ request }) => {
26
+ const formData = await request.formData();
27
+ const action = formData.get('action');
28
+
29
+ if (action === 'start' && !sshxProcess) {
30
+ sshxProcess = spawn('/cloudide/workspace/hf-ssh/sshx/sshx', []);
31
+ sshxProcess.stdout?.on('data', handleProcessOutput);
32
+ sshxProcess.stderr?.on('data', handleProcessOutput);
33
+ sshxProcess.on('close', handleProcessClose);
34
+ return json({ status: 'started' });
35
+ } else if (action === 'stop' && sshxProcess) {
36
+ sshxProcess.kill();
37
+ handleProcessClose();
38
+ return json({ status: 'stopped' });
39
+ }
40
+
41
+ return json({ error: 'Invalid action' }, { status: 400 });
42
  };
43
 
44
  function handleProcessOutput(data: Buffer) {
45
+ sshxOutput += data.toString();
46
  }
47
 
48
  function handleProcessClose() {
49
+ sshxProcess = null;
50
+ sshxOutput += '\nSSHX process has ended.';
51
  }
52
 
53
  function extractFromOutput(output: string, regex: RegExp): string | null {
54
+ const match = output.match(regex);
55
+ return match ? match[1] : null;
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
+ const interval = setInterval(refreshData, 1000);
70
+ return () => clearInterval(interval);
71
+ }, [refreshData]);
72
+
73
+ useEffect(() => {
74
+ setLocalOutput(output);
75
+ }, [output]);
76
+
77
+ const isLoading = navigation.state === 'submitting' || navigation.state === 'loading';
78
+
79
+ return (
80
+ <div className="container">
81
+ <h1 className="title">SSHX Control</h1>
82
+ <p className="status">
83
+ Status: <span className={status === 'running' ? 'status-running' : 'status-stopped'}>{status}</span>
84
+ </p>
85
+ <div className="button-group">
86
+ <Form method="post">
87
+ <button
88
+ type="submit"
89
+ name="action"
90
+ value="start"
91
+ className={`button button-start ${status === 'running' || isLoading ? 'button-disabled' : ''}`}
92
+ disabled={status === 'running' || isLoading}
93
+ >
94
+ Start SSHX
95
+ </button>
96
+ </Form>
97
+ <Form method="post">
98
+ <button
99
+ type="submit"
100
+ name="action"
101
+ value="stop"
102
+ className={`button button-stop ${status === 'stopped' || isLoading ? 'button-disabled' : ''}`}
103
+ disabled={status === 'stopped' || isLoading}
104
+ >
105
+ Stop SSHX
106
+ </button>
107
+ </Form>
108
+ </div>
109
+ {status === 'running' && (
110
+ <div className="info-box">
111
+ <p>
112
+ <strong>Link:</strong>{' '}
113
+ {link ? (
114
+ <a href={link} target="_blank" rel="noopener noreferrer" className="link">
115
+ {link}
116
+ </a>
117
+ ) : (
118
+ 'Not available'
119
+ )}
120
+ </p>
121
+ <p>
122
+ <strong>Shell:</strong> {shell || 'Not available'}
123
+ </p>
124
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
125
  )}
126
+ <h2 className="subtitle">Output:</h2>
127
+ <pre className="output">{localOutput}</pre>
128
+
129
+ <style dangerouslySetInnerHTML={{
130
+ __html: `
131
+ :root {
132
+ --primary-color: #4a90e2;
133
+ --secondary-color: #f5a623;
134
+ --background-color: #f9f9f9;
135
+ --text-color: #333;
136
+ --border-color: #e0e0e0;
137
+ }
138
+
139
+ body {
140
+ font-family: 'Arial', sans-serif;
141
+ line-height: 1.6;
142
+ color: var(--text-color);
143
+ background-color: var(--background-color);
144
+ margin: 0;
145
+ padding: 0;
146
+ }
147
+
148
+ .container {
149
+ max-width: 800px;
150
+ margin: 0 auto;
151
+ padding: 20px;
152
+ }
153
+
154
+ .title {
155
+ color: var(--primary-color);
156
+ border-bottom: 2px solid var(--primary-color);
157
+ padding-bottom: 10px;
158
+ margin-bottom: 20px;
159
+ }
160
+
161
+ .subtitle {
162
+ color: var(--secondary-color);
163
+ border-bottom: 1px solid var(--secondary-color);
164
+ padding-bottom: 5px;
165
+ margin-top: 20px;
166
+ }
167
+
168
+ .status {
169
+ font-size: 18px;
170
+ font-weight: bold;
171
+ margin-bottom: 20px;
172
+ }
173
+
174
+ .status-running {
175
+ color: #4CAF50;
176
+ }
177
+
178
+ .status-stopped {
179
+ color: #f44336;
180
+ }
181
+
182
+ .button-group {
183
+ display: flex;
184
+ gap: 10px;
185
+ margin-bottom: 20px;
186
+ }
187
+
188
+ .button {
189
+ padding: 10px 20px;
190
+ font-size: 16px;
191
+ color: white;
192
+ border: none;
193
+ border-radius: 5px;
194
+ cursor: pointer;
195
+ transition: background-color 0.3s ease;
196
+ }
197
+
198
+ .button-start {
199
+ background-color: #4CAF50;
200
+ }
201
+
202
+ .button-stop {
203
+ background-color: #f44336;
204
+ }
205
+
206
+ .button-disabled {
207
+ opacity: 0.5;
208
+ cursor: not-allowed;
209
+ }
210
+
211
+ .info-box {
212
+ background-color: #e8f4fd;
213
+ padding: 15px;
214
+ border-radius: 5px;
215
+ margin-bottom: 20px;
216
+ border: 1px solid var(--border-color);
217
+ }
218
+
219
+ .link {
220
+ color: var(--primary-color);
221
+ text-decoration: none;
222
+ }
223
+
224
+ .link:hover {
225
+ text-decoration: underline;
226
+ }
227
+
228
+ .output {
229
+ background-color: #f0f0f0;
230
+ padding: 15px;
231
+ border-radius: 5px;
232
+ white-space: pre-wrap;
233
+ word-wrap: break-word;
234
+ border: 1px solid var(--border-color);
235
+ font-family: 'Courier New', monospace;
236
+ font-size: 14px;
237
+ max-height: 400px;
238
+ overflow-y: auto;
239
+ }
240
+ `}} />
241
  </div>
242
+ );
243
+ }