EasySmartControl / src /sshx.tsx
orztv
update
14fc0ec
raw
history blame
7.26 kB
import { useState, useEffect, useCallback } from 'react';
import { json, type ActionFunction, type LoaderFunction } from '@remix-run/node';
import { useLoaderData, Form, useSubmit, useNavigation } from '@remix-run/react';
import { spawn, type ChildProcess } from 'child_process';
type SshxStatus = 'running' | 'stopped';
interface LoaderData {
status: SshxStatus;
output: string;
link: string | null;
shell: string | null;
}
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');
if (action === 'start' && !sshxProcess) {
sshxProcess = spawn('/cloudide/workspace/hf-ssh/sshx/sshx', []);
sshxProcess.stdout?.on('data', handleProcessOutput);
sshxProcess.stderr?.on('data', handleProcessOutput);
sshxProcess.on('close', handleProcessClose);
return json({ status: 'started' });
} else if (action === 'stop' && sshxProcess) {
sshxProcess.kill();
handleProcessClose();
return json({ status: 'stopped' });
}
return json({ error: 'Invalid action' }, { 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';
return (
<div className="container">
<h1 className="title">SSHX Control</h1>
<p className="status">
Status: <span className={status === 'running' ? 'status-running' : 'status-stopped'}>{status}</span>
</p>
<div className="button-group">
<Form method="post">
<button
type="submit"
name="action"
value="start"
className={`button button-start ${status === 'running' || isLoading ? 'button-disabled' : ''}`}
disabled={status === 'running' || isLoading}
>
Start SSHX
</button>
</Form>
<Form method="post">
<button
type="submit"
name="action"
value="stop"
className={`button button-stop ${status === 'stopped' || isLoading ? 'button-disabled' : ''}`}
disabled={status === 'stopped' || isLoading}
>
Stop SSHX
</button>
</Form>
</div>
{status === 'running' && (
<div className="info-box">
<p>
<strong>Link:</strong>{' '}
{link ? (
<a href={link} target="_blank" rel="noopener noreferrer" className="link">
{link}
</a>
) : (
'Not available'
)}
</p>
<p>
<strong>Shell:</strong> {shell || 'Not available'}
</p>
</div>
)}
<h2 className="subtitle">Output:</h2>
<pre className="output">{localOutput}</pre>
<style dangerouslySetInnerHTML={{
__html: `
:root {
--primary-color: #4a90e2;
--secondary-color: #f5a623;
--background-color: #f9f9f9;
--text-color: #333;
--border-color: #e0e0e0;
}
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.title {
color: var(--primary-color);
border-bottom: 2px solid var(--primary-color);
padding-bottom: 10px;
margin-bottom: 20px;
}
.subtitle {
color: var(--secondary-color);
border-bottom: 1px solid var(--secondary-color);
padding-bottom: 5px;
margin-top: 20px;
}
.status {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
}
.status-running {
color: #4CAF50;
}
.status-stopped {
color: #f44336;
}
.button-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.button {
padding: 10px 20px;
font-size: 16px;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.button-start {
background-color: #4CAF50;
}
.button-stop {
background-color: #f44336;
}
.button-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info-box {
background-color: #e8f4fd;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border: 1px solid var(--border-color);
}
.link {
color: var(--primary-color);
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.output {
background-color: #f0f0f0;
padding: 15px;
border-radius: 5px;
white-space: pre-wrap;
word-wrap: break-word;
border: 1px solid var(--border-color);
font-family: 'Courier New', monospace;
font-size: 14px;
max-height: 400px;
overflow-y: auto;
}
`}} />
</div>
);
}