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 +4 -4
- data/CHANGELOG.md +8 -1
- data/README.md +10 -1
- data/docs/ARCHITECTURE.md +24 -7
- data/docs/ASSUMPTIONS.md +2 -1
- data/docs/COMPATIBILITY.md +16 -5
- data/docs/FOREMAN_AUDIT.md +297 -0
- data/lib/foruiman/cli.rb +2 -0
- data/lib/foruiman/engine.rb +30 -9
- data/lib/foruiman/log_store.rb +1 -1
- data/lib/foruiman/process.rb +44 -4
- data/lib/foruiman/procfile.rb +3 -4
- data/lib/foruiman/tui/application.rb +3 -3
- data/lib/foruiman/tui/renderer.rb +13 -13
- data/lib/foruiman/tui/state.rb +5 -1
- data/lib/foruiman/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c7f857660af7989aab5c7175e501361ca523780f6eb14f65f7c1620da4040c9
|
|
4
|
+
data.tar.gz: ad726e87ac32a31a1eb07c3a6b9dc4e7eea5f34374e5395153b0c7a73f7831ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2256d813401208d3c672a391cc7ef8670f81cb4cd6ac9d84a22dc510fb612edc52863612d3d9310d84d9a4de05e239339c2a03b8a6ff87469257e81bc016e0c7
|
|
7
|
+
data.tar.gz: 4074ebafd99da6d35bdb99b2fa3aad0ce24eac452ff763e13180bb554db8135a23efdfb2cdf436da8fe001838f1db861445efa43a7e1be42a1f85ba95745c715
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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`
|
|
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
|
-
|
|
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
|
|
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
|
|
51
|
-
handlers. An embedding loop that drives
|
|
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.
|
|
60
|
-
|
|
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.
|
|
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
|
-
-
|
|
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`.
|
data/docs/COMPATIBILITY.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
data/lib/foruiman/engine.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
data/lib/foruiman/log_store.rb
CHANGED
data/lib/foruiman/process.rb
CHANGED
|
@@ -11,9 +11,49 @@ class Foruiman::Process
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def run(options = {})
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
data/lib/foruiman/procfile.rb
CHANGED
|
@@ -52,10 +52,9 @@ class Foruiman::Procfile
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
54
|
def validate_entry!(name, command, location)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 ==
|
|
130
|
-
mark = name ==
|
|
131
|
-
number = if name ==
|
|
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
|
-
|
|
137
|
-
|
|
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.
|
|
151
|
-
detail = if state.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
273
|
-
toggle = if state.
|
|
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"]
|
data/lib/foruiman/tui/state.rb
CHANGED
|
@@ -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,
|
|
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
|
data/lib/foruiman/version.rb
CHANGED
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.
|
|
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
|