orztv commited on
Commit
0b02a00
·
1 Parent(s): cbdb202
Files changed (2) hide show
  1. src/remix.sh +2 -0
  2. src/sshx.tsx +142 -53
src/remix.sh CHANGED
@@ -16,6 +16,8 @@ pnpm add @types/node @types/react @types/react-dom --save-dev
16
 
17
  # 复制 sshx 路由文件
18
  cp ${HOMEDIR}/sshx.tsx ${HOMEDIR}/${REMIX_NAME}/app/routes/sshx.tsx
 
 
19
  pnpm run build
20
 
21
  # 返回 HOMEDIR
 
16
 
17
  # 复制 sshx 路由文件
18
  cp ${HOMEDIR}/sshx.tsx ${HOMEDIR}/${REMIX_NAME}/app/routes/sshx.tsx
19
+
20
+ # 构建应用
21
  pnpm run build
22
 
23
  # 返回 HOMEDIR
src/sshx.tsx CHANGED
@@ -1,65 +1,154 @@
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
+ /// <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
  }