✨ Process lifecycle management
This commit is contained in:
		
							
								
								
									
										
											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"` | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user