Skip to content
13 changes: 2 additions & 11 deletions backend/internal/compilerServer/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,7 @@ func (app *CompiledApp) IsAlive() bool {
return false
}

if !process.IsAlive() {
return false
}

// Send a ping to the process
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/health", process.Port))
if resp != nil {
resp.Body.Close()
}
return err == nil && resp.StatusCode == http.StatusOK
return process.CheckHealth()
}

func (app *CompiledApp) keepAlive() {
Expand Down Expand Up @@ -264,7 +255,7 @@ func (app *CompiledApp) StartServer() error {
}

// Send a ping to the process
if serverProcess.IsHealthy() {
if serverProcess.CheckHealth() {
if atomic.CompareAndSwapInt64(app.keepAliveRunning, 0, 1) {
go app.keepAlive()
}
Expand Down
4 changes: 4 additions & 0 deletions backend/internal/identity/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func (id Id) String() string {

// Cleans inputs and then creates a category from them. If you have a valid category already,
// ust path.Join to combine it with another category.
//
// Example:
// identity.Category("app", "appId") -> "/app/appId"
// identity.Category("my", "app", "badString/../") -> "/my/app/badString%2F%2E%2E%2F
func Category(ids ...string) string {
parts := make([]string, 0, len(ids))
for _, id := range ids {
Expand Down
7 changes: 7 additions & 0 deletions backend/internal/process/handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ func (m *ProcessManager) IsAlive(id ProcessId) bool {
return r.IsAlive(id)
}

func (m *ProcessManager) CheckHealth(id ProcessId) bool {
r := m.ReadHandle()
defer r.Close()

return r.CheckHealth(id)
}

func (m *ProcessManager) CopyOutData() []Process {
r := m.ReadHandle()
defer r.Close()
Expand Down
12 changes: 10 additions & 2 deletions backend/internal/process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,16 @@ func (r *RHandle) IsAlive(id ProcessId) bool {
return process.IsAlive()
}

func (proc *Process) IsHealthy() bool {
return proc.IsAlive() && proc.HealthCheck.Check(health.RunningProcessInfo{Pid: proc.Pid, Port: proc.Port})
func (r *RHandle) CheckHealth(id ProcessId) bool {
process, found := r.FindById(id)
if !found {
return false
}
return process.CheckHealth()
}

func (proc *Process) CheckHealth() bool {
return proc.HealthCheck.Check(health.RunningProcessInfo{Pid: proc.Pid, Port: proc.Port})
}

// This reads the path variable to find the right executable.
Expand Down
79 changes: 79 additions & 0 deletions backend/internal/server/process_rpc.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,89 @@
package server

import (
"net/http"

"robinplatform.dev/internal/identity"
"robinplatform.dev/internal/process"
"robinplatform.dev/internal/pubsub"
)

type StartProcessForAppInput struct {
AppId string `json:"appId"`
ProcessKey string `json:"processKey"`
Command string `json:"command"`
Args []string `json:"args"`
}

var StartProcessForApp = AppsRpcMethod[StartProcessForAppInput, map[string]any]{
Name: "StartProcess",
Run: func(req RpcRequest[StartProcessForAppInput]) (map[string]any, *HttpError) {
id := process.ProcessId{
Category: identity.Category("app", req.Data.AppId),
Key: req.Data.ProcessKey,
}

processConfig := process.ProcessConfig{
Command: req.Data.Command,
Args: req.Data.Args,
Id: id,
}

proc, err := process.Manager.Spawn(processConfig)
if err != nil {
return nil, Errorf(http.StatusInternalServerError, "Failed to spawn new process %s: %s", req.Data.AppId, err)
}

return map[string]any{
"processKey": proc.Id,
"pid": proc.Pid,
}, nil
},
}

type StopProcessForAppInput struct {
AppId string `json:"appId"`
ProcessKey string `json:"processKey"`
}

var StopProcessForApp = AppsRpcMethod[StartProcessForAppInput, map[string]any]{
Name: "StopProcess",
Run: func(req RpcRequest[StartProcessForAppInput]) (map[string]any, *HttpError) {
id := process.ProcessId{
Category: identity.Category("app", req.Data.AppId),
Key: req.Data.ProcessKey,
}

if err := process.Manager.Remove(id); err != nil {
return nil, Errorf(http.StatusInternalServerError, "Failed to kill process %s: %s", req.Data.AppId, err)
}

return map[string]any{}, nil
},
}

type CheckProcessHealthInput struct {
AppId string `json:"appId"`
ProcessKey string `json:"processKey"`
}

var CheckProcessHealth = AppsRpcMethod[CheckProcessHealthInput, map[string]any]{
Name: "CheckProcessHealth",
Run: func(req RpcRequest[CheckProcessHealthInput]) (map[string]any, *HttpError) {
id := process.ProcessId{
Category: identity.Category("app", req.Data.AppId),
Key: req.Data.ProcessKey,
}

isHealthy := process.Manager.CheckHealth(id)

return map[string]any{
"processKey": id,
"isHealthy": isHealthy,
}, nil
},
}

func PipeTopic[T any](topicId pubsub.TopicId, req *StreamRequest[T, any]) error {
sub, err := pubsub.SubscribeAny(&pubsub.Topics, topicId)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions backend/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ func (server *Server) loadRpcMethods() {
CreateTopic.Register(server)
PublishTopic.Register(server)

StartProcessForApp.Register(server)
StopProcessForApp.Register(server)
CheckProcessHealth.Register(server)

// Streaming methods

wsHandler := &RpcWebsocket{}
Expand Down
104 changes: 104 additions & 0 deletions frontend/components/ProcessDebugger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { z } from 'zod';
import React from 'react';
import { runRpcQuery, useRpcQuery } from '../hooks/useRpcQuery';
import { useTopic } from '../../toolkit/react/stream';
import { ScrollWindow } from './ScrollWindow';
import toast from 'react-hot-toast';

type ProcessInfo = z.infer<typeof ProcessInfo>;
const ProcessInfo = z.object({});

type Process = z.infer<typeof Process>;
const Process = z.object({
id: z.object({
category: z.string(),
key: z.string(),
}),
command: z.string(),
args: z.array(z.string()),
});

// This is a temporary bit of code to just display what's in the processes DB
// to make writing other features easier
export function ProcessDebugger() {
const { data: processes = [], error } = useRpcQuery({
method: 'ListProcesses',
data: {},
result: z.array(Process),
});

const [currentProcess, setCurrentProcess] = React.useState<Process>();
const { state } = useTopic({
topicId: currentProcess && {
category: `/logs${currentProcess.id.category}`,
key: currentProcess.id.key,
},
resultType: z.string(),
fetchState: () =>
runRpcQuery({
method: 'GetProcessLogs',
data: { processId: currentProcess?.id },
result: z.object({
counter: z.number(),
text: z.string(),
}),
}).then(({ counter, text }) => ({ counter, state: text })),
reducer: (prev, message) => {
return prev + '\n' + message;
},
});

React.useEffect(() => {
if (error) {
toast.error(`${String(error)}`);
}
}, [error]);

return (
<div
className={'full col robin-rounded robin-gap robin-pad'}
style={{ backgroundColor: 'DarkSlateGray', maxHeight: '100%' }}
>
<div>Processes</div>

<ScrollWindow className={'full'} innerClassName={'col robin-gap'}>
{processes?.map((value) => {
const key = `${value.id.category} ${value.id.key}`;
return (
<div
key={key}
className={'robin-rounded robin-pad'}
style={{ backgroundColor: 'Coral', width: '100%' }}
>
{key}

<button onClick={() => setCurrentProcess(value)}>Select</button>

<pre
style={{
width: '100%',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
}}
>
{JSON.stringify(value, null, 2)}
</pre>
</div>
);
})}
</ScrollWindow>

<ScrollWindow className={'full'} innerClassName={'col robin-gap'}>
<pre
style={{
width: '100%',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
}}
>
{state}
</pre>
</ScrollWindow>
</div>
);
}
Loading