✨ 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 { StatusCommand } from "./src/cmd/status.ts" | ||||
| import { InfoCommand } from "./src/cmd/info.ts" | ||||
| import { ProcessCommand } from "./src/cmd/process-info.ts" | ||||
|  | ||||
| const [node, app, ...args] = process.argv | ||||
|  | ||||
| @@ -29,4 +30,5 @@ cli.register(LogoutCommand) | ||||
| cli.register(ListServerCommand) | ||||
| cli.register(StatusCommand) | ||||
| cli.register(InfoCommand) | ||||
| cli.register(ProcessCommand) | ||||
| cli.runExit(args) | ||||
| @@ -11,6 +11,7 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "chalk": "^5.3.0", | ||||
|     "cli-table3": "^0.6.5", | ||||
|     "clipanion": "^4.0.0-rc.4", | ||||
|     "figlet": "^1.7.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 | ||||
| 	warden.InstancePool = pool | ||||
|  | ||||
| 	for _, instance := range warden.InstancePool { | ||||
| 		instance.Wake() | ||||
| 	} | ||||
| 	warden.StartPool() | ||||
| } | ||||
|   | ||||
| @@ -8,8 +8,13 @@ import ( | ||||
| ) | ||||
|  | ||||
| func getApplications(c *fiber.Ctx) error { | ||||
| 	applications := lo.FlatMap(navi.R.Regions, func(item *navi.Region, idx int) []warden.Application { | ||||
| 		return item.Applications | ||||
| 	applications := lo.FlatMap(navi.R.Regions, func(item *navi.Region, idx int) []warden.ApplicationInfo { | ||||
| 		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) | ||||
| @@ -24,3 +29,45 @@ func getApplicationLogs(c *fiber.Ctx) error { | ||||
| 		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("/applications", getApplications) | ||||
| 		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) | ||||
| 	} | ||||
|   | ||||
| @@ -2,10 +2,11 @@ package warden | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/samber/lo" | ||||
| @@ -87,15 +88,16 @@ func (v *AppInstance) Start() error { | ||||
| 	// Monitor | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			if v.Cmd.Process == nil || v.Cmd.ProcessState == nil { | ||||
| 			if v.Cmd != nil && v.Cmd.Process == nil { | ||||
| 				v.Status = AppStarting | ||||
| 			} else if !v.Cmd.ProcessState.Exited() { | ||||
| 			} else if v.Cmd != nil && v.Cmd.ProcessState == nil { | ||||
| 				v.Status = AppStarted | ||||
| 			} else { | ||||
| 				v.Status = lo.Ternary(v.Cmd.ProcessState.Success(), AppExited, AppFailure) | ||||
| 				v.Status = lo.Ternary(v.Cmd == nil, AppExited, AppFailure) | ||||
| 				v.Cmd = nil | ||||
| 				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 { | ||||
| 	if v.Cmd != nil && v.Cmd.Process != nil { | ||||
| 		if err := v.Cmd.Process.Signal(os.Interrupt); err != nil { | ||||
| 			v.Cmd.Process.Kill() | ||||
| 		if err := v.Cmd.Process.Signal(syscall.SIGTERM); err != nil { | ||||
| 			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 | ||||
| 		} else { | ||||
| 			v.Cmd = nil | ||||
|   | ||||
| @@ -6,3 +6,8 @@ type Application struct { | ||||
| 	Command     []string `json:"command" toml:"command"` | ||||
| 	Environment []string `json:"environment" toml:"environment"` | ||||
| } | ||||
|  | ||||
| type ApplicationInfo struct { | ||||
| 	Application | ||||
| 	Status AppStatus `json:"status"` | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user