Spaces:
Paused
Paused
| 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> | |
| ); | |
| } |