foruiman 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 906d36f65025e56552898118d3a3e5fec8b81c6046f32a7df9c627f048a89aa6
4
- data.tar.gz: af7c8f777be7302aca9bd3e5cb50a17cc9acd9e2c6036e5d5b8033779d0a492f
3
+ metadata.gz: 5c7f857660af7989aab5c7175e501361ca523780f6eb14f65f7c1620da4040c9
4
+ data.tar.gz: ad726e87ac32a31a1eb07c3a6b9dc4e7eea5f34374e5395153b0c7a73f7831ad
5
5
  SHA512:
6
- metadata.gz: b24d2f3e374331fe3c054c7930f5f1e5cd1e9bcc12e85befa8aeaf7bf8c367b70ab67942c19ac8a3fed56930ce9236a3cf422121a84bc2043584356450035ae0
7
- data.tar.gz: 2b78d3244f014e4cf2e4959a8bedb62b308fb04c38fc8326d151d015983feeb8373a346852f2e730360b4e31a10a591dc63e410990e861571351646201ac9026
6
+ metadata.gz: 2256d813401208d3c672a391cc7ef8670f81cb4cd6ac9d84a22dc510fb612edc52863612d3d9310d84d9a4de05e239339c2a03b8a6ff87469257e81bc016e0c7
7
+ data.tar.gz: 4074ebafd99da6d35bdb99b2fa3aad0ce24eac452ff763e13180bb554db8135a23efdfb2cdf436da8fe001838f1db861445efa43a7e1be42a1f85ba95745c715
data/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.4.0
4
+
5
+ - Forward USR1/USR2 to child process groups in both TUI and plain mode, preserving
6
+ application handlers and the supervisor's independent-process policy.
7
+ - Fix inherited terminal stdin in plain mode while retaining descendant cleanup.
8
+ - Validate ports after process selection, preserving each entry's original offset.
9
+ - Allow a Procfile process named `all` with its own logs and controls, separate
10
+ from the aggregate tab.
4
11
 
5
12
  ## 0.3.0
6
13
 
data/README.md CHANGED
@@ -56,6 +56,13 @@ interrupts the selected process. Debugger tab completion is not forwarded.
56
56
  Press `s` to stop the selected process, or all processes from `all`; on a stopped
57
57
  process tab, `s` starts it again.
58
58
 
59
+ The aggregate `all` tab uses shortcut `0`. A process named `all` has its own
60
+ numbered tab and independent controls. USR1 and USR2 sent to Foruiman are forwarded
61
+ to every child process group for application-defined handling.
62
+
63
+ In plain mode, children inherit stdin, including terminal input. Multiple children
64
+ reading stdin share that stream; use TUI input controls to target one process.
65
+
59
66
  ## Options
60
67
 
61
68
  | Option | Default | Purpose |
@@ -113,7 +120,9 @@ bundle exec ruby script/smoke_gem.rb
113
120
  Releases use RubyGems trusted publishing. Configure the publisher once with
114
121
  repository `Nuzair46/foruiman`, workflow `release.yml`, and environment `release`.
115
122
 
116
- To publish, update `lib/foruiman/version.rb` on `main`, open the
123
+ To publish, update `lib/foruiman/version.rb` and `CHANGELOG.md`, then run
124
+ `bundle install` to update the version in `Gemfile.lock`. Commit all three files
125
+ to `main`; CI requires the lockfile to match the gem version. Open the
117
126
  [Release workflow](https://github.com/Nuzair46/foruiman/actions/workflows/release.yml),
118
127
  choose **Run workflow**, and select `main`. GitHub Actions runs the full CI suite,
119
128
  creates the version tag, and publishes the gem to RubyGems.org.
data/docs/ARCHITECTURE.md CHANGED
@@ -3,14 +3,20 @@
3
3
  `Procfile` retains Foreman's ordered parser/writer API with strict file validation.
4
4
  `Env` retains its quoting rules and adds a non-mutating precedence merge. `Process`
5
5
  wraps `/bin/sh -c` with a new process group, two output streams, and configurable stdin.
6
- `CLI < Thor` validates the full configuration before starting the engine.
6
+ The shell catches USR1/USR2 while waiting so group forwarding does not kill the
7
+ wrapper ahead of a signal-aware application. Executed programs receive normal
8
+ signal dispositions and can install their own handlers.
9
+ `CLI < Thor` validates configuration and selected port allocations before opening
10
+ the TUI or starting children. `check` validates every allocation. Original Procfile
11
+ offsets survive selection; embedded `start` validates its entire batch before
12
+ launching any child, and `restart` validates before changing process state.
7
13
  `Configuration` safely loads `.foreman` defaults, merges explicit CLI values,
8
14
  resolves Foreman-compatible paths and environments, and validates numeric options.
9
15
  `run` uses `exec` for one-off commands with direct terminal IO and exact exit status.
10
16
 
11
17
  `Engine` owns registration, PID/group tracking, nonblocking pipes, child reaping,
12
18
  and lifecycle transitions. A single caller thread drives all mutations. Signal
13
- handlers only set a flag and wake a self-pipe. The loop reaps only its own direct
19
+ handlers only queue the signal and wake a self-pipe. The loop reaps only its own direct
14
20
  children, services bounded reads in round-robin order, checks TERM deadlines, and
15
21
  starts pending replacements once old groups and pipes are finished. Group existence
16
22
  is tracked independently of leader status; Linux `/proc` distinguishes running
@@ -47,8 +53,9 @@ end
47
53
  ```
48
54
 
49
55
  Call engine methods and event listeners on the driving thread; this is not a
50
- cross-thread messaging API. `run` handles INT/TERM/HUP and restores existing
51
- handlers. An embedding loop that drives `tick` directly owns its signal handling.
56
+ cross-thread messaging API. `run` handles INT/TERM/HUP shutdown, forwards USR1/USR2
57
+ to owned groups, and restores existing handlers. An embedding loop that drives
58
+ `tick` directly owns its signal handling.
52
59
  `close` disconnects observers so a failed renderer/output consumer cannot prevent
53
60
  process cleanup. `term_timeout:` controls the shutdown grace period (CLI `-t`,
54
61
  default five seconds). `exit_on: :all` retains independent processes; `:any` or
@@ -56,8 +63,15 @@ default five seconds). `exit_on: :all` retains independent processes; `:any` or
56
63
  its exit code. Intentional stops/restarts do not trigger the policy. `state(name)` exposes lifecycle state for
57
64
  rendering; callers should not mutate it.
58
65
 
59
- Children inherit the configured input stream in plain and embedded use. Before
60
- startup, `manage_input!` gives each child a dedicated pseudo-terminal while the
66
+ Children inherit the configured input stream in plain and embedded use. For a
67
+ terminal input stream, `Process` forks, calls `setsid`, and execs the shell so reads
68
+ do not stop with SIGTTIN in a background group of the supervisor's session. The
69
+ child keeps its inherited terminal descriptor and its own process group; a pipe
70
+ reports exec failures synchronously. This does not give the child a controlling
71
+ terminal for `/dev/tty` or shell job control. Multiple inherited readers compete
72
+ for the same input. Nonterminal input keeps the ordinary `spawn` path.
73
+
74
+ Before startup, `manage_input!` gives each child a dedicated pseudo-terminal while the
61
75
  caller retains the real terminal; `write_input(name, bytes)` forwards input,
62
76
  `interrupt_process(name)` sends SIGINT to that process group, and `resize_inputs` keeps
63
77
  the pseudo-terminals sized with the UI. The TUI uses this mode so watch commands
@@ -70,7 +84,10 @@ line erasure before recording child output; screen controls remain suppressed.
70
84
 
71
85
  `LogStore` uses per-process `Ring` instances and an aggregate `Ring`, each with O(1)
72
86
  append, eviction, record replacement, and identity lookup. Immutable `Data` records
73
- are shared. A partial record has one sequence identity; completing it replaces
87
+ are shared. `logs.all` or `logs[:all]` accesses the aggregate; `logs["all"]` accesses
88
+ a process actually named `all`. The TUI uses symbol `:all` for aggregate state and
89
+ string names for processes, with `State#aggregate?` defining control scope.
90
+ A partial record has one sequence identity; completing it replaces
74
91
  retained references without re-inserting an already evicted aggregate record.
75
92
  Sequence order represents first observation, not line completion time. Actual
76
93
  memory use depends on line length and ring capacity; retained text is bounded by
data/docs/ASSUMPTIONS.md CHANGED
@@ -9,7 +9,8 @@ The missing RFC was requested during implementation; these choices fill the gaps
9
9
  - Foreman's `-f`, `-d`, `-e`, `-p` aliases; `--log-lines`, `--no-tui`, `--no-dotenv`.
10
10
  - Default capacity 10,000 records per ring.
11
11
  - Process tabs in entry order, then `all`; select `all` initially.
12
- - `all` reserved as a process name to avoid an ambiguous aggregate tab.
12
+ - The aggregate tab has a separate internal identity, so `all` is also a valid
13
+ process name. This replaces the original reserved-name assumption.
13
14
  - Vim-style and arrow navigation, digits, `f`, Space, `r`/`R`, `s`/`S`, `?`, `q`.
14
15
  - Generated `PS=name.1`, as in Foreman; no other generated variables.
15
16
  - Relative file paths resolved against invocation directory or explicit `-d`.
@@ -3,23 +3,34 @@
3
3
  Foruiman supports common Foreman development workflows. It is not a drop-in
4
4
  replacement for every Foreman feature.
5
5
 
6
+ See the [2026-09-27 compatibility audit](FOREMAN_AUDIT.md) for issue-backed gaps,
7
+ direct comparisons with Foreman 0.90.0, and the implementation scope for compatible
8
+ development workflows in this TUI. Exporters, the Foreman Ruby API, older
9
+ runtimes, and exact legacy presentation are excluded from that work. Its
10
+ modernization assessment recommends retaining consistent app-root execution,
11
+ strict configuration/env-file validation, and meaningful TUI options. The four
12
+ selected improvements are implemented in the unreleased changes below; the audit
13
+ preserves the original findings separately from their implementation status.
14
+
6
15
  | Area | Foruiman behavior |
7
16
  | --- | --- |
8
17
  | Identity | `foruiman` gem/executable, `Foruiman` namespace, version 0.3.0 |
9
18
  | Runtime | Ruby 3.2+, POSIX process groups; Linux CI |
10
19
  | CLI | Thor-based `start [PROCESS]`, `run COMMAND [ARGS...]`, `check`, `version`, and `help` |
11
20
  | Excluded features | No export, scaling/formation, forced color, or timestamp toggle |
12
- | Procfile validation | Reject malformed lines, duplicates, empty commands, and reserved `all`; report line numbers |
21
+ | Procfile validation | Reject malformed lines, duplicates, and empty commands; report line numbers. A process named `all` is valid |
13
22
  | Working directory | Procfile directory unless `-d`; explicit `-f` and `-e` paths resolve from invocation |
14
23
  | Environment | Explicit `-e file1,file2` replaces default `.env` loading; later files win; parent `ENV` stays untouched |
15
- | Ports | `-p` or `.foreman` port, then loaded `PORT`, then 5000; plus 100 per original entry |
24
+ | Ports | `-p` or `.foreman` port, then loaded `PORT`, then 5000; plus 100 per original entry. `start PROCESS` validates only that allocation; `check` validates all |
16
25
  | Expansion | `/bin/sh -c` performs shell expansion; no Ruby string substitution |
17
26
  | Instances | Exactly one instance per entry; `PS=name.1` |
18
27
  | Failures | Independent by default; `--exit-on any` stops on any natural exit, `failure` only on an unsuccessful exit |
19
28
  | Restart | Stop only the affected group, wait for descendants and output, then replace |
20
29
  | Shutdown | TERM, configurable `-t` grace (default five seconds), KILL; track groups after leader exit; restore prior signal handlers |
30
+ | Signals | USR1/USR2 reach every owned process group; applications choose how to handle them. INT/TERM/HUP request orderly shutdown |
21
31
  | Output | Separate stdout/stderr metadata; bounded logs and live partial records |
22
- | Terminal | Tabs, independent scroll/follow, restart, start/stop, and selected-process input controls |
32
+ | Terminal | Tabs, independent scroll/follow, restart, start/stop, and selected-process input controls; aggregate `0: all` is separate from a process named `all` |
33
+ | Plain stdin | Inherited stdin supports pipes, terminal line input, raw reads, and EOF; concurrent readers share the stream |
23
34
  | Plain exit | Default status 0/1; automatic group shutdown preserves its triggering exit code (128 + signal for a signal exit); explicit orderly shutdown returns 0 |
24
35
  | Interactive exit | Remain open after all processes exit by default; automatic shutdown policies close the TUI after cleanup |
25
36
 
@@ -67,8 +78,8 @@ does not generate per-process `PORT` or `PS` values or open the TUI.
67
78
  - A `.foreman` file now supplies defaults. Remove unsupported keys before using it.
68
79
 
69
80
  Use `--exit-on any` for Foreman's stop-on-first-exit behavior. Intentional TUI
70
- stops and restarts do not trigger that policy. Exact Foreman log formatting,
71
- USR1/USR2 forwarding, and its Ruby embedding API remain outside compatibility.
81
+ stops and restarts do not trigger that policy. Exact Foreman log formatting and
82
+ its Ruby embedding API remain outside compatibility.
72
83
 
73
84
  Full-screen child terminal applications and background daemonization are outside
74
85
  this release. Remote process control, persistence across Foruiman sessions,
@@ -0,0 +1,297 @@
1
+ # Foreman compatibility audit — 2026-09-27
2
+
3
+ **Scope: Foreman development workflows in a TUI Procfile runner.** The target
4
+ is to reuse Procfiles, environment files, applicable `.foreman` settings, and
5
+ `start` / `run` commands on Foruiman's supported Ruby/POSIX platforms. The TUI's
6
+ independent processes, restarts, input controls, bounded logs, and presentation
7
+ remain intentional behavior.
8
+
9
+ **Original modernization recommendation:** drop three of the nine proposed ports in
10
+ full, simplify two others, and treat formation as an optional product feature.
11
+ Prioritize signal forwarding, working terminal stdin, and selected-process port
12
+ validation; removing the process-name collision with `all` is a useful follow-up.
13
+ The table below assesses all nine items individually.
14
+
15
+ ## Implementation status
16
+
17
+ The selected work is implemented in the unreleased changes:
18
+
19
+ - **1 — USR1/USR2:** forward to owned process groups in TUI and plain mode, including
20
+ descendants. Signal-aware applications and their waiting shell wrappers stay
21
+ alive; applications without a handler retain the default signal behavior.
22
+ - **2 — Plain terminal stdin:** children can read lines, raw bytes, and EOF from
23
+ inherited terminal stdin. Isolated child sessions preserve process-group cleanup.
24
+ This supplies terminal IO, not a new controlling terminal or shell job control.
25
+ - **6 — Process named `all`:** process logs, input, and controls are separate from
26
+ the aggregate tab, which keeps shortcut `0`.
27
+ - **8 — Selected ports:** validate only the selected process at startup without
28
+ changing its original +100 offset. Whole-app startup and `check` still reject
29
+ invalid allocations before launching children. Positive base ports remain required.
30
+
31
+ Regression coverage is in [terminal integration](../spec/integration/terminal_spec.rb),
32
+ [supervisor integration](../spec/integration/supervisor_spec.rb),
33
+ [CLI](../spec/foruiman/cli_spec.rb), and [TUI](../spec/foruiman/tui_spec.rb) specs.
34
+ The remaining recommendations retain existing behavior or defer formation.
35
+
36
+ The baseline measurements and original assessment below describe the pre-fix
37
+ version. They are preserved as audit evidence, not claims of current failures.
38
+
39
+ ## Baseline and evidence
40
+
41
+ - Foruiman: commit `a2f8ebb3a81e4f026284f78c5a1bd615f3f3068e`, version 0.3.0.
42
+ - Foreman: installed gem 0.90.0, also the latest version returned by the
43
+ [RubyGems API](https://rubygems.org/api/v1/versions/foreman/latest.json) at audit time.
44
+ - Upstream `main`: [`f65ddba83932bd4670e014389d6e27ea1e20b469`](https://github.com/ddollar/foreman/tree/f65ddba83932bd4670e014389d6e27ea1e20b469),
45
+ matching our [recorded import](UPSTREAM.md).
46
+ - Execution environment: Linux, MRI Ruby 3.4.5. The existing RSpec suite passed:
47
+ **149 examples, 0 failures**, seed 59308.
48
+ - Ran **56 paired CLI probes**, plus **one paired controlling-terminal input
49
+ reproduction**. Each runner used a fresh temporary directory. Probes captured
50
+ stdout, stderr, exit status, termination signal, and timeouts. Some probes
51
+ deliberately used invalid input; this is not a compatibility percentage.
52
+ - Retrieved all **442 upstream issues: 53 open, 389 closed**, excluding PRs.
53
+ Screened titles, read all open issue bodies and relevant closed reports, and
54
+ followed selected resolution comments. This is not a claim to have reproduced
55
+ every historical report or read every comment.
56
+ - The [Foruiman tracker](https://github.com/Nuzair46/foruiman/issues) contained no
57
+ standalone issues and five closed PRs. [PR #5](https://github.com/Nuzair46/foruiman/pull/5)
58
+ explicitly excluded formation, export, exact log formatting, and USR forwarding.
59
+
60
+ The [Foreman manual](https://ddollar.github.io/foreman/), pinned implementation,
61
+ actual 0.90.0 behavior, and issue history were compared. An open issue is evidence
62
+ of a reported problem or requested feature, not proof that it still exists in
63
+ 0.90.0. Historical Foreman releases also differ from each other.
64
+
65
+ The runtime comparison uses the [upstream CI matrix](https://github.com/ddollar/foreman/blob/f65ddba83932bd4670e014389d6e27ea1e20b469/.github/workflows/ci.yml)
66
+ and [Foreman CLI/engine source](https://github.com/ddollar/foreman/tree/f65ddba83932bd4670e014389d6e27ea1e20b469/lib/foreman),
67
+ not an assumption that an unrestricted gemspec proves every Ruby version works.
68
+
69
+ ## Original modernization assessment of the nine items
70
+
71
+ The goal is a predictable development supervisor with a TUI. Existing useful
72
+ workflows matter; reproducing incidental parsing and CLI behavior is optional.
73
+ Dropping a port means retaining a documented difference, not claiming parity.
74
+
75
+ | Original item | Recommendation | Reason and intended contract |
76
+ | --- | --- | --- |
77
+ | 1. USR1/USR2 forwarding | **Keep the fix.** | These signals currently terminate the shared supervisor instead of reaching children. The TUI does not replace application-defined signal handling. Forward the signals without closing the supervisor; do not reinterpret them as TUI restart shortcuts. [Engine](../lib/foruiman/engine.rb), upstream [#673](https://github.com/ddollar/foreman/issues/673). |
78
+ | 2. Terminal stdin in `--no-tui` | **Keep the fix.** | A supported execution mode should allow children to read its controlling terminal. Correct the process-group/input interaction while preserving descendant cleanup. This is a correctness problem, not a legacy UI preference. [Process spawning](../lib/foruiman/process.rb). |
79
+ | 3. Formation, scaling and exclusions | **Defer full formation unless it is a product requirement.** | Multiple instances and startup exclusions remain useful capabilities; they are not obsolete just because the interface has tabs. They add instance identity, controls and port allocation work. Existing `start PROCESS` covers a single selected process. TUI stop buttons are not a substitute for excluding a process before it starts. If `-m` is implemented, preserve its counts/exclusions and default-zero semantics rather than silently changing the meaning of an existing flag. Upstream [#398](https://github.com/ddollar/foreman/issues/398). |
80
+ | 4. Reusing `.foreman` defaults | **Keep supported settings; drop the proposed permissiveness.** | Continue honoring supported env/root/port/timeout defaults and CLI overrides. There is no need to silently accept export-only `app`, `user`, `log`, `run`, or `template` keys, or treat mistyped/null values as absent. Retain clear validation and document removing unsupported keys during migration. This avoids suggesting a setting has an effect when it does not. [Configuration](../lib/foruiman/configuration.rb). |
81
+ | 5. Arbitrary `run` working directory | **Do not port Foreman's special case.** | Keep the current consistent application-root cwd for named and arbitrary commands. `-d` has one meaning, and relative commands work relative to the configured app. Foreman's arbitrary-command invocation-cwd behavior is a migration difference, not a correctness requirement for this product. Use `run -d . ...` when invocation cwd is wanted; this also selects invocation-root default `.env` lookup. An explicit `-e` can preserve a different env-file choice. [CLI](../lib/foruiman/cli.rb). |
82
+ | 6. Valid process name `all` | **Keep as a useful design improvement, after correctness fixes.** | An aggregate tab need not prevent a real process from being named `all`. Use a separate internal identity for the aggregate view. This removes an unnecessary UI/name collision without adopting permissive parsing for malformed or duplicate entries. The current explicit reserved-name error remains an acceptable documented migration restriction until changed. [Procfile](../lib/foruiman/procfile.rb). |
83
+ | 7. Legacy presentation-option acceptance | **Do not add acceptance-only compatibility flags.** | Do not add `-c`, `--color`, or `--timestamp` switches solely to accept and ignore them. The TUI owns presentation. Keep a clear unsupported-option error; if color/timestamp control is later useful in the plain fallback, implement it as a real feature with documented behavior. |
84
+ | 8. Port allocation for selected processes | **Fix selection validation; defer port-zero support.** | `start web -p 65500` should not fail because an unselected worker would receive 65600. Validate selected instances and preserve original offsets. Do not port arbitrary `to_i` coercion. Base port zero is a separate allocation policy: the +100 rule would give the second type 100, so accepting zero is not an automatic-port policy for the whole app. Keep positive-port validation until zero has an explicit use case and defined semantics. [Engine](../lib/foruiman/engine.rb). |
85
+ | 9. Environment-file option edge cases | **Do not port permissive list parsing.** | Keep rejecting `-e ''` and `-e custom.env,`. Use the existing `--no-dotenv` to disable default loading, and `-e custom.env` for a nonempty list. `--no-dotenv` still permits explicit `-e` files, including a configured `.foreman` env setting; remove that explicit setting too if the intent is to load no file. Preserve precedence and valid multiple-file support. [Configuration](../lib/foruiman/configuration.rb). |
86
+
87
+ This leaves **three correctness fixes**, **one process-name improvement**, and
88
+ **formation as a separate optional feature**. The other recommendations preserve
89
+ current behavior and require clear migration documentation, not porting code.
90
+
91
+ ## Differences excluded from this version's compatibility work
92
+
93
+ The wider exclusions below remain in addition to the recommendations above.
94
+ They do not imply that missing features exist or that an observed difference was
95
+ incorrect.
96
+
97
+ | Exclude | Treatment |
98
+ | --- | --- |
99
+ | Exporters and custom export templates | No systemd, upstart, launchd, runit, inittab, bluepill, daemon or supervisord export work. Keep clear errors for unsupported config keys and document removing them during migration. |
100
+ | Ruby embedding/extension API and old executable identity | Keep the `Foruiman` namespace and `foruiman` gem/executable. No `Foreman::*` adapter, replacement gem identity, or requirement to satisfy existing `gem list -i foreman` checks. Changing `bin/dev` to invoke Foruiman is part of migration. |
101
+ | Older Ruby and native Windows support | Keep the existing Ruby 3.2+ and POSIX scope. Platform expansion is separate work. Linux tests do not establish macOS correctness; verify any platform-specific claim separately. |
102
+ | Exact log format, raw-log transport and cross-stream ordering | Tabs, timestamps, stream labels, lifecycle text, colors and separate stdout/stderr metadata can differ. TUI control-sequence interpretation, bounded records and long-line splitting are intentional. Byte-identical plain output, raw JSON transport and exact merged ordering are not compatibility goals for this version. |
103
+ | Default stop-on-first-exit and aggregate exit status | Keep independent processes, isolated restarts, remaining open after children exit, aggregate 0/1 completion status, and explicit user quit returning 0. Retain the existing `--exit-on any` option and verify its documented triggering-code behavior. There is no unique Foreman-style triggering process under the independent default. |
104
+ | No-argument help, `tree`, and exact diagnostics | Keep default `start`, full-screen interaction, shortcuts, and current help/error wording and streams. Do not restore `tree` solely for Thor parity. |
105
+ | Permissive parsing and `check` behavior | Keep clear validation of malformed/duplicate/empty entries and the broader environment/port checks documented for `check`. The `all` name collision is resolved without relaxing those checks. |
106
+ | Upstream expansion and signal-status quirks | Keep shell quoting/variable boundaries and direct `run` exec. Do not copy substring replacement that expands single-quoted variables or corrupts `$FOOBAR`, return success for a signaled command just because upstream does, or reject arbitrary `run` solely because an unrelated Procfile is empty. |
107
+ | Production init, escaped daemons and unrelated feature requests | No PID 1 init guarantee, daemon management, remote sessions, automatic reload/sequencing, or features belonging to Puppet Foreman/node-foreman. Keep cleanup of owned development process groups. |
108
+
109
+ ## Important details from the reproductions
110
+
111
+ **Terminal input is a real regression, independent of the TUI.** The stdin
112
+ reproduction used a controlling terminal created by `PTY.spawn`, not just a PTY
113
+ file descriptor. The child wrote `READY`, then called `STDIN.gets`. Foreman
114
+ printed `GOT:hello` and exited normally. Foruiman's background process group
115
+ stopped on terminal input; after two seconds the harness requested shutdown and
116
+ the configured timeout escalated to KILL. Existing terminal specs mostly use
117
+ `PTY.open` plus `Process.spawn`, which does not establish that slave as the
118
+ child's controlling terminal. Add a controlling-terminal regression test before
119
+ changing process group or input handling.
120
+
121
+ **Excluded evidence: exact signal exit parity.** For a direct
122
+ `ruby child.rb` that sends itself TERM, Foreman returned 0. Foruiman's extra shell
123
+ reported 143 internally and the default policy returned 1. For `run ruby
124
+ child.rb`, Foreman returned 0 while Foruiman itself was terminated by TERM
125
+ (`Process::Status#termsig == 15`). A shell may display 143 for a signaled command,
126
+ but wait-status consumers can distinguish it from an ordinary exit 143. Foreman
127
+ 0.90.0's treatment of a directly signaled child is an upstream quirk, not a
128
+ recommendation to hide failures.
129
+
130
+ **Excluded evidence: upstream command-expansion quirks.** With
131
+ `.env` containing `FOO=hello` and `FOOBAR=world`:
132
+
133
+ | Procfile command | Foreman child output | Foruiman child output |
134
+ | --- | --- | --- |
135
+ | `printf '%s\n' '$FOO'` | `hello` | `$FOO` |
136
+ | `printf '%s\n' "$FOOBAR"` | `helloBAR` | `world` |
137
+
138
+ With `FOO=$(printf expanded)` and `printf '%s\n' "$FOO"`, Foreman printed
139
+ `expanded`; Foruiman printed the literal `$(printf expanded)`. Foruiman's normal
140
+ shell semantics are retained for this TUI scope; this difference is documented
141
+ and is not an implementation task.
142
+
143
+ **Port limits can reject an otherwise usable selected process.** This is not
144
+ only about invalid port strings: 65500 is valid for `web`, but validation of an
145
+ unselected second entry rejects the entire command. Formation implementation
146
+ would need to allocate selected instances without renumbering their original
147
+ Procfile positions. The selection-validation bug can be fixed independently of
148
+ formation support.
149
+
150
+ **No signal orphan was observed in the dedicated USR1 cleanup probe.** The
151
+ supervisor still terminated instead of forwarding. Report that confirmed
152
+ failure without assuming every such termination leaves a live child behind.
153
+
154
+ ## Minimal reproductions
155
+
156
+ Use a fresh directory for each fixture. Run the listed arguments with Foreman
157
+ 0.90.0 and then Foruiman 0.3.0. Use `--no-tui` on Foruiman when inspecting a short-lived command without
158
+ the persistent TUI. Compare process behavior, cwd, environment and argument
159
+ acceptance. These reproduce the original findings, including differences now
160
+ recommended for retention; they are not all failing acceptance criteria. Exact
161
+ log text and aggregate status under independent supervision are outside the
162
+ scoped comparison.
163
+
164
+ | Fixture | Command arguments | Observed difference |
165
+ | --- | --- | --- |
166
+ | `Procfile`: `web: echo hello` | `start -m web=2` | Two instances versus unknown switch. |
167
+ | Same Procfile; `.foreman`: `app: demo` | `start` | Runs versus unsupported config key. |
168
+ | Create empty `app/` directory; no Procfile | `run -d app ruby -e 'puts Dir.pwd'` | Invocation directory versus `app/`. |
169
+ | `Procfile`: `all: echo hello` | `start` | Runs versus reserved-name error. |
170
+ | `Procfile`: `web: echo hello` | `start -c` or `start --no-timestamp` | Accepted versus unknown switch. |
171
+ | `Procfile`: `web: echo hello`, then `worker: echo worker` on the next line | `start web -p 65500` | Runs web versus rejecting an unselected worker's port. |
172
+ | `.env`: `FOO=file`, with inherited `FOO=parent` | `run -e '' ruby -e 'puts ENV["FOO"]'` | Prints parent versus rejecting empty env option. |
173
+
174
+ For terminal input, create `reader.rb`:
175
+
176
+ ```ruby
177
+ STDOUT.sync = true
178
+ puts "READY"
179
+ puts "GOT:#{STDIN.gets}"
180
+ ```
181
+
182
+ Use `web: ruby reader.rb` as the Procfile. In a real foreground terminal, run
183
+ `foreman start -t 0.1`, type `hello` and Enter after READY. Repeat with
184
+ `foruiman start --no-tui -t 0.1`. The first prints `GOT:hello`; the second stops
185
+ the reader. Ctrl-C requests shutdown. A pipe-fed stdin test will not reproduce
186
+ this controlling-terminal problem.
187
+
188
+ For signal forwarding, create `signals.rb`:
189
+
190
+ ```ruby
191
+ STDOUT.sync = true
192
+ Signal.trap(:USR1) { puts "RECEIVED USR1" }
193
+ Signal.trap(:USR2) { puts "RECEIVED USR2" }
194
+ puts "READY"
195
+ sleep
196
+ ```
197
+
198
+ Use `web: ruby signals.rb` as the Procfile. After READY, send `kill -USR1 PID`
199
+ from another terminal to the **supervisor's** PID. Foreman prints the forwarded
200
+ message and remains running; Foruiman terminates. Repeat in a fresh invocation
201
+ for USR2. Terminate a surviving Foreman supervisor normally after observing it.
202
+
203
+ ## Behaviors already supported
204
+
205
+ The paired probes confirmed common environment precedence: inherited variables
206
+ are available, `.env` overrides them, explicit `-e first,last` replaces the
207
+ default `.env`, and later explicit files win. CLI port overrides `.foreman`,
208
+ loaded `PORT` and inherited `PORT`. Selecting the second process retains its
209
+ original +100 port offset and `PS=worker.1`.
210
+
211
+ Nested `-f` moves the supervised/named-command working directory while default
212
+ `.env` still comes from invocation unless `-d` is supplied. Named `run` uses the
213
+ Procfile root. Arbitrary `run` works without a Procfile, preserves argument
214
+ boundaries and child switches, and preserves ordinary nonzero exit codes.
215
+
216
+ The existing passing suite additionally covers quoted/escaped environment
217
+ values, CRLF, missing explicit environment files, stdin pipes, shutdown timeout
218
+ escalation, bounded log retention, cleanup of descendants in managed groups,
219
+ isolated restarts, and TUI terminal restoration. These are useful tests, but
220
+ they assert Foruiman's own contract; passing them is not a Foreman conformance
221
+ certificate.
222
+
223
+ ## Issue-history conclusions
224
+
225
+ The most useful closed reports are [#673](https://github.com/ddollar/foreman/issues/673)
226
+ (USR forwarding), [#676](https://github.com/ddollar/foreman/issues/676)
227
+ (context for our intentional lifetime difference), [#398](https://github.com/ddollar/foreman/issues/398)
228
+ (formation defaults), [#274](https://github.com/ddollar/foreman/issues/274) and
229
+ [#275](https://github.com/ddollar/foreman/issues/275) (one-off argument handling),
230
+ [#271](https://github.com/ddollar/foreman/issues/271) (run without a Procfile),
231
+ [#230](https://github.com/ddollar/foreman/issues/230) (named run), and
232
+ [#489](https://github.com/ddollar/foreman/issues/489) (interactive run signals).
233
+ They identify relevant command/process regressions and intentional differences;
234
+ the modernization assessment above determines which observations warrant work.
235
+
236
+ Do not restore historical failures as features. Unknown process selection
237
+ ([#565](https://github.com/ddollar/foreman/issues/565),
238
+ [#568](https://github.com/ddollar/foreman/issues/568)) hung in the 0.90.0 probe
239
+ until the harness sent TERM; Foruiman's immediate error is an improvement.
240
+ Foreman's environment interpolation changed between historical versions
241
+ ([#561](https://github.com/ddollar/foreman/issues/561)), so compatibility with
242
+ every Foreman release cannot be inferred from matching 0.90.0.
243
+
244
+ The following accounts for all 53 currently open issues. Grouping describes
245
+ their relevance; it does not claim each reported application failure is fixed.
246
+
247
+ | Open upstream issues | Assessment for Foruiman |
248
+ | --- | --- |
249
+ | [#779](https://github.com/ddollar/foreman/issues/779), [#775](https://github.com/ddollar/foreman/issues/775), [#768](https://github.com/ddollar/foreman/issues/768), [#708](https://github.com/ddollar/foreman/issues/708) | Child/descendant cleanup. Managed process-group cleanup is tested; escaped sessions, Spring daemons and exact reported applications remain unverified. Closed [#810](https://github.com/ddollar/foreman/issues/810) is a related npx case whose reporter used an exec wrapper. |
250
+ | [#681](https://github.com/ddollar/foreman/issues/681) | Excluded: successful child exit stopping peers is Foreman behavior. Preserve Foruiman's independent default. |
251
+ | [#703](https://github.com/ddollar/foreman/issues/703) | Debugger echo/input: Foruiman offers selected-process TUI input, but the plain-mode controlling-terminal failure remains. |
252
+ | [#808](https://github.com/ddollar/foreman/issues/808), [#750](https://github.com/ddollar/foreman/issues/750), [#754](https://github.com/ddollar/foreman/issues/754) | Logging requests. Exact presentation/raw logging is excluded. Acceptance-only color/timestamp aliases are also not recommended. Real display controls can be added independently if useful. |
253
+ | [#702](https://github.com/ddollar/foreman/issues/702), [#755](https://github.com/ddollar/foreman/issues/755) | Environment precedence and physical multiline `.env` values. Current parsers share Foreman's limitations. Dotenv compatibility is a different target; do not silently reverse precedence or assume full dotenv syntax. Escaped `\n` in double quotes is supported. |
254
+ | [#784](https://github.com/ddollar/foreman/issues/784), [#751](https://github.com/ddollar/foreman/issues/751), [#715](https://github.com/ddollar/foreman/issues/715), [#714](https://github.com/ddollar/foreman/issues/714) | PORT defaults, `.foreman` configuration and offsets. Common semantics match; fix selected-process validation and treat scaling as an optional feature. Do not change the default 5000 to resolve an OS-specific port conflict while claiming identical defaults. |
255
+ | [#705](https://github.com/ddollar/foreman/issues/705) | Optional product feature: multiple worker instances require formation or an equivalent explicit design. Historical `-c` concurrency syntax in the report is not the 0.90.0 contract; use `-m` if Foreman formation is implemented. |
256
+ | [#794](https://github.com/ddollar/foreman/issues/794), [#709](https://github.com/ddollar/foreman/issues/709), [#704](https://github.com/ddollar/foreman/issues/704), [#668](https://github.com/ddollar/foreman/issues/668), [#695](https://github.com/ddollar/foreman/issues/695) | Excluded: exporters and deployment templates. Their absence is not a blocker for this TUI scope. The old `File.exists?` failure in #794 was already fixed in the baseline source despite the issue remaining open. |
257
+ | [#797](https://github.com/ddollar/foreman/issues/797), [#793](https://github.com/ddollar/foreman/issues/793), [#789](https://github.com/ddollar/foreman/issues/789), [#733](https://github.com/ddollar/foreman/issues/733) | Excluded: native Windows. Foruiman remains POSIX-only; no Windows compatibility or fix is claimed. |
258
+ | [#759](https://github.com/ddollar/foreman/issues/759) | Windows DSC export is an upstream feature request, not a supported Foreman format that must be copied. |
259
+ | [#766](https://github.com/ddollar/foreman/issues/766), [#764](https://github.com/ddollar/foreman/issues/764), [#748](https://github.com/ddollar/foreman/issues/748) | Default-start UX is intentional and excluded from parity. Normal `start -f` path/option handling remains applicable and covered. |
260
+ | [#762](https://github.com/ddollar/foreman/issues/762), [#707](https://github.com/ddollar/foreman/issues/707) | Environment usage questions. Parent env is inherited; explicit files and their overriding precedence must stay consistent. |
261
+ | [#679](https://github.com/ddollar/foreman/issues/679), [#684](https://github.com/ddollar/foreman/issues/684), [#746](https://github.com/ddollar/foreman/issues/746), [#699](https://github.com/ddollar/foreman/issues/699), [#713](https://github.com/ddollar/foreman/issues/713) | Requests/questions about reload, code watching, sequencing, conditional Procfiles and remote log views. They are not existing Foreman requirements. HUP means shutdown in both implementations. Foruiman's manual restarts do not imply automatic reload support. |
262
+ | [#796](https://github.com/ddollar/foreman/issues/796), [#786](https://github.com/ddollar/foreman/issues/786), [#782](https://github.com/ddollar/foreman/issues/782), [#693](https://github.com/ddollar/foreman/issues/693), [#731](https://github.com/ddollar/foreman/issues/731), [#645](https://github.com/ddollar/foreman/issues/645), [#700](https://github.com/ddollar/foreman/issues/700) | App-specific failures, startup/output/shutdown reports, or insufficient reproductions. No verified Foreman conformance change can be concluded from their titles alone. Real Rails/Yarn/Redis/Sidekiq fixtures remain useful validation work. |
263
+ | [#752](https://github.com/ddollar/foreman/issues/752), [#688](https://github.com/ddollar/foreman/issues/688) | Historical Thor dependency failures. Foreman 0.90.0 now depends on Thor ~>1.4; Foruiman allows >=1.3,<2. Exact Thor behavior should be pinned in differential tests. |
264
+ | [#811](https://github.com/ddollar/foreman/issues/811), [#806](https://github.com/ddollar/foreman/issues/806), [#783](https://github.com/ddollar/foreman/issues/783), [#778](https://github.com/ddollar/foreman/issues/778), [#776](https://github.com/ddollar/foreman/issues/776), [#767](https://github.com/ddollar/foreman/issues/767) | Project links, another fork, release naming and style tooling. No runtime compatibility requirement. |
265
+ | [#736](https://github.com/ddollar/foreman/issues/736), [#718](https://github.com/ddollar/foreman/issues/718) | Reports concern Puppet's Foreman or node-foreman, not this Ruby CLI. |
266
+
267
+ ## Recommended implementation order
268
+
269
+ 1. **Fix process-control correctness.** Forward USR1/USR2 and fix real
270
+ controlling-terminal stdin in `--no-tui`. Preserve isolated restarts,
271
+ process-group cleanup, user quit, and optional shutdown policies.
272
+ 2. **Fix selected-process port validation.** Check allocations for selected
273
+ processes without renumbering original entry offsets. This does not require
274
+ formation or port-zero support.
275
+ 3. **Remove the process-name/UI collision if desired.** Give the aggregate tab
276
+ its own identity so a process called `all` can work. Keep strict validation
277
+ of malformed and duplicate entries.
278
+ 4. **Consider formation independently.** Add multi-instance processes and
279
+ exclusions only as an explicit capability. If exposing Foreman's `-m`, retain
280
+ its semantics and test per-instance ports, `PS`, logs and controls. Until
281
+ then, keep a clear unsupported-option error.
282
+ 5. **Document retained differences.** Keep consistent app-root `run`, supported
283
+ `.foreman` defaults with strict validation, explicit env-file syntax,
284
+ positive base ports and the TUI's presentation contract. Give migration
285
+ examples instead of adding silent no-op flags or copying parser quirks.
286
+
287
+ Test the correctness fixes with real controlling terminals, signal handlers,
288
+ cleanup checks and selected-process fixtures. Compare with pinned Foreman where
289
+ behavior is deliberately shared; test retained differences against Foruiman's
290
+ own documented contract. No default-lifetime reversal, exporter, API shim or
291
+ legacy rendering layer is needed.
292
+
293
+ With these modernization choices, describe the product as **a modern alternative
294
+ to Foreman for Procfile development workflows**. List the supported shared
295
+ workflows and migration differences. An unqualified drop-in claim would conflict
296
+ with the intentionally retained command/configuration differences. See
297
+ [COMPATIBILITY.md](COMPATIBILITY.md) for current implemented behavior.
data/lib/foruiman/cli.rb CHANGED
@@ -35,6 +35,7 @@ class Foruiman::CLI < Thor
35
35
  def start(process = nil)
36
36
  engine = build_engine
37
37
  engine.select(process) if process
38
+ engine.validate_ports!
38
39
  interactive = configuration.tui? && $stdin.tty? && $stdout.tty?
39
40
  diagnostics = Foruiman::Diagnostics.new(interactive: interactive)
40
41
  code = if interactive
@@ -76,6 +77,7 @@ class Foruiman::CLI < Thor
76
77
  desc "check", "Validate the Procfile, environment files, ports, and log capacity without starting processes"
77
78
  def check
78
79
  engine = build_engine
80
+ engine.validate_ports!
79
81
  puts "valid Procfile (#{engine.process_names.join(', ')})"
80
82
  rescue Foruiman::Error, SystemCallError => e
81
83
  raise Thor::Error, e.message
@@ -7,7 +7,8 @@ require_relative "output"
7
7
  # Derived from Foreman's registration, process lookup, pipes and self-pipe signal
8
8
  # handling. All process state and output now belong to the caller's event loop.
9
9
  class Foruiman::Engine
10
- HANDLED_SIGNALS = %i[INT TERM HUP].freeze
10
+ FORWARDED_SIGNALS = %i[USR1 USR2].freeze
11
+ HANDLED_SIGNALS = %i[INT TERM HUP USR1 USR2].freeze
11
12
  TERM_TIMEOUT = 5.0
12
13
  READ_CHUNK = 4096
13
14
  READ_BUDGET = 64 * 1024
@@ -50,7 +51,7 @@ class Foruiman::Engine
50
51
  @failed = false
51
52
  @closed = false
52
53
  @started = false
53
- @signal_requested = false
54
+ @pending_signals = []
54
55
  @self_reader, @self_writer = create_pipe
55
56
  load_procfile(procfile) if procfile
56
57
  rescue StandardError
@@ -62,7 +63,6 @@ class Foruiman::Engine
62
63
  def register(name, command)
63
64
  raise Foruiman::Error, "cannot register after startup" if @started
64
65
  raise Foruiman::Error, "duplicate process: #{name}" if @names.key?(name)
65
- raise Foruiman::Error, "allocated port exceeds 65535 for #{name}" if @base_port + (processes.size * 100) > 65_535
66
66
 
67
67
  Foruiman::Procfile.new[name] = command
68
68
  process = Foruiman::Process.new(command, cwd: root, env: env)
@@ -77,9 +77,6 @@ class Foruiman::Engine
77
77
  def load_procfile(filename)
78
78
  parsed = Foruiman::Procfile.new(filename)
79
79
  entries = parsed.entries.to_a
80
- last_port = @base_port + ((processes.size + entries.size - 1) * 100)
81
- raise Foruiman::Error, "allocated port #{last_port} exceeds 65535" if last_port > 65_535
82
-
83
80
  entries.each { |name, command| register(name, command) }
84
81
  @procfile_path = File.expand_path(filename).freeze
85
82
  self
@@ -89,6 +86,15 @@ class Foruiman::Engine
89
86
  @names.keys
90
87
  end
91
88
 
89
+ def validate_ports!(entries = processes)
90
+ entries.each do |entry|
91
+ next if entry.port <= 65_535
92
+
93
+ raise Foruiman::Error, "allocated port #{entry.port} exceeds 65535 for #{entry.name}"
94
+ end
95
+ self
96
+ end
97
+
92
98
  def process(name)
93
99
  @names[name]&.process
94
100
  end
@@ -126,6 +132,7 @@ class Foruiman::Engine
126
132
  return if @shutdown
127
133
 
128
134
  targets = name ? [state(name)] : processes
135
+ validate_ports!(targets)
129
136
  unless @started
130
137
  @logs = Foruiman::LogStore.new(process_names, capacity: @log_lines) do |record|
131
138
  emit(:output, state(record.name), record: record)
@@ -146,6 +153,7 @@ class Foruiman::Engine
146
153
  entry = state(name)
147
154
  return if entry.restart_pending
148
155
 
156
+ validate_ports!([entry])
149
157
  entry.restart_pending = true
150
158
  entry.status = :restarting
151
159
  lifecycle(entry, :restarting, "restarting")
@@ -236,13 +244,13 @@ class Foruiman::Engine
236
244
  def tick(timeout: 0.03)
237
245
  return if @closed
238
246
 
239
- shutdown if @signal_requested
247
+ handle_signals
240
248
  reap_children
241
249
  advance_groups
242
250
  writable = processes.filter_map { |entry| entry.input unless entry.input_buffer.empty? }
243
251
  ready, writable = IO.select([@self_reader, *@readers.keys], writable, nil, timeout) || [[], []]
244
252
  drain_signal_pipe if ready.delete(@self_reader)
245
- shutdown if @signal_requested
253
+ handle_signals
246
254
  writable.each { |input| flush_input(@inputs.fetch(input)) if @inputs.key?(input) }
247
255
  read_output(ready)
248
256
  reap_children
@@ -280,6 +288,7 @@ class Foruiman::Engine
280
288
  begin
281
289
  pid = entry.process.run(output: stdout_writer, error: stderr_writer,
282
290
  input: input_slave || @input,
291
+ new_session: !@managed_input && @input.respond_to?(:tty?) && @input.tty?,
283
292
  env: { "PORT" => entry.port.to_s, "PS" => "#{entry.name}.1" })
284
293
  rescue SystemCallError => e
285
294
  stdout_reader.close
@@ -490,12 +499,24 @@ class Foruiman::Engine
490
499
  @old_handlers = {}
491
500
  HANDLED_SIGNALS.each do |signal|
492
501
  @old_handlers[signal] = Signal.trap(signal) do
493
- @signal_requested = true
502
+ @pending_signals << signal
494
503
  notice_signal
495
504
  end
496
505
  end
497
506
  end
498
507
 
508
+ def handle_signals
509
+ pending = @pending_signals
510
+ @pending_signals = []
511
+ pending.each do |signal|
512
+ if FORWARDED_SIGNALS.include?(signal)
513
+ processes.each { |entry| signal_group(entry, signal) }
514
+ else
515
+ shutdown
516
+ end
517
+ end
518
+ end
519
+
499
520
  def restore_signal_handlers
500
521
  @old_handlers&.each { |signal, handler| Signal.trap(signal, handler) }
501
522
  @old_handlers = nil
@@ -14,7 +14,7 @@ class Foruiman::LogStore
14
14
  end
15
15
 
16
16
  def [](name)
17
- name == "all" ? all : @buffers.fetch(name)
17
+ name == :all ? all : @buffers.fetch(name)
18
18
  end
19
19
 
20
20
  def write(name:, stream:, pid:, text:, complete:, previous: nil)
@@ -11,9 +11,49 @@ class Foruiman::Process
11
11
  end
12
12
 
13
13
  def run(options = {})
14
- ::Process.spawn(env.merge(options.fetch(:env, {})), "/bin/sh", "-c", command,
15
- chdir: cwd, in: options.fetch(:input, $stdin), out: options.fetch(:output, $stdout),
16
- err: options.fetch(:error, $stderr), pgroup: true, unsetenv_others: true,
17
- close_others: true)
14
+ environment = env.merge(options.fetch(:env, {}))
15
+ redirects = { chdir: cwd, in: options.fetch(:input, $stdin), out: options.fetch(:output, $stdout),
16
+ err: options.fetch(:error, $stderr), unsetenv_others: true, close_others: true }
17
+ return spawn_session(environment, redirects) if options[:new_session]
18
+
19
+ ::Process.spawn(environment, "/bin/sh", "-c", shell_command, **redirects, pgroup: true)
20
+ end
21
+
22
+ private
23
+
24
+ def shell_command
25
+ # Group forwarding also reaches the shell waiting for the application.
26
+ # Catch (rather than ignore) these signals so exec resets the dispositions
27
+ # and the application can decide whether to handle them or terminate.
28
+ "trap ':' USR1 USR2\n#{command}"
29
+ end
30
+
31
+ def spawn_session(environment, redirects)
32
+ # A background group in the supervisor's session cannot read its controlling
33
+ # terminal. A new session keeps inherited stdin usable and still gives us a
34
+ # group whose ID is the child PID, without taking the terminal's foreground.
35
+ reader, writer = IO.pipe
36
+ writer.close_on_exec = true
37
+ pid = ::Process.fork do
38
+ reader.close
39
+ ::Process.setsid
40
+ exec(environment, "/bin/sh", "-c", shell_command, **redirects)
41
+ rescue Exception => e # rubocop:disable Lint/RescueException -- never unwind the fork into supervisor cleanup
42
+ writer.write("#{e.is_a?(SystemCallError) ? e.errno : 0}\n#{e.message}")
43
+ ensure
44
+ ::Process.exit!(127)
45
+ end
46
+ writer.close
47
+ failure = reader.read
48
+ unless failure.empty?
49
+ ::Process.waitpid(pid)
50
+ errno, message = failure.split("\n", 2)
51
+ raise ArgumentError, message if errno.to_i.zero?
52
+
53
+ raise SystemCallError.new(message, errno.to_i)
54
+ end
55
+ pid
56
+ ensure
57
+ [reader, writer].compact.each { |io| io.close unless io.closed? }
18
58
  end
19
59
  end
@@ -52,10 +52,9 @@ class Foruiman::Procfile
52
52
  private
53
53
 
54
54
  def validate_entry!(name, command, location)
55
- unless name.match?(/\A[A-Za-z0-9_-]+\z/) && !command.strip.empty? && !command.include?("\0")
56
- raise ParseError, "#{location}: expected NAME: command"
57
- end
58
- raise ParseError, "#{location}: 'all' is reserved for the aggregate tab" if name == "all"
55
+ return if name.match?(/\A[A-Za-z0-9_-]+\z/) && !command.strip.empty? && !command.include?("\0")
56
+
57
+ raise ParseError, "#{location}: expected NAME: command"
59
58
  end
60
59
 
61
60
  def parse(filename)
@@ -92,7 +92,7 @@ module Foruiman::TUI
92
92
  end
93
93
 
94
94
  def begin_input
95
- if state.name == "all"
95
+ if state.aggregate?
96
96
  feedback("Select a process before entering input mode")
97
97
  elsif @engine.state(state.name).status != :running
98
98
  feedback("#{state.name} is not running")
@@ -135,7 +135,7 @@ module Foruiman::TUI
135
135
  end
136
136
 
137
137
  def toggle_process
138
- if state.name == "all"
138
+ if state.aggregate?
139
139
  control_all(:stop)
140
140
  return
141
141
  end
@@ -151,7 +151,7 @@ module Foruiman::TUI
151
151
  end
152
152
 
153
153
  def control(action)
154
- if state.name == "all"
154
+ if state.aggregate?
155
155
  if action == :restart
156
156
  control_all(action)
157
157
  else
@@ -126,15 +126,15 @@ module Foruiman::TUI
126
126
 
127
127
  def tab(name, index, state, engine, width)
128
128
  selected = index == state.selected
129
- entry = name == "all" ? nil : engine.processes.find { |process| process.name == name }
130
- mark = name == "all" ? "≡" : Theme::STATUS_MARKS.fetch(entry&.status, "○")
131
- number = if name == "all"
129
+ entry = name == :all ? nil : engine.processes.find { |process| process.name == name }
130
+ mark = name == :all ? "≡" : Theme::STATUS_MARKS.fetch(entry&.status, "○")
131
+ number = if name == :all
132
132
  "0"
133
133
  else
134
134
  (index < 9 ? (index + 1).to_s : "·")
135
135
  end
136
- name = Text.clip(name, [width - 12, 6].max.clamp(6, 22), ellipsis: true)
137
- color = name == "all" ? :accent : @theme.process(index)
136
+ color = name == :all ? :accent : @theme.process(index)
137
+ name = Text.clip(name.to_s, [width - 12, 6].max.clamp(6, 22), ellipsis: true)
138
138
  mark_color = entry ? Theme::STATUS_COLORS.fetch(entry.status, :muted) : :muted
139
139
  @theme.paint(" #{number} ", selected ? :muted : :faint, selected: selected) +
140
140
  @theme.paint("#{mark} ", mark_color, selected: selected) +
@@ -147,14 +147,14 @@ module Foruiman::TUI
147
147
  @theme.paint(" ? / Escape close help ", :muted))
148
148
  end
149
149
 
150
- title = state.name == "all" ? " ≡ all logs " : " › #{state.name} "
151
- detail = if state.name == "all"
150
+ title = state.aggregate? ? " ≡ all logs " : " › #{state.name} "
151
+ detail = if state.aggregate?
152
152
  "#{engine.processes.size} processes"
153
153
  else
154
154
  entry = engine.state(state.name)
155
155
  process_detail(entry)
156
156
  end
157
- active = if state.name == "all"
157
+ active = if state.aggregate?
158
158
  engine.processes.any? { |entry| entry.status == :running }
159
159
  else
160
160
  engine.state(state.name).status == :running
@@ -171,7 +171,7 @@ module Foruiman::TUI
171
171
  title = @theme.paint(title, :text, bold: true)
172
172
  if @inside >= 45
173
173
  title += @theme.paint(" #{detail} ", :muted)
174
- elsif state.name != "all" && @inside >= 28
174
+ elsif !state.aggregate? && @inside >= 28
175
175
  entry = engine.state(state.name)
176
176
  title += @theme.paint(" #{entry.status} ", Theme::STATUS_COLORS.fetch(entry.status, :muted))
177
177
  end
@@ -200,7 +200,7 @@ module Foruiman::TUI
200
200
  thumb_start = buffer.size <= @height ? 0 : (first * travel / (buffer.size - @height))
201
201
  Array.new(@height) do |index|
202
202
  content = if records[index]
203
- @formatter.row(records[index], aggregate: state.name == "all", width: @inside - 2)
203
+ @formatter.row(records[index], aggregate: state.aggregate?, width: @inside - 2)
204
204
  elsif records.empty? && index == @height / 2
205
205
  @theme.paint("Waiting for output…", :faint)
206
206
  else
@@ -226,7 +226,7 @@ module Foruiman::TUI
226
226
  else
227
227
  " f resume "
228
228
  end
229
- location = " PID #{engine.state(state.name).pid || '-'} " if state.name != "all" && @inside < 45
229
+ location = " PID #{engine.state(state.name).pid || '-'} " if !state.aggregate? && @inside < 45
230
230
  border(@theme.paint(count, :faint), @theme.paint(location, state.viewport.following ? :faint : :amber),
231
231
  bottom: true)
232
232
  end
@@ -269,8 +269,8 @@ module Foruiman::TUI
269
269
  end
270
270
 
271
271
  def shortcuts(state, engine)
272
- restart = ["r", state.name == "all" ? "↻ restart all" : "↻ restart"]
273
- toggle = if state.name == "all"
272
+ restart = ["r", state.aggregate? ? "↻ restart all" : "↻ restart"]
273
+ toggle = if state.aggregate?
274
274
  ["s", "■ stop all"]
275
275
  elsif process_active?(engine.state(state.name))
276
276
  ["s", "■ stop"]
@@ -9,7 +9,7 @@ module Foruiman::TUI
9
9
  attr_accessor :help, :feedback, :input_target
10
10
 
11
11
  def initialize(names)
12
- @tabs = [*names, "all"].freeze
12
+ @tabs = [*names, :all].freeze
13
13
  @selected = tabs.size - 1
14
14
  @viewports = tabs.to_h { |name| [name, Viewport.new] }
15
15
  @help = false
@@ -22,6 +22,10 @@ module Foruiman::TUI
22
22
  tabs[selected]
23
23
  end
24
24
 
25
+ def aggregate?
26
+ name == :all
27
+ end
28
+
25
29
  def viewport
26
30
  viewports.fetch(name)
27
31
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Foruiman
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foruiman
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foruiman contributors
@@ -64,6 +64,7 @@ files:
64
64
  - docs/ARCHITECTURE.md
65
65
  - docs/ASSUMPTIONS.md
66
66
  - docs/COMPATIBILITY.md
67
+ - docs/FOREMAN_AUDIT.md
67
68
  - docs/RAILS.md
68
69
  - docs/UPSTREAM.md
69
70
  - docs/terminal-preview.png