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