///|
/// The managed-application model.
///
/// `App` doubles as the launch specification and the live runtime record,
/// mirroring PM2's `pm2_env`. It is serialized verbatim to `dump.json` so a
/// restart of the daemon can resurrect the process list.
///|
pub let status_online : String = "online"
///|
pub let status_stopped : String = "stopped"
///|
pub let status_stopping : String = "stopping"
///|
pub let status_launching : String = "launching"
///|
pub let status_errored : String = "errored"
///|
/// A process managed by pm3.
pub(all) struct App {
// --- identity ---
mut pm_id : Int
mut name : String
/// The namespace the app belongs to — PM2's `pm2_env.namespace`. `default`
/// unless `--namespace` (or the config file) says otherwise. The field is
/// named `app_namespace` because `namespace` is a reserved word in MoonBit;
/// the option and the config key keep PM2's spelling.
mut app_namespace : String
// --- launch ---
mut script : String
mut args : Array[String]
/// Empty means "execute `script` directly"; otherwise `script` is passed as
/// the first argument to this interpreter (e.g. `python3`, `node`, `bash`).
mut interpreter : String
/// Arguments passed to the interpreter, before the script — PM2's
/// `node_args` / `interpreter_args`. For example `["--harmony"]` turns
/// `node script.js` into `node --harmony script.js`.
mut interpreter_args : Array[String]
mut cwd : String
mut env : Map[String, String]
// --- logs ---
mut out_log : String
mut err_log : String
/// Send both stdout and stderr to `out_log`.
mut merge_logs : Bool
/// Optional `moment`-style prefix stamped onto every log line.
mut log_date_format : String
// --- supervision ---
mut autorestart : Bool
mut max_restarts : Int
/// Delay before a restart, in milliseconds.
mut restart_delay : Int
/// A process that dies sooner than this is considered unstable.
mut min_uptime : Int
/// Restart when RSS exceeds this many bytes. `0` disables the check.
mut max_memory_restart : Int64
/// Grace period before SIGKILL, in milliseconds.
mut kill_timeout : Int
/// Signal used for graceful shutdown.
mut kill_signal : String
/// Exit codes that should *not* trigger an automatic restart.
mut stop_exit_codes : Array[Int]
// --- runtime state ---
mut status : String
/// The app's own version — the `version` of the nearest `package.json` at
/// or above its script, PM2's `pm2_env.version`. `N/A` when there is none.
mut version : String
/// Login name of the user that started the app, PM2's `pm2_env.username`.
mut username : String
/// What the user asked for: `online` or `stopped`.
mut desired_state : String
mut pid : Int
mut restarts : Int
mut unstable_restarts : Int
mut created_at : Int64
mut started_at : Int64
mut stopped_at : Int64
mut exit_code : Int
mut out_log_size : Int64
mut err_log_size : Int64
mut memory : Int64
mut cpu : Double
mut last_start_ms : Int64
} derive(Eq, Debug, ToJson, FromJson)
///|
/// Default log path for a stream (`out` or `err`) of an application.
pub fn default_log_path(name : String, stream : String) -> String {
path_join(pm3_log_dir(), "\{name}-\{stream}.log")
}
///|
/// The login name of the user running pm3 — PM2's `pm2_env.username`, taken
/// from `Common.getCurrentUsername`: the passwd entry of the effective user
/// comes first, the usual environment variables are the fallback.
pub fn current_username() -> String {
match passwd_username() {
Some(name) => return name
None => ()
}
for key in ["USER", "LNAME", "USERNAME", "SUDO_USER", "C9_USER", "LOGNAME"] {
match @env.get_env_var(key) {
Some(value) if value.length() > 0 => return value
_ => ()
}
}
""
}
///|
/// `getuid(2)` from the C library.
extern "C" fn pm3_uid() -> Int = "pm3_uid"
///|
/// Resolve `/etc/passwd` for the uid this process runs as.
fn passwd_username() -> String? {
let uid = pm3_uid()
if uid < 0 {
return None
}
let text = @fs.read_file_to_string("/etc/passwd") catch { _ => return None }
for line in text.split("\n") {
let fields : Array[StringView] = []
for field in line.split(":") {
fields.push(field)
}
if fields.length() >= 3 {
match parse_non_negative_int(fields[2]) {
Some(id) if id == uid => return Some(fields[0].to_owned())
_ => ()
}
}
}
None
}
///|
/// The signal `pm3 stop` sends before `kill_timeout` expires. PM2 v7's
/// daemon uses its `KILL_SIGNAL` constant — `PM2_KILL_SIGNAL` when set,
/// `SIGINT` otherwise — so pm3 starts from the same default while still
/// letting each app override it.
pub fn default_kill_signal() -> String {
match @env.get_env_var("PM2_KILL_SIGNAL") {
Some(value) if value.length() > 0 => value
_ => "SIGINT"
}
}
///|
/// The default grace period before escalating to `SIGKILL`, matching PM2's
/// `KILL_TIMEOUT`.
pub let default_kill_timeout_ms : Int = 1600
///|
/// Build a new application with conservative defaults.
pub fn App::new(name : String, script : String, args : Array[String]) -> App {
let now = @env.now().reinterpret_as_int64()
{
pm_id: -1,
name,
app_namespace: "default",
script,
args,
interpreter: "",
interpreter_args: [],
cwd: "",
env: Map([]),
out_log: default_log_path(name, "out"),
err_log: default_log_path(name, "err"),
merge_logs: false,
log_date_format: "",
autorestart: true,
max_restarts: 16,
restart_delay: 0,
min_uptime: 1000,
max_memory_restart: 0L,
kill_timeout: default_kill_timeout_ms,
kill_signal: default_kill_signal(),
stop_exit_codes: [],
status: status_stopped,
version: "N/A",
username: current_username(),
desired_state: status_stopped,
pid: 0,
restarts: 0,
unstable_restarts: 0,
created_at: now,
started_at: 0L,
stopped_at: 0L,
exit_code: 0,
out_log_size: 0L,
err_log_size: 0L,
memory: 0L,
cpu: 0.0,
last_start_ms: 0L,
}
}
///|
/// The argv pm3 will execute for this app: the interpreter (if any), its
/// arguments, then the script and the script's own arguments.
pub fn App::command_line(self : App) -> (String, Array[String]) {
if self.interpreter.length() > 0 && self.interpreter != "none" {
let argv : Array[String] = []
argv.push_iter(self.interpreter_args.iter())
argv.push(self.script)
argv.push_iter(self.args.iter())
(self.interpreter, argv)
} else {
(self.script, self.args.copy())
}
}
///|
/// Human-readable one-line command, for `pm3 list` and `pm3 describe`.
pub fn App::command_display(self : App) -> String {
let (cmd, argv) = self.command_line()
let buf = StringBuilder()
buf.write_string(cmd)
for arg in argv {
buf.write_char(' ')
buf.write_string(arg)
}
buf.to_string()
}
///|
/// Whether the app currently has a live process.
pub fn App::is_online(self : App) -> Bool {
self.status == status_online || self.status == status_launching
}
///|
/// Whether a `pm3 stop` was requested.
pub fn App::wants_stop(self : App) -> Bool {
self.desired_state == status_stopped
}
///|
/// Record that the process just started.
pub fn App::mark_started(self : App, pid : Int) -> Unit {
self.pid = pid
self.status = status_online
self.started_at = @env.now().reinterpret_as_int64()
self.last_start_ms = self.started_at
self.exit_code = 0
}
///|
/// Record that the process exited with `code`. The resource samples are
/// dropped with it: PM2's monitor reports `0` for anything that is not
/// running, and the last sample of a dead process is not memory it holds.
pub fn App::mark_exited(self : App, code : Int) -> Unit {
self.pid = 0
self.memory = 0L
self.cpu = 0.0
self.exit_code = code
self.stopped_at = @env.now().reinterpret_as_int64()
if self.wants_stop() {
self.status = status_stopped
} else {
self.status = status_errored
}
}
///|
/// Whether the process should be restarted after exiting with `code`.
pub fn App::should_restart(self : App, code : Int) -> Bool {
if self.wants_stop() {
return false
}
if !self.autorestart {
return false
}
if self.stop_exit_codes.contains(code) {
return false
}
self.max_restarts <= 0 || self.restarts < self.max_restarts
}
///|
/// Record one restart, tracking whether the previous run was "unstable".
pub fn App::record_restart(self : App) -> Unit {
let now = @env.now().reinterpret_as_int64()
let uptime = now - self.last_start_ms
if self.last_start_ms > 0L && uptime < self.min_uptime.to_int64() {
self.unstable_restarts = self.unstable_restarts + 1
} else {
self.unstable_restarts = 0
}
self.restarts = self.restarts + 1
}
///|
/// A compact JSON description of an app, used for IPC payloads and `--json`.
pub fn App::to_summary_json(self : App) -> Json {
@json.to_json(self)
}