orztv commited on
Commit
982c207
·
1 Parent(s): 637945c
Files changed (3) hide show
  1. src/remix.sh +1 -0
  2. src/sshx.tsx +203 -36
  3. src/startup.sh +1 -4
src/remix.sh CHANGED
@@ -11,6 +11,7 @@ cd ${HOMEDIR}/${REMIX_NAME}
11
 
12
  # 安装依赖并构建
13
  pnpm install
 
14
  pnpm add @types/node @types/react @types/react-dom --save-dev
15
 
16
  # 复制 sshx 路由文件
 
11
 
12
  # 安装依赖并构建
13
  pnpm install
14
+ pnpm add react react-dom @remix-run/node @remix-run/react
15
  pnpm add @types/node @types/react @types/react-dom --save-dev
16
 
17
  # 复制 sshx 路由文件
src/sshx.tsx CHANGED
@@ -1,65 +1,232 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { json, ActionFunction } from '@remix-run/node';
3
- import { useLoaderData, Form, useSubmit } from '@remix-run/react';
4
  import { spawn, ChildProcess } from 'child_process';
5
 
 
 
 
 
 
 
 
 
 
 
 
6
  let sshxProcess: ChildProcess | null = null;
7
  let sshxOutput = '';
8
 
9
- export const loader = async () => {
10
- return json({ status: sshxProcess ? 'running' : 'stopped', output: sshxOutput });
 
 
 
 
11
  };
12
 
13
  export const action: ActionFunction = async ({ request }) => {
14
  const formData = await request.formData();
15
  const action = formData.get('action');
16
 
17
- if (action === 'start' && !sshxProcess) {
18
- sshxProcess = spawn('/home/pn/sshx/sshx', []);
19
- sshxProcess.stdout?.on('data', (data: Buffer) => {
20
- sshxOutput += data.toString();
21
- });
22
- sshxProcess.stderr?.on('data', (data: Buffer) => {
23
- sshxOutput += data.toString();
24
- });
25
- return json({ status: 'started' });
26
- } else if (action === 'stop' && sshxProcess) {
27
- sshxProcess.kill();
28
- sshxProcess = null;
29
- return json({ status: 'stopped' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
 
32
- return json({ error: 'Invalid action' }, { status: 400 });
33
  };
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  export default function Sshx() {
36
- const { status, output } = useLoaderData<{ status: string; output: string }>();
37
  const submit = useSubmit();
 
38
  const [localOutput, setLocalOutput] = useState(output);
39
 
 
 
 
 
40
  useEffect(() => {
41
- const interval = setInterval(() => {
42
- submit(null, { method: 'get', replace: true });
43
- }, 1000);
44
  return () => clearInterval(interval);
45
- }, [submit]);
46
 
47
  useEffect(() => {
48
  setLocalOutput(output);
49
  }, [output]);
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  return (
52
- <div>
53
- <h1>SSHX Control</h1>
54
- <p>Status: {status}</p>
55
- <Form method="post">
56
- <button type="submit" name="action" value="start">Start SSHX</button>
57
- </Form>
58
- <Form method="post">
59
- <button type="submit" name="action" value="stop">Stop SSHX</button>
60
- </Form>
61
- <h2>Output:</h2>
62
- <pre>{localOutput}</pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  </div>
64
  );
65
- }
 
1
+ import { json, ActionFunction, LoaderFunction } from '@remix-run/node';
2
+ import { useLoaderData, Form, useSubmit, useTransition } from '@remix-run/react';
3
+ import { useEffect, useState, useCallback } from 'react';
4
  import { spawn, ChildProcess } from 'child_process';
5
 
6
+ // Types
7
+ type SshxStatus = 'running' | 'stopped';
8
+
9
+ interface LoaderData {
10
+ status: SshxStatus;
11
+ output: string;
12
+ link: string | null;
13
+ shell: string | null;
14
+ }
15
+
16
+ // Server-side state
17
  let sshxProcess: ChildProcess | null = null;
18
  let sshxOutput = '';
19
 
20
+ export const loader: LoaderFunction = async () => {
21
+ const status: SshxStatus = sshxProcess ? 'running' : 'stopped';
22
+ const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/);
23
+ const shell = extractFromOutput(sshxOutput, /Shell:\s+([^\n]+)/);
24
+
25
+ return json<LoaderData>({ status, output: sshxOutput, link, shell });
26
  };
27
 
28
  export const action: ActionFunction = async ({ request }) => {
29
  const formData = await request.formData();
30
  const action = formData.get('action');
31
 
32
+ switch (action) {
33
+ case 'start':
34
+ if (!sshxProcess) {
35
+ try {
36
+ sshxProcess = spawn('/home/pn/sshx/sshx', [], { env: process.env });
37
+ sshxProcess.stdout?.on('data', handleProcessOutput);
38
+ sshxProcess.stderr?.on('data', handleProcessOutput);
39
+ sshxProcess.on('close', handleProcessClose);
40
+ sshxProcess.on('error', (error) => {
41
+ sshxOutput += `Error: ${error.message}\n`;
42
+ handleProcessClose();
43
+ });
44
+ return json({ status: 'started' });
45
+ } catch (error) {
46
+ return json({ error: 'Failed to start SSHX process' }, { status: 500 });
47
+ }
48
+ }
49
+ break;
50
+ case 'stop':
51
+ if (sshxProcess) {
52
+ sshxProcess.kill();
53
+ sshxProcess = null;
54
+ sshxOutput = '';
55
+ return json({ status: 'stopped' });
56
+ }
57
+ break;
58
+ default:
59
+ return json({ error: 'Invalid action' }, { status: 400 });
60
  }
61
 
62
+ return json({ error: 'Action not performed' }, { status: 400 });
63
  };
64
 
65
+ function handleProcessOutput(data: Buffer) {
66
+ sshxOutput += data.toString();
67
+ }
68
+
69
+ function handleProcessClose() {
70
+ sshxProcess = null;
71
+ sshxOutput += '\nSSHX process has ended.';
72
+ }
73
+
74
+ function extractFromOutput(output: string, regex: RegExp): string | null {
75
+ const match = output.match(regex);
76
+ return match ? match[1] : null;
77
+ }
78
+
79
  export default function Sshx() {
80
+ const { status, output, link, shell } = useLoaderData<LoaderData>();
81
  const submit = useSubmit();
82
+ const transition = useTransition();
83
  const [localOutput, setLocalOutput] = useState(output);
84
 
85
+ const refreshData = useCallback(() => {
86
+ submit(null, { method: 'get', replace: true });
87
+ }, [submit]);
88
+
89
  useEffect(() => {
90
+ const interval = setInterval(refreshData, 1000);
 
 
91
  return () => clearInterval(interval);
92
+ }, [refreshData]);
93
 
94
  useEffect(() => {
95
  setLocalOutput(output);
96
  }, [output]);
97
 
98
+ const isLoading = transition.state === 'submitting' || transition.state === 'loading';
99
+
100
+ const styles = {
101
+ container: {
102
+ fontFamily: 'Arial, sans-serif',
103
+ maxWidth: '800px',
104
+ margin: '0 auto',
105
+ padding: '20px',
106
+ },
107
+ header: {
108
+ color: '#333',
109
+ borderBottom: '2px solid #333',
110
+ paddingBottom: '10px',
111
+ },
112
+ status: {
113
+ fontSize: '18px',
114
+ fontWeight: 'bold' as const,
115
+ },
116
+ statusRunning: {
117
+ color: 'green',
118
+ },
119
+ statusStopped: {
120
+ color: 'red',
121
+ },
122
+ buttonGroup: {
123
+ display: 'flex',
124
+ gap: '10px',
125
+ marginBottom: '20px',
126
+ },
127
+ button: {
128
+ padding: '10px 20px',
129
+ fontSize: '16px',
130
+ color: 'white',
131
+ border: 'none',
132
+ borderRadius: '5px',
133
+ cursor: 'pointer',
134
+ },
135
+ startButton: {
136
+ backgroundColor: '#4CAF50',
137
+ },
138
+ stopButton: {
139
+ backgroundColor: '#f44336',
140
+ },
141
+ disabledButton: {
142
+ opacity: 0.5,
143
+ cursor: 'not-allowed',
144
+ },
145
+ infoBox: {
146
+ backgroundColor: '#f0f0f0',
147
+ padding: '15px',
148
+ borderRadius: '5px',
149
+ marginBottom: '20px',
150
+ },
151
+ link: {
152
+ color: '#1a0dab',
153
+ },
154
+ outputHeader: {
155
+ color: '#333',
156
+ borderBottom: '1px solid #333',
157
+ paddingBottom: '5px',
158
+ },
159
+ output: {
160
+ backgroundColor: '#f0f0f0',
161
+ padding: '15px',
162
+ borderRadius: '5px',
163
+ whiteSpace: 'pre-wrap' as const,
164
+ wordWrap: 'break-word' as const,
165
+ },
166
+ };
167
+
168
  return (
169
+ <div style={styles.container}>
170
+ <h1 style={styles.header}>SSHX Control</h1>
171
+ <p style={styles.status}>
172
+ Status:{' '}
173
+ <span style={status === 'running' ? styles.statusRunning : styles.statusStopped}>
174
+ {status}
175
+ </span>
176
+ </p>
177
+ <div style={styles.buttonGroup}>
178
+ <Form method="post">
179
+ <button
180
+ type="submit"
181
+ name="action"
182
+ value="start"
183
+ style={{
184
+ ...styles.button,
185
+ ...styles.startButton,
186
+ ...(status === 'running' || isLoading ? styles.disabledButton : {}),
187
+ }}
188
+ disabled={status === 'running' || isLoading}
189
+ aria-disabled={status === 'running' || isLoading}
190
+ >
191
+ Start SSHX
192
+ </button>
193
+ </Form>
194
+ <Form method="post">
195
+ <button
196
+ type="submit"
197
+ name="action"
198
+ value="stop"
199
+ style={{
200
+ ...styles.button,
201
+ ...styles.stopButton,
202
+ ...(status === 'stopped' || isLoading ? styles.disabledButton : {}),
203
+ }}
204
+ disabled={status === 'stopped' || isLoading}
205
+ aria-disabled={status === 'stopped' || isLoading}
206
+ >
207
+ Stop SSHX
208
+ </button>
209
+ </Form>
210
+ </div>
211
+ {status === 'running' && (
212
+ <div style={styles.infoBox}>
213
+ <p>
214
+ <strong>Link:</strong>{' '}
215
+ {link ? (
216
+ <a href={link} target="_blank" rel="noopener noreferrer" style={styles.link}>
217
+ {link}
218
+ </a>
219
+ ) : (
220
+ 'Not available'
221
+ )}
222
+ </p>
223
+ <p>
224
+ <strong>Shell:</strong> {shell || 'Not available'}
225
+ </p>
226
+ </div>
227
+ )}
228
+ <h2 style={styles.outputHeader}>Output:</h2>
229
+ <pre style={styles.output}>{localOutput}</pre>
230
  </div>
231
  );
232
+ }
src/startup.sh CHANGED
@@ -4,7 +4,4 @@
4
  cd ${HOMEDIR}/${REMIX_NAME}
5
  PORT=$PORT pm2 start pnpm --name "remix" -- start
6
 
7
-
8
- # PM2 会保持前台运行,无需额外的命令来保持容器运行
9
-
10
- $HOMEDIR/sshx/sshx
 
4
  cd ${HOMEDIR}/${REMIX_NAME}
5
  PORT=$PORT pm2 start pnpm --name "remix" -- start
6
 
7
+ pm2 save