EMILUA(3em)

πŸ“¦ version 0.12.1, in nixos 26.05, nixpkgs unstable

πŸ’œ EMILUA(3em) β€” the vibe check

okay so picture this: you cracked open the manual for Emilua, and the section header says EMILUA(3em) β€” that's the manual name right there, the Emilua reference, section 3em, bestie, don't let it scare you 🫢


✨ NAME ✨

Emilua β€” it's a Lua execution engine 🌈 and that's literally the whole flex.

Like, Lua runs inside it. It's the engine. That's the job. Iconic behavior, no notes. πŸ‘‘


πŸŽ€ SYNOPSIS πŸŽ€

here's the shape of it, copy-paste ready, exact characters, do not touch:

local system = require "system"
system.spawn(opts: table) -> subprocess

so first you pull in the module with require "system" and stash it in a local called system β€” then you call system.spawn with a single opts argument that's a table, and what comes back is a subprocess. πŸ¦‹

opts: table β†’ table in. -> subprocess β†’ subprocess out. that's the whole trade, bestie. πŸ’…


part 1 energy only β€” the rest of this page is coming, keep scrolling, we're literally not done 🧚

πŸ’– DESCRIPTION πŸ’–

Creates a new process. Slay! You're literally spawning a whole new little guy into existence. πŸ‘‘


✨ Named parameters ✨

  • program: string | filesystem.path | file_descriptor β€” this is what you're running, bestie. Three ways to say it:
    • string β€” a simple filename. The system goes hunting for this file in the list of directories specified by PATH (exactly the same way as for execvp(3)). Very "look it up in the group chat directory" energy.
    • filesystem.path β€” the path (which can be absolute or relative) of the executable. You know exactly where it lives.
    • file_descriptor β€” a file descriptor pointing straight to the executable. See fexecve(3). Iconic, lowkey advanced.
  • arguments: string[] | nil β€” a table of strings that will be used as the created process' argv. On nil, an empty argv will be used.
⚠️ Caution: Don't forget to include the name of the program as the first argument. Not me forgetting argv[0] again 😭 fr fr remember it.
  • environment: { [string]: string } | nil β€” a table of strings that will be used as the created process' envp. On nil, an empty envp will be used.

If "\0pid" is used as the value for an environment variable, the value will be replaced by the pid of the child. This is useful to implement some FD3-based protocols that use variables such as LISTEN_PID for robustness. Only one environment variable may use the value "\0pid". One (1) single variable, bestie, don't get greedy πŸ’…

  • stdin, stdout, stderr: "share" | file_descriptor | nil β€” the standard handles, obviously. Three moods:
    • "share" β€” the spawned process will share the specified standard handle (stdin, stdout, or stderr) with the caller process. Same vibe, same handle 🫢
    • file_descriptor β€” use the file descriptor as the specified standard handle (stdin, stdout, or stderr) for the spawned process.
    • nil β€” create and use a closed pipe end as the specified standard handle (stdin, stdout, or stderr) for the spawned process.
🩷 Note: On Windows, it's unspecified (will vary depending on whether any redirection is done at all, dwCreationFlags's value, etc). Chaos behaviour, ngl, don't count on it.
  • extra_fds: { [integer]: file_descriptor|libc_service.slave } | nil β€” extra file descriptors for the child to inherit. Parent and child processes don't need to share the same numeric value reference for a given file description. The file descriptor number used in the child process will be the one specified in the key portion of the dictionary argument. Only file descriptors numbered from 3 to 9 are acceptable (i.e. the same limitations of low fds that you're likely to face on older UNIX shells). If you need to pass more than 10 file descriptors β€” stdin, stdout, stderr, plus these extra 7 file descriptors β€” use another interface (e.g. SCM_RIGHTS). Seven extra fds max, that's the whole budget bestie πŸŽ€
🩷 Note: Not available on Windows.
  • signal_on_gcreaper: integer = system.signal.SIGTERM β€” okay so this one's a whole saga, sit down. Each process is responsible for reaping its own children. A process that fails to reap its children will soon exhaust its OS-provided resources. For short-lived programs that's hardly a problem given the process quits and its children are re-parented to the next subreaper in the chain (usually the init process). However for a concurrency runtime such as Emilua we expect other concurrent tasks to remain unaffected by the one failing task (be it a single fiber or the whole VM). Emilua will then transparently reap any child process for which its handle has been GC'ed. signal_on_gcreaper allows the user to specify a signal to be sent to the child that's about to be reaped at this occasion.

By default, the signal system.signal.SIGTERM will be sent to the child and then the main Emilua process will β€” indefinitely, non-blockingly, and non-pollingly β€” await for all of its children to finish even if there's no longer any Lua program being executed. Use the more dangerous system.signal.SIGKILL if you don't want the main Emilua process to wait long for the child. Use 0 if you don't want the Emilua reaper to send any signal before awaiting for the child.

🩷 Note: Ideally the system kernel would expose some re-parent syscall, but until then (if ever), signal_on_gcreaper will be necessary. Manifesting that syscall for us πŸ™
🩷 Note: Only available on Linux.
  • pd_daemon: boolean = see-below β€” instead of the default terminate-on-close behaviour, allow the process to live until it is explicitly killed with kill(2). Main character energy: it stays alive until it decides otherwise.

By default, it's true unless the parent process is in capability mode (see cap_enter(2)).

🩷 Note: Only available on FreeBSD.
  • scheduler.policy: string | nil β€” the scheduling vibe check. Values acceptable on Linux for non-real-time policies are:
    • "other" β€” see SCHED_OTHER.
    • "batch" β€” see SCHED_BATCH.
    • "idle" β€” see SCHED_IDLE.

Values acceptable on Linux for real-time policies are:

  • "fifo" β€” see SCHED_FIFO. Must also set scheduler.priority. Required, no skipping πŸ’€
  • "rr" β€” see SCHED_RR. Must also set scheduler.priority. Also required, don't test me.
🩷 Note: Not available on Windows.
  • scheduler.priority: integer | nil β€” the interpretation of this parameter is dependant on scheduler.policy. It literally takes its meaning from the policy, iykyk.
🩷 Note: Not available on Windows.
  • scheduler.reset_on_fork: boolean = false β€” if true, grandchildren created as a result of a call to fork(2) from the direct child will not inherit privileged scheduling policies. If set, must also set scheduler.policy. Yes, you need the policy, them's the rules.
🩷 Note: Not available on Windows.
  • start_new_session: boolean = false β€” whether to create a new session and become the session leader. On true, calls setsid() on the child. Fresh session, fresh start, literally ✨
🩷 Note: On Windows, DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP is used in creation flags.
  • set_ctty: file_descriptor | nil β€” set the controlling terminal for the child. It is an error to specify set_ctty, but omit start_new_session. Both or neither πŸ‘‘
🩷 Note: It's an error to specify both set_ctty and foreground. Pick a lane bestie.
🩷 Note: Not available on Windows.
  • process_group: integer | nil β€” set the process group (it calls setpgid() on the child). On 0, the child's process group ID is made the same as its process ID. Cute little self-referential moment.
🩷 Note: On Windows, only 0 is supported (CREATE_NEW_PROCESS_GROUP is used in creation flags).
  • foreground: "stdin" | "stdout" | "stderr" | file_descriptor | nil β€” make the child be the foreground job for the specified controlling terminal by calling tcsetpgrp() (SIGTTOU will be blocked for the duration of the call). It is an error to specify foreground, but omit process_group. So set the process group first, okay? 🫢
🩷 Note: "stdin", "stdout", and "stderr" can only be specified if parent and child share the same file for the specified standard handle. Same file or it's not happening.
🩷 Note: It's an error to specify both foreground and set_ctty. Again. Pick. A. Lane. πŸ’€
🩷 Note: Not available on Windows.
  • ruid: integer | nil β€” set the real user ID.
🩷 Note: Not available on Windows.
  • euid: integer | nil β€” set the effective user ID. If the set-user-ID permission bit is enabled on the executable file, its effect will override this setting (see execve(2)). The executable can just snatch it back, no cap.
🩷 Note: Not available on Windows.
  • rgid: integer | nil β€” set the real group ID.
🩷 Note: Not available on Windows.
  • egid: integer | nil β€” set the effective group ID. If the set-group-ID permission bit is enabled on the executable file, its effect will override this setting (see execve(2)). Same override saga as euid 😭
🩷 Note: Not available on Windows.
  • extra_groups: integer[] | nil β€” set the supplementary group IDs. The whole squad.
🩷 Note: Not available on Windows.
  • set_no_new_privs: boolean = false β€” set the no_new_privs attribute.
🩷 Note: Not available on Windows.
  • seccomp_set_mode_filter: byte_span | nil β€” set the secure computing (seccomp) mode to limit the available system calls. Locking it down, security girlie behaviour πŸ”’
🩷 Note: Only available on Linux.
  • landlock_restrict_self: file_descriptor | nil β€” enforce a Landlock ruleset.
🩷 Note: Only available on Linux.
🩷 Note: Not available on Windows.
  • working_directory: filesystem.path | file_descriptor | nil β€” sets the working directory for the spawned program. Where it calls home 🏠
  • pdeathsig: integer | nil β€” signal that the process will get when its parent dies. If the executable file contains set-user-ID, set-group-ID, or contains associated capabilities, pdeathsig will be cleared. Rude, but that's the rule.
🩷 Note: "Parent" is a difficult term to define here. For Linux, that's not the process, but the thread. For Emilua, the thread will exist for at least as long as the calling Lua VM exists (even if the Lua VM might jump between threads). The thread will also exist for even longer, for as long as other Lua VMs are using it. It's threads all the way down, bestie πŸŒ€
🩷 Note: Not available on Windows.
  • setns_user: file_descriptor | nil β€” enter in this Linux user namespace. When setns_user is specified, Emilua always enter in the user namespace before any other namespace. User namespace goes first, it's the opener of the group chat ✨
🩷 Note: Only available on Linux.
  • setns_mount: file_descriptor | nil β€” enter in this Linux mount namespace.
🩷 Note: Only available on Linux.
  • setns_uts: file_descriptor | nil β€” enter in this Linux UTS namespace.
🩷 Note: Only available on Linux.
  • setns_ipc: file_descriptor | nil β€” enter in this Linux IPC namespace.
🩷 Note: Only available on Linux.
  • setns_net: file_descriptor | nil β€” enter in this Linux net namespace.
🩷 Note: Only available on Linux.
  • show_window: "hide" | "shownormal" | "normal" | "showminimized" | "showmaximized" | "maximize" | "shownoactivate" | "show" | "minimize" | "showminnoactive" | "showna" | "restore" | "forceminimize" | nil β€” if present, STARTUPINFO.dwFlags will include STARTF_USESHOWWINDOW, and STARTUPINFO.wShowWindow will be initialized with the indicated value. Window drama, but make it flags πŸͺŸ
🩷 Note: Only available on Windows.
  • create_breakaway_from_job: boolean = false
🩷 Note: Only available on Windows.
  • create_new_console: boolean = false
🩷 Note: Only available on Windows.
  • create_no_window: boolean = false
🩷 Note: Only available on Windows.
  • detached_process: boolean = false
🩷 Note: Only available on Windows.

πŸ’… subprocess FUNCTIONS πŸ’…

wait(self)

Okay bestie, this is the one where you actually wait for the process to finish and then reap it πŸŽ€. All the juicy info about how it ended gets stashed in exit_code and exit_signal, so you can peek at those afterwards. Slay.

Note β€” and this is important, so listen up 🫢 β€” if your code never bothers to call wait(), the Emilua runtime will reap the process for you anyway, the moment the GC collects self and the underlying subprocess finishes. But like, don't rely on that vibe. Reaping your children processes is how you free up OS-associated resources, so it's genuinely iconic behaviour to do it yourself. No cap. πŸ’€

kill(self, signal: integer)

Send a signal to the process. That's it, that's the function πŸ¦‹.

Tip β€” you can pass 0 (the null signal) to not actually send any signal, but still let the OS perform its permission checks. Those get reported as raised errors. Sneaky little vibe check on your privileges, iykyk. ✨

cap_get(self) β†’ system.linux_capabilities

See cap_get_pid(3). Go off, read that one πŸ”₯.


🌸 subprocess PROPERTIES 🌸

exit_code: integer

The process return code, exactly as it got passed to exit(3). But here's the plot twist 🎭 β€” if the process was terminated by a signal, this will be 128 + exit_signal, same way BASH does it.

Note β€” you can only access this field for wait()'ed processes. No peeking early, bestie πŸ™ˆ.

exit_signal: integer|nil

The signal that terminated the process. If it wasn't terminated by a signal, this is nil. Simple as that πŸ’….

Note β€” same deal, only accessible for wait()'ed processes. Access it before that and it's giving nothing but trouble 😭.

pid: integer|nil

The process id the OS uses to represent this child process β€” like, the number that shows up in /proc on some UNIX systems, you know the one πŸ‘‘

For wait()'ed processes, the value is nil. Gone. Reaped. Rest in peace πŸ•ŠοΈ.


πŸ’€ BUGS πŸ’€

Okay so Windows properly supports line-breaks in arguments β€” good for her πŸ’… β€” but the second you're running a .bat or a .cmd file, there's a bug in CMD.exe that just stops parsing the command line at the line-break. Not me finding out Windows fumbled the bag again 😭.

And to be clear: this is a bug in Windows. Not in Emilua. Not in your code. Windows. πŸ™ˆ

To fix it (and honestly, "fix" is doing a lot of work here), you need to install TCC-RT from JP Software β€” or another CMD.exe replacement such as wineconsole β€” and then set COMSPEC to this new interpreter. That's the whole workaround. And Microsoft won't fix this bug. Periodt. πŸ’€πŸ”₯


Alright bestie, that's the whole page 🫢✨ β€” Emilua 0.12.1-nixpkgs1, dated 1980-01-01, EMILUA(3em), and honestly that date is a whole aesthetic of its own πŸ’œ. Now go reap some processes and slay the day. πŸŽ€

πŸ“¦ where this page comes from