orztv commited on
Commit
49aaab5
·
1 Parent(s): d4f6714
Files changed (1) hide show
  1. src/sshx.tsx +45 -191
src/sshx.tsx CHANGED
@@ -1,232 +1,86 @@
1
- import { json, ActionFunction, LoaderFunction } from '@remix-run/node';
2
- import { useLoaderData, Form, useSubmit, useNavigation } 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 navigation = useNavigation();
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 = navigation.state === 'submitting' || navigation.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
- }
 
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
+ sshxOutput = '';
30
+ return json({ status: 'stopped' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
 
33
+ return json({ error: 'Invalid action' }, { status: 400 });
34
  };
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  export default function Sshx() {
37
+ const { status, output } = useLoaderData<{ status: string; output: string }>();
38
  const submit = useSubmit();
 
39
  const [localOutput, setLocalOutput] = useState(output);
40
 
 
 
 
 
41
  useEffect(() => {
42
+ const interval = setInterval(() => {
43
+ submit(null, { method: 'get', replace: true });
44
+ }, 1000);
45
  return () => clearInterval(interval);
46
+ }, [submit]);
47
 
48
  useEffect(() => {
49
  setLocalOutput(output);
50
  }, [output]);
51
 
52
+ const getLink = () => {
53
+ const match = localOutput.match(/Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/);
54
+ return match ? match[1] : null;
55
+ };
56
 
57
+ const getShell = () => {
58
+ const match = localOutput.match(/Shell:\s+([^\n]+)/);
59
+ return match ? match[1] : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  };
61
 
62
  return (
63
+ <div style={{ fontFamily: 'Arial, sans-serif', maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
64
+ <h1 style={{ color: '#333', borderBottom: '2px solid #333', paddingBottom: '10px' }}>SSHX Control</h1>
65
+ <p style={{ fontSize: '18px', fontWeight: 'bold' }}>
66
+ Status: <span style={{ color: status === 'running' ? 'green' : 'red' }}>{status}</span>
 
 
 
67
  </p>
68
+ <div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
69
  <Form method="post">
70
+ <button type="submit" name="action" value="start" style={{ padding: '10px 20px', fontSize: '16px', backgroundColor: '#4CAF50', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }} disabled={status === 'running'}>Start SSHX</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </Form>
72
  <Form method="post">
73
+ <button type="submit" name="action" value="stop" style={{ padding: '10px 20px', fontSize: '16px', backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }} disabled={status === 'stopped'}>Stop SSHX</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  </Form>
75
  </div>
76
  {status === 'running' && (
77
+ <div style={{ backgroundColor: '#f0f0f0', padding: '15px', borderRadius: '5px', marginBottom: '20px' }}>
78
+ <p style={{ margin: '5px 0' }}><strong>Link:</strong> <a href={getLink() || '#'} target="_blank" rel="noopener noreferrer" style={{ color: '#1a0dab' }}>{getLink()}</a></p>
79
+ <p style={{ margin: '5px 0' }}><strong>Shell:</strong> {getShell()}</p>
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
  )}
82
+ <h2 style={{ color: '#333', borderBottom: '1px solid #333', paddingBottom: '5px' }}>Output:</h2>
83
+ <pre style={{ backgroundColor: '#f0f0f0', padding: '15px', borderRadius: '5px', whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>{localOutput}</pre>
84
  </div>
85
  );
86
+ }