EasySmartControl / src /sshx.tsx
orztv
update
d4f6714
raw
history blame
7.59 kB
import { json, ActionFunction, LoaderFunction } from '@remix-run/node';
import { useLoaderData, Form, useSubmit, useNavigation } from '@remix-run/react';
import { useEffect, useState, useCallback } from 'react';
import { spawn, ChildProcess } from 'child_process';
// Types
type SshxStatus = 'running' | 'stopped';
interface LoaderData {
status: SshxStatus;
output: string;
link: string | null;
shell: string | null;
}
// Server-side state
let sshxProcess: ChildProcess | null = null;
let sshxOutput = '';
export const loader: LoaderFunction = async () => {
const status: SshxStatus = sshxProcess ? 'running' : 'stopped';
const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/);
const shell = extractFromOutput(sshxOutput, /Shell:\s+([^\n]+)/);
return json<LoaderData>({ status, output: sshxOutput, link, shell });
};
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const action = formData.get('action');
switch (action) {
case 'start':
if (!sshxProcess) {
try {
sshxProcess = spawn('/home/pn/sshx/sshx', [], { env: process.env });
sshxProcess.stdout?.on('data', handleProcessOutput);
sshxProcess.stderr?.on('data', handleProcessOutput);
sshxProcess.on('close', handleProcessClose);
sshxProcess.on('error', (error) => {
sshxOutput += `Error: ${error.message}\n`;
handleProcessClose();
});
return json({ status: 'started' });
} catch (error) {
return json({ error: 'Failed to start SSHX process' }, { status: 500 });
}
}
break;
case 'stop':
if (sshxProcess) {
sshxProcess.kill();
sshxProcess = null;
sshxOutput = '';
return json({ status: 'stopped' });
}
break;
default:
return json({ error: 'Invalid action' }, { status: 400 });
}
return json({ error: 'Action not performed' }, { status: 400 });
};
function handleProcessOutput(data: Buffer) {
sshxOutput += data.toString();
}
function handleProcessClose() {
sshxProcess = null;
sshxOutput += '\nSSHX process has ended.';
}
function extractFromOutput(output: string, regex: RegExp): string | null {
const match = output.match(regex);
return match ? match[1] : null;
}
export default function Sshx() {
const { status, output, link, shell } = useLoaderData<LoaderData>();
const submit = useSubmit();
const navigation = useNavigation();
const [localOutput, setLocalOutput] = useState(output);
const refreshData = useCallback(() => {
submit(null, { method: 'get', replace: true });
}, [submit]);
useEffect(() => {
const interval = setInterval(refreshData, 1000);
return () => clearInterval(interval);
}, [refreshData]);
useEffect(() => {
setLocalOutput(output);
}, [output]);
const isLoading = navigation.state === 'submitting' || navigation.state === 'loading';
const styles = {
container: {
fontFamily: 'Arial, sans-serif',
maxWidth: '800px',
margin: '0 auto',
padding: '20px',
},
header: {
color: '#333',
borderBottom: '2px solid #333',
paddingBottom: '10px',
},
status: {
fontSize: '18px',
fontWeight: 'bold' as const,
},
statusRunning: {
color: 'green',
},
statusStopped: {
color: 'red',
},
buttonGroup: {
display: 'flex',
gap: '10px',
marginBottom: '20px',
},
button: {
padding: '10px 20px',
fontSize: '16px',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
},
startButton: {
backgroundColor: '#4CAF50',
},
stopButton: {
backgroundColor: '#f44336',
},
disabledButton: {
opacity: 0.5,
cursor: 'not-allowed',
},
infoBox: {
backgroundColor: '#f0f0f0',
padding: '15px',
borderRadius: '5px',
marginBottom: '20px',
},
link: {
color: '#1a0dab',
},
outputHeader: {
color: '#333',
borderBottom: '1px solid #333',
paddingBottom: '5px',
},
output: {
backgroundColor: '#f0f0f0',
padding: '15px',
borderRadius: '5px',
whiteSpace: 'pre-wrap' as const,
wordWrap: 'break-word' as const,
},
};
return (
<div style={styles.container}>
<h1 style={styles.header}>SSHX Control</h1>
<p style={styles.status}>
Status:{' '}
<span style={status === 'running' ? styles.statusRunning : styles.statusStopped}>
{status}
</span>
</p>
<div style={styles.buttonGroup}>
<Form method="post">
<button
type="submit"
name="action"
value="start"
style={{
...styles.button,
...styles.startButton,
...(status === 'running' || isLoading ? styles.disabledButton : {}),
}}
disabled={status === 'running' || isLoading}
aria-disabled={status === 'running' || isLoading}
>
Start SSHX
</button>
</Form>
<Form method="post">
<button
type="submit"
name="action"
value="stop"
style={{
...styles.button,
...styles.stopButton,
...(status === 'stopped' || isLoading ? styles.disabledButton : {}),
}}
disabled={status === 'stopped' || isLoading}
aria-disabled={status === 'stopped' || isLoading}
>
Stop SSHX
</button>
</Form>
</div>
{status === 'running' && (
<div style={styles.infoBox}>
<p>
<strong>Link:</strong>{' '}
{link ? (
<a href={link} target="_blank" rel="noopener noreferrer" style={styles.link}>
{link}
</a>
) : (
'Not available'
)}
</p>
<p>
<strong>Shell:</strong> {shell || 'Not available'}
</p>
</div>
)}
<h2 style={styles.outputHeader}>Output:</h2>
<pre style={styles.output}>{localOutput}</pre>
</div>
);
}