✨ Process lifecycle management
This commit is contained in:
parent
f66f144f2e
commit
ae12eb2a15
BIN
cli/bun.lockb
BIN
cli/bun.lockb
Binary file not shown.
@ -7,6 +7,7 @@ import { LogoutCommand } from "./src/cmd/logout.ts"
|
|||||||
import { ListServerCommand } from "./src/cmd/list.ts"
|
import { ListServerCommand } from "./src/cmd/list.ts"
|
||||||
import { StatusCommand } from "./src/cmd/status.ts"
|
import { StatusCommand } from "./src/cmd/status.ts"
|
||||||
import { InfoCommand } from "./src/cmd/info.ts"
|
import { InfoCommand } from "./src/cmd/info.ts"
|
||||||
|
import { ProcessCommand } from "./src/cmd/process-info.ts"
|
||||||
|
|
||||||
const [node, app, ...args] = process.argv
|
const [node, app, ...args] = process.argv
|
||||||
|
|
||||||
@ -29,4 +30,5 @@ cli.register(LogoutCommand)
|
|||||||
cli.register(ListServerCommand)
|
cli.register(ListServerCommand)
|
||||||
cli.register(StatusCommand)
|
cli.register(StatusCommand)
|
||||||
cli.register(InfoCommand)
|
cli.register(InfoCommand)
|
||||||
|
cli.register(ProcessCommand)
|
||||||
cli.runExit(args)
|
cli.runExit(args)
|
@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
|
"cli-table3": "^0.6.5",
|
||||||
"clipanion": "^4.0.0-rc.4",
|
"clipanion": "^4.0.0-rc.4",
|
||||||
"figlet": "^1.7.0",
|
"figlet": "^1.7.0",
|
||||||
"ora": "^8.1.0"
|
"ora": "^8.1.0"
|
||||||
|
146
cli/src/cmd/process-info.ts
Normal file
146
cli/src/cmd/process-info.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { Command, Option, type Usage } from "clipanion"
|
||||||
|
import { RsConfig } from "../utils/config.ts"
|
||||||
|
import { createAuthHeader } from "../utils/auth.ts"
|
||||||
|
import Table from "cli-table3"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import ora from "ora"
|
||||||
|
|
||||||
|
export class ProcessCommand extends Command {
|
||||||
|
static paths = [[`process`], [`ps`]]
|
||||||
|
static usage: Usage = {
|
||||||
|
category: `Networking`,
|
||||||
|
description: `Loading the application of RoadSign Server`,
|
||||||
|
details: `Fetching the configured things amount and other things of a connected server`,
|
||||||
|
examples: [
|
||||||
|
["Fetch app directory from labeled server", `ps <label>`],
|
||||||
|
["Fetch app logs from labeled server", `ps <label> <applicationId> logs`]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
label = Option.String({ required: true })
|
||||||
|
applicationId = Option.String({ required: false })
|
||||||
|
subcommand = Option.String({ required: false })
|
||||||
|
loop = Option.Boolean("--loop,--follow,-f", false, { description: "Keep updating the results" })
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
const config = await RsConfig.getInstance()
|
||||||
|
|
||||||
|
const server = config.config.servers.find(item => item.label === this.label)
|
||||||
|
if (server == null) {
|
||||||
|
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.label)} was not found.\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = ora(`Fetching stats from server ${this.label}...`).start()
|
||||||
|
const prefStart = performance.now()
|
||||||
|
|
||||||
|
|
||||||
|
if (this.applicationId == null) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${server.url}/cgi/applications`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
const prefTook = performance.now() - prefStart
|
||||||
|
if (!this.loop) {
|
||||||
|
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = new Table({
|
||||||
|
head: ["ID", "Status", "Command"],
|
||||||
|
colWidths: [20, 10, 48]
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusMapping = ["Created", "Starting", "Started", "Exited", "Failed"]
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
for (const app of data) {
|
||||||
|
table.push([app["id"], statusMapping[app["status"]], app["command"].join(" ")])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.stdout.write(table.toString())
|
||||||
|
} catch (e) {
|
||||||
|
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (this.subcommand) {
|
||||||
|
case "logs":
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${server.url}/cgi/applications/${this.applicationId}/logs`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status === 404) {
|
||||||
|
spinner.fail(`App with id ${chalk.bold(this.applicationId)} was not found! 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
const prefTook = performance.now() - prefStart
|
||||||
|
if (!this.loop) {
|
||||||
|
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.stdout.write(await res.text())
|
||||||
|
} catch (e) {
|
||||||
|
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.loop) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
spinner.text = "Updating..."
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
this.context.stdout.write("\x1Bc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "start":
|
||||||
|
case "stop":
|
||||||
|
case "restart":
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${server.url}/cgi/applications/${this.applicationId}/${this.subcommand}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status === 404) {
|
||||||
|
spinner.fail(`App with id ${chalk.bold(this.applicationId)} was not found! 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (res.status === 500) {
|
||||||
|
this.context.stdout.write(chalk.red(`Server failed to perform action for application: ${await res.text()}\n`))
|
||||||
|
spinner.fail(`Failed to perform action ${chalk.bold(this.applicationId)}... 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
const prefTook = performance.now() - prefStart
|
||||||
|
if (!this.loop) {
|
||||||
|
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spinner.succeed(`Action for application ${chalk.bold(this.applicationId)} has been performed. 🎉`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.context.stdout.write(chalk.red(`Subcommand ${chalk.bold(this.subcommand)} was not found.\n`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
}
|
@ -15,8 +15,5 @@ func InitializeWarden(regions []*Region) {
|
|||||||
|
|
||||||
// Hot swap
|
// Hot swap
|
||||||
warden.InstancePool = pool
|
warden.InstancePool = pool
|
||||||
|
warden.StartPool()
|
||||||
for _, instance := range warden.InstancePool {
|
|
||||||
instance.Wake()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func getApplications(c *fiber.Ctx) error {
|
func getApplications(c *fiber.Ctx) error {
|
||||||
applications := lo.FlatMap(navi.R.Regions, func(item *navi.Region, idx int) []warden.Application {
|
applications := lo.FlatMap(navi.R.Regions, func(item *navi.Region, idx int) []warden.ApplicationInfo {
|
||||||
return item.Applications
|
return lo.Map(item.Applications, func(item warden.Application, index int) warden.ApplicationInfo {
|
||||||
|
return warden.ApplicationInfo{
|
||||||
|
Application: item,
|
||||||
|
Status: warden.GetFromPool(item.ID).Status,
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return c.JSON(applications)
|
return c.JSON(applications)
|
||||||
@ -24,3 +29,45 @@ func getApplicationLogs(c *fiber.Ctx) error {
|
|||||||
return c.SendString(instance.Logs())
|
return c.SendString(instance.Logs())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func letApplicationStart(c *fiber.Ctx) error {
|
||||||
|
if instance, ok := lo.Find(warden.InstancePool, func(item *warden.AppInstance) bool {
|
||||||
|
return item.Manifest.ID == c.Params("id")
|
||||||
|
}); !ok {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
if err := instance.Wake(); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func letApplicationStop(c *fiber.Ctx) error {
|
||||||
|
if instance, ok := lo.Find(warden.InstancePool, func(item *warden.AppInstance) bool {
|
||||||
|
return item.Manifest.ID == c.Params("id")
|
||||||
|
}); !ok {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
if err := instance.Stop(); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func letApplicationRestart(c *fiber.Ctx) error {
|
||||||
|
if instance, ok := lo.Find(warden.InstancePool, func(item *warden.AppInstance) bool {
|
||||||
|
return item.Manifest.ID == c.Params("id")
|
||||||
|
}); !ok {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
if err := instance.Stop(); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
if err := instance.Start(); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -49,6 +49,9 @@ func InitSideload() *fiber.App {
|
|||||||
cgi.Get("/regions/cfg/:id", getRegionConfig)
|
cgi.Get("/regions/cfg/:id", getRegionConfig)
|
||||||
cgi.Get("/applications", getApplications)
|
cgi.Get("/applications", getApplications)
|
||||||
cgi.Get("/applications/:id/logs", getApplicationLogs)
|
cgi.Get("/applications/:id/logs", getApplicationLogs)
|
||||||
|
cgi.Post("/applications/:id/start", letApplicationStart)
|
||||||
|
cgi.Post("/applications/:id/stop", letApplicationStop)
|
||||||
|
cgi.Post("/applications/:id/restart", letApplicationRestart)
|
||||||
|
|
||||||
cgi.Post("/reload", doReload)
|
cgi.Post("/reload", doReload)
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@ package warden
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"github.com/rs/zerolog/log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@ -87,15 +88,16 @@ func (v *AppInstance) Start() error {
|
|||||||
// Monitor
|
// Monitor
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
if v.Cmd.Process == nil || v.Cmd.ProcessState == nil {
|
if v.Cmd != nil && v.Cmd.Process == nil {
|
||||||
v.Status = AppStarting
|
v.Status = AppStarting
|
||||||
} else if !v.Cmd.ProcessState.Exited() {
|
} else if v.Cmd != nil && v.Cmd.ProcessState == nil {
|
||||||
v.Status = AppStarted
|
v.Status = AppStarted
|
||||||
} else {
|
} else {
|
||||||
v.Status = lo.Ternary(v.Cmd.ProcessState.Success(), AppExited, AppFailure)
|
v.Status = lo.Ternary(v.Cmd == nil, AppExited, AppFailure)
|
||||||
|
v.Cmd = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(1000 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -104,8 +106,13 @@ func (v *AppInstance) Start() error {
|
|||||||
|
|
||||||
func (v *AppInstance) Stop() error {
|
func (v *AppInstance) Stop() error {
|
||||||
if v.Cmd != nil && v.Cmd.Process != nil {
|
if v.Cmd != nil && v.Cmd.Process != nil {
|
||||||
if err := v.Cmd.Process.Signal(os.Interrupt); err != nil {
|
if err := v.Cmd.Process.Signal(syscall.SIGTERM); err != nil {
|
||||||
v.Cmd.Process.Kill()
|
log.Warn().Int("pid", v.Cmd.Process.Pid).Err(err).Msgf("Failed to send SIGTERM to process...")
|
||||||
|
if err = v.Cmd.Process.Kill(); err != nil {
|
||||||
|
log.Error().Int("pid", v.Cmd.Process.Pid).Err(err).Msgf("Failed to kill process...")
|
||||||
|
} else {
|
||||||
|
v.Cmd = nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
v.Cmd = nil
|
v.Cmd = nil
|
||||||
|
@ -6,3 +6,8 @@ type Application struct {
|
|||||||
Command []string `json:"command" toml:"command"`
|
Command []string `json:"command" toml:"command"`
|
||||||
Environment []string `json:"environment" toml:"environment"`
|
Environment []string `json:"environment" toml:"environment"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ApplicationInfo struct {
|
||||||
|
Application
|
||||||
|
Status AppStatus `json:"status"`
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user