diff --git a/app/api/supervisor/groups/[name]/signal/route.ts b/app/api/supervisor/groups/[name]/signal/route.ts new file mode 100644 index 0000000..f208aec --- /dev/null +++ b/app/api/supervisor/groups/[name]/signal/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +interface RouteParams { + params: Promise<{ name: string }>; +} + +// POST - Send signal to all processes in a group +export async function POST(request: NextRequest, { params }: RouteParams) { + try { + const { name } = await params; + const body = await request.json(); + const { signal } = body; + + if (!signal) { + return NextResponse.json( + { error: 'Signal is required' }, + { status: 400 } + ); + } + + const client = createSupervisorClient(); + const results = await client.signalProcessGroup(name, signal); + + return NextResponse.json({ + success: true, + message: `Signal ${signal} sent to group ${name}`, + results, + }); + } catch (error: any) { + console.error('Supervisor signal process group error:', error); + return NextResponse.json( + { error: error.message || 'Failed to send signal to process group' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/[name]/signal/route.ts b/app/api/supervisor/processes/[name]/signal/route.ts new file mode 100644 index 0000000..5e41c15 --- /dev/null +++ b/app/api/supervisor/processes/[name]/signal/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +interface RouteParams { + params: Promise<{ name: string }>; +} + +// POST - Send signal to a process +export async function POST(request: NextRequest, { params }: RouteParams) { + try { + const { name } = await params; + const body = await request.json(); + const { signal } = body; + + if (!signal) { + return NextResponse.json( + { error: 'Signal is required' }, + { status: 400 } + ); + } + + const client = createSupervisorClient(); + const result = await client.signalProcess(name, signal); + + return NextResponse.json({ + success: result, + message: `Signal ${signal} sent to ${name}`, + }); + } catch (error: any) { + console.error('Supervisor signal process error:', error); + return NextResponse.json( + { error: error.message || 'Failed to send signal to process' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/signal-all/route.ts b/app/api/supervisor/processes/signal-all/route.ts new file mode 100644 index 0000000..914b32a --- /dev/null +++ b/app/api/supervisor/processes/signal-all/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +// POST - Send signal to all processes +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { signal } = body; + + if (!signal) { + return NextResponse.json( + { error: 'Signal is required' }, + { status: 400 } + ); + } + + const client = createSupervisorClient(); + const results = await client.signalAllProcesses(signal); + + return NextResponse.json({ + success: true, + message: `Signal ${signal} sent to all processes`, + results, + }); + } catch (error: any) { + console.error('Supervisor signal all processes error:', error); + return NextResponse.json( + { error: error.message || 'Failed to send signal to all processes' }, + { status: 500 } + ); + } +} diff --git a/components/process/ProcessCard.tsx b/components/process/ProcessCard.tsx index 81e24e9..25793f3 100644 --- a/components/process/ProcessCard.tsx +++ b/components/process/ProcessCard.tsx @@ -1,11 +1,13 @@ 'use client'; -import { Play, Square, RotateCw, Activity } from 'lucide-react'; +import { useState } from 'react'; +import { Play, Square, RotateCw, Activity, Zap } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { ProcessInfo, ProcessStateCode, getProcessStateClass, formatUptime, canStartProcess, canStopProcess } from '@/lib/supervisor/types'; import { useStartProcess, useStopProcess, useRestartProcess } from '@/lib/hooks/useSupervisor'; +import { SignalSender } from './SignalSender'; import { cn } from '@/lib/utils/cn'; interface ProcessCardProps { @@ -15,6 +17,7 @@ interface ProcessCardProps { } export function ProcessCard({ process, isSelected = false, onSelectionChange }: ProcessCardProps) { + const [showSignalModal, setShowSignalModal] = useState(false); const startMutation = useStartProcess(); const stopMutation = useStopProcess(); const restartMutation = useRestartProcess(); @@ -131,6 +134,15 @@ export function ProcessCard({ process, isSelected = false, onSelectionChange }: > + {/* Description */} @@ -140,6 +152,11 @@ export function ProcessCard({ process, isSelected = false, onSelectionChange }:

)} + + {/* Signal Modal */} + {showSignalModal && ( + setShowSignalModal(false)} /> + )} ); } diff --git a/components/process/SignalSender.tsx b/components/process/SignalSender.tsx new file mode 100644 index 0000000..d94293b --- /dev/null +++ b/components/process/SignalSender.tsx @@ -0,0 +1,154 @@ +'use client'; + +import { useState } from 'react'; +import { useSignalProcess } from '@/lib/hooks/useSupervisor'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { X, AlertTriangle } from 'lucide-react'; +import { cn } from '@/lib/utils/cn'; + +interface SignalSenderProps { + processName: string; + onClose: () => void; +} + +const COMMON_SIGNALS = [ + { value: 'HUP', label: 'HUP (1)', description: 'Hangup - reload configuration', dangerous: false }, + { value: 'INT', label: 'INT (2)', description: 'Interrupt - graceful shutdown', dangerous: false }, + { value: 'QUIT', label: 'QUIT (3)', description: 'Quit', dangerous: false }, + { value: 'TERM', label: 'TERM (15)', description: 'Terminate - graceful shutdown', dangerous: true }, + { value: 'KILL', label: 'KILL (9)', description: 'Kill - immediate termination', dangerous: true }, + { value: 'USR1', label: 'USR1 (10)', description: 'User-defined signal 1', dangerous: false }, + { value: 'USR2', label: 'USR2 (12)', description: 'User-defined signal 2', dangerous: false }, +]; + +export function SignalSender({ processName, onClose }: SignalSenderProps) { + const [selectedSignal, setSelectedSignal] = useState(''); + const [customSignal, setCustomSignal] = useState(''); + const [showConfirm, setShowConfirm] = useState(false); + + const signalMutation = useSignalProcess(); + + const handleSendSignal = () => { + const signal = selectedSignal || customSignal; + if (!signal) return; + + const isDangerous = ['TERM', 'KILL', 'QUIT'].includes(signal.toUpperCase()); + + if (isDangerous && !showConfirm) { + setShowConfirm(true); + return; + } + + signalMutation.mutate( + { name: processName, signal }, + { + onSuccess: () => { + onClose(); + }, + } + ); + }; + + const signal = selectedSignal || customSignal; + const isDangerous = signal && ['TERM', 'KILL', 'QUIT'].includes(signal.toUpperCase()); + + return ( +
+ + +
+
+ Send Signal + + Send Unix signal to {processName} + +
+ +
+
+ + + {/* Common Signals */} +
+ +
+ {COMMON_SIGNALS.map((sig) => ( + + ))} +
+
+ + {/* Custom Signal */} +
+ + { + setCustomSignal(e.target.value.toUpperCase()); + setSelectedSignal(''); + setShowConfirm(false); + }} + className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + /> +
+ + {/* Confirmation Warning */} + {isDangerous && showConfirm && ( +
+
+ +
+

Warning: Dangerous Signal

+

+ Signal {signal} may terminate or kill the process. + Are you sure you want to continue? +

+
+
+
+ )} + + {/* Actions */} +
+ + +
+
+
+
+ ); +} diff --git a/lib/hooks/useSupervisor.ts b/lib/hooks/useSupervisor.ts index 9c162d7..9d074c3 100644 --- a/lib/hooks/useSupervisor.ts +++ b/lib/hooks/useSupervisor.ts @@ -558,3 +558,90 @@ export function useRemoveProcessGroup() { }, }); } + +// Signal Operations + +async function signalProcess(name: string, signal: string): Promise<{ success: boolean; message: string }> { + const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/signal`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ signal }), + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to send signal'); + } + return response.json(); +} + +async function signalProcessGroup(name: string, signal: string): Promise<{ success: boolean; message: string; results: any[] }> { + const response = await fetch(`/api/supervisor/groups/${encodeURIComponent(name)}/signal`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ signal }), + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to send signal to group'); + } + return response.json(); +} + +async function signalAllProcesses(signal: string): Promise<{ success: boolean; message: string; results: any[] }> { + const response = await fetch('/api/supervisor/processes/signal-all', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ signal }), + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to send signal to all processes'); + } + return response.json(); +} + +export function useSignalProcess() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ name, signal }: { name: string; signal: string }) => signalProcess(name, signal), + onSuccess: (data, variables) => { + toast.success(data.message); + queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) }); + queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); + }, + onError: (error: Error) => { + toast.error(`Failed to send signal: ${error.message}`); + }, + }); +} + +export function useSignalProcessGroup() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ name, signal }: { name: string; signal: string }) => signalProcessGroup(name, signal), + onSuccess: (data) => { + toast.success(data.message); + queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); + }, + onError: (error: Error) => { + toast.error(`Failed to send signal to group: ${error.message}`); + }, + }); +} + +export function useSignalAllProcesses() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (signal: string) => signalAllProcesses(signal), + onSuccess: (data) => { + toast.success(data.message); + queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); + }, + onError: (error: Error) => { + toast.error(`Failed to send signal to all processes: ${error.message}`); + }, + }); +}