///|
/// Interpreter resolution, the way PM2 does it.
///
/// PM2 never runs a script naked unless the file is a real executable: it
/// looks the extension up in a small map (`.js` → `node`, `.py` → `python`,
/// `.sh` → `bash`, …), and everything else that is not a binary is handed to
/// `node`. pm3 is language-agnostic, so the mapped commands are looked up on
/// `PATH` like any other program; what it keeps is PM2's decision, so
/// `pm3 start app.js` really runs `node app.js` even when `app.js` has no
/// shebang.
///
/// The pieces here are pure: the filesystem probes (`which`, reading the first
/// bytes of a file) live in the daemon, which is the side that has an async
/// runtime.
///|
/// The extension of `path`, including the dot, as `path.extname` defines it:
/// the last dot of the last component, and nothing for a leading dot or a
/// missing one.
pub fn script_extension(path : String) -> String {
let base = path_basename(path)
match base.rev_find(".") {
Some(index) if index > 0 =>
base.view(start_offset=index, end_offset=base.length()).to_owned()
_ => ""
}
}
///|
/// PM2's `lib/API/interpreter.json`, verbatim.
///
/// The returned name is a command, not a path: it is looked up on `PATH` when
/// the app starts.
pub fn interpreter_for_extension(extension : String) -> String? {
match extension {
".sh" => Some("bash")
".py" => Some("python")
".rb" => Some("ruby")
".php" => Some("php")
".pl" => Some("perl")
".js" => Some("node")
".coffee" => Some("coffee")
".ls" => Some("lsc")
".ts" => Some("bun")
".tsx" => Some("bun")
_ => None
}
}
///|
/// Whether a file's first bytes look like a compiled executable rather than
/// text — PM2's `isbinaryfile` heuristic: a NUL byte, or more than 10% of the
/// sample being non-text, means binary. UTF-8 multibyte sequences and a UTF-8
/// BOM are not binary. An empty sample is text.
pub fn is_binary_content(bytes : Bytes) -> Bool {
let total = if bytes.length() > 512 { 512 } else { bytes.length() }
if total == 0 {
return false
}
if total >= 3 &&
bytes[0].to_int() == 0xEF &&
bytes[1].to_int() == 0xBB &&
bytes[2].to_int() == 0xBF {
return false
}
let mut suspicious = 0
let mut i = 0
while i < total {
let byte = bytes[i].to_int()
if byte == 0 {
return true
}
if (byte < 7 || byte > 14) && (byte < 32 || byte > 127) {
// A UTF-8 two- or three-byte sequence is not suspicious.
if byte > 193 && byte < 224 && i + 1 < total {
i = i + 1
let next = bytes[i].to_int()
if next > 127 && next < 192 {
i = i + 1
continue
}
} else if byte > 223 && byte < 240 && i + 2 < total {
i = i + 1
let next = bytes[i].to_int()
if next > 127 && next < 192 {
let after = bytes[i + 1].to_int()
if after > 127 && after < 192 {
i = i + 2
continue
}
}
}
suspicious = suspicious + 1
// Read at least 32 bytes before making a decision.
if i > 32 && suspicious * 100 / total > 10 {
return true
}
}
i = i + 1
}
suspicious * 100 / total > 10
}
///|
/// Whether a `python` interpreter needs its output unbuffered: PM2 exports
/// `PYTHONUNBUFFERED=1` whenever the interpreter contains `python`, so that
/// logs appear in the file as the program writes them.
pub fn is_python_interpreter(interpreter : String) -> Bool {
interpreter.contains("python")
}