foruiman 0.1.2
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 +7 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE +19 -0
- data/README.md +102 -0
- data/bin/foruiman +6 -0
- data/docs/ARCHITECTURE.md +84 -0
- data/docs/ASSUMPTIONS.md +23 -0
- data/docs/COMPATIBILITY.md +27 -0
- data/docs/RAILS.md +54 -0
- data/docs/UPSTREAM.md +25 -0
- data/docs/terminal-preview.png +0 -0
- data/lib/foruiman/ansi.rb +147 -0
- data/lib/foruiman/cli.rb +83 -0
- data/lib/foruiman/diagnostics.rb +27 -0
- data/lib/foruiman/engine.rb +403 -0
- data/lib/foruiman/env.rb +35 -0
- data/lib/foruiman/log_store.rb +31 -0
- data/lib/foruiman/output.rb +76 -0
- data/lib/foruiman/plain.rb +22 -0
- data/lib/foruiman/process.rb +19 -0
- data/lib/foruiman/procfile.rb +81 -0
- data/lib/foruiman/ring.rb +53 -0
- data/lib/foruiman/tui/application.rb +105 -0
- data/lib/foruiman/tui/keyboard.rb +66 -0
- data/lib/foruiman/tui/log_formatter.rb +47 -0
- data/lib/foruiman/tui/renderer.rb +306 -0
- data/lib/foruiman/tui/state.rb +34 -0
- data/lib/foruiman/tui/terminal.rb +44 -0
- data/lib/foruiman/tui/text.rb +45 -0
- data/lib/foruiman/tui/theme.rb +71 -0
- data/lib/foruiman/tui/viewport.rb +56 -0
- data/lib/foruiman/version.rb +5 -0
- data/lib/foruiman.rb +11 -0
- metadata +116 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0de0b215283d2c3dc89422aa9e6f6da235acf10f5ede3907afcf8e7c88dd9290
|
|
4
|
+
data.tar.gz: 81dfcdcfbc8d7ab445242fd5c07419192b6445e9caa3107fcdce5a2a027d914e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bb3cbdd6d6c38335bd5d6d4b467bbedec757cb2e534896b24d471c8b4275203d22cb22ec642fafe63410df21c1616322ae2e4616b0b26dce277959308f0092c5
|
|
7
|
+
data.tar.gz: 03db66ce62f3e21723aac3808791b2a027bb2ab497d415de11730661ff90b5150b82a0d7351ea2c156abc93be670e93a5b575933d06bf6a0fdcdcece35f7b517
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Test Ruby 3.2, 3.3, 3.4, and 4.0 in a reusable CI workflow and keep the
|
|
6
|
+
development dependency set compatible with Ruby 3.2.
|
|
7
|
+
- Add a tag-driven RubyGems trusted-publishing workflow gated by the full CI suite.
|
|
8
|
+
- Make renderer row-width checks independent of the runner's terminal settings.
|
|
9
|
+
|
|
10
|
+
## 0.1.2
|
|
11
|
+
|
|
12
|
+
- Fix lowercase `r` on the `all` tab to restart every process; keep `R` available
|
|
13
|
+
from any tab and make the footer describe the selected restart scope.
|
|
14
|
+
- Show the active Procfile in the header, including custom paths and working
|
|
15
|
+
directories, with filename priority in narrow terminals.
|
|
16
|
+
- Follow the terminal's global theme through its ANSI palette and default
|
|
17
|
+
background. Use reverse-video selection for light, dark, and monochrome themes.
|
|
18
|
+
- Verify group restarts with real keyboard input and process cleanup, and cover
|
|
19
|
+
Procfile titles, theme inheritance, and preserved child colors.
|
|
20
|
+
|
|
21
|
+
## 0.1.1
|
|
22
|
+
|
|
23
|
+
- Redesign the TUI with a forest palette, framed process navigation, selected-tab
|
|
24
|
+
backgrounds, status colors, aligned log columns, a scrollbar, and grouped help.
|
|
25
|
+
- Support truecolor, 256 colors, and `NO_COLOR`; preserve child ANSI styles without
|
|
26
|
+
letting resets overwrite the interface background.
|
|
27
|
+
- Adapt chrome and log metadata to terminal size, keep selected tabs visible,
|
|
28
|
+
and use the actual visible log height for scrolling and paging.
|
|
29
|
+
- Include a static preview fixture and expand color, layout, and PTY coverage.
|
|
30
|
+
|
|
31
|
+
## 0.1.0
|
|
32
|
+
|
|
33
|
+
- Fork-derived MVP based on Foreman `f65ddba83932bd4670e014389d6e27ea1e20b469`.
|
|
34
|
+
- Strict Procfiles, layered environments, and validated port allocation.
|
|
35
|
+
- Independent process supervision, bounded log rings, and asynchronous restarts.
|
|
36
|
+
- Terminal tabs, scrolling, Unicode-aware rows, and exception-safe restoration.
|
|
37
|
+
- Automatic plain mode and process-group cleanup with TERM/KILL escalation.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2012 David Dollar
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
8
|
+
so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Foruiman
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Nuzair46/foruiman/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
A modern Procfile runner with process tabs, bounded logs, scrolling, and isolated
|
|
8
|
+
restarts. Foruiman is a fork of [Foreman](https://github.com/ddollar/foreman) and
|
|
9
|
+
retains its MIT license.
|
|
10
|
+
|
|
11
|
+
Requires Ruby 3.2+ and a POSIX system. CI covers Ruby 3.2, 3.3, 3.4, and 4.0 on Linux.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
gem install foruiman
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Run
|
|
20
|
+
|
|
21
|
+
Create a `Procfile`:
|
|
22
|
+
|
|
23
|
+
```procfile
|
|
24
|
+
web: bundle exec rails server -p "$PORT"
|
|
25
|
+
worker: bundle exec sidekiq
|
|
26
|
+
css: yarn build:css --watch
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then start Foruiman:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
foruiman
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Useful commands:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
foruiman start -f Procfile.dev # use another Procfile
|
|
39
|
+
foruiman start web # run one process
|
|
40
|
+
foruiman start --no-tui # stream plain logs
|
|
41
|
+
foruiman check # validate without starting
|
|
42
|
+
foruiman --version
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The TUI opens when stdin and stdout are terminals. Otherwise, Foruiman streams
|
|
46
|
+
plain logs and exits when the processes finish. Processes run independently, and
|
|
47
|
+
restarting one process does not interrupt the others.
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
| Option | Default | Purpose |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `-f`, `--procfile FILE` | `Procfile` | Procfile to run |
|
|
54
|
+
| `-d`, `--root DIR` | Current directory | Working directory |
|
|
55
|
+
| `-e`, `--env FILE` | None | Additional environment file |
|
|
56
|
+
| `--no-dotenv` | `.env` enabled | Skip `.env` |
|
|
57
|
+
| `-p`, `--port PORT` | `5000` | Base port; entries increment by 100 |
|
|
58
|
+
| `--log-lines N` | `10000` | Retained records per process and in `all` |
|
|
59
|
+
| `--no-tui` | TUI when interactive | Force plain output |
|
|
60
|
+
|
|
61
|
+
## Keyboard
|
|
62
|
+
|
|
63
|
+
| Key | Action |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| Tab / Shift-Tab, Left / Right, `h` / `l` | Change tab |
|
|
66
|
+
| `1`–`9`, `0` | Select a process or `all` |
|
|
67
|
+
| Up / Down, `k` / `j` | Scroll |
|
|
68
|
+
| PageUp / PageDown, Ctrl-U / Ctrl-D | Scroll a page |
|
|
69
|
+
| Home / `g` | Jump to oldest retained log |
|
|
70
|
+
| End / `G` / `f` | Follow new output |
|
|
71
|
+
| Space | Pause or resume following |
|
|
72
|
+
| `r` | Restart the selected process, or all from `all` |
|
|
73
|
+
| `R` | Restart all processes |
|
|
74
|
+
| `s` / `S` | Stop selected / stop all |
|
|
75
|
+
| `?` / Escape | Toggle help / close help |
|
|
76
|
+
| `q` / Ctrl-C | Stop processes and quit |
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
- [Rails usage](docs/RAILS.md)
|
|
81
|
+
- [Architecture and supervisor API](docs/ARCHITECTURE.md)
|
|
82
|
+
- [Differences from Foreman](docs/COMPATIBILITY.md)
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
bundle install
|
|
88
|
+
bundle exec rspec
|
|
89
|
+
bundle exec rubocop
|
|
90
|
+
gem build foruiman.gemspec
|
|
91
|
+
bundle exec ruby script/smoke_gem.rb
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Release
|
|
95
|
+
|
|
96
|
+
Releases use RubyGems trusted publishing. Configure the publisher once with
|
|
97
|
+
repository `Nuzair46/foruiman`, workflow `release.yml`, and environment `release`.
|
|
98
|
+
|
|
99
|
+
To publish, update `lib/foruiman/version.rb` on `main`, open the
|
|
100
|
+
[Release workflow](https://github.com/Nuzair46/foruiman/actions/workflows/release.yml),
|
|
101
|
+
choose **Run workflow**, and select `main`. GitHub Actions runs the full CI suite,
|
|
102
|
+
creates the version tag, and publishes the gem to RubyGems.org.
|
data/bin/foruiman
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Architecture and embedding
|
|
2
|
+
|
|
3
|
+
`Procfile` retains Foreman's ordered parser/writer API with strict file validation.
|
|
4
|
+
`Env` retains its quoting rules and adds a non-mutating precedence merge. `Process`
|
|
5
|
+
wraps `/bin/sh -c` with a new process group, two output streams, and null stdin.
|
|
6
|
+
`CLI < Thor` validates the full configuration before starting the engine.
|
|
7
|
+
|
|
8
|
+
`Engine` owns registration, PID/group tracking, nonblocking pipes, child reaping,
|
|
9
|
+
and lifecycle transitions. A single caller thread drives all mutations. Signal
|
|
10
|
+
handlers only set a flag and wake a self-pipe. The loop reaps only its own direct
|
|
11
|
+
children, services bounded reads in round-robin order, checks TERM deadlines, and
|
|
12
|
+
starts pending replacements once old groups and pipes are finished. Group existence
|
|
13
|
+
is tracked independently of leader status; Linux `/proc` distinguishes running
|
|
14
|
+
members from orphan zombies awaiting the system reaper.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require "foruiman"
|
|
18
|
+
|
|
19
|
+
env = Foruiman::Env.load(root: Dir.pwd)
|
|
20
|
+
engine = Foruiman::Engine.new(procfile: "Procfile", env: env, log_lines: 2_000)
|
|
21
|
+
engine.on_event do |event|
|
|
22
|
+
# Types: output, started, restarting, stopping, exited, killed, failed.
|
|
23
|
+
# Fields: type, name, pid, status, record, message.
|
|
24
|
+
# Output records add sequence, stream, time, text, complete.
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
engine.run # install handlers, start all, loop, clean up, restore handlers; returns 0/1
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For an embedded event loop:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
begin
|
|
34
|
+
engine.start_all # or engine.start("web")
|
|
35
|
+
engine.tick(timeout: 0.03)
|
|
36
|
+
engine.restart("web") # asynchronous; peers keep running
|
|
37
|
+
engine.stop("worker") # asynchronous
|
|
38
|
+
engine.shutdown # cancels replacements and requests group cleanup
|
|
39
|
+
engine.tick until engine.finished?
|
|
40
|
+
ensure
|
|
41
|
+
engine.close # finishes cleanup, closes pipes; safe to repeat
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Call engine methods and event listeners on the driving thread; this is not a
|
|
46
|
+
cross-thread messaging API. `run` handles INT/TERM/HUP and restores existing
|
|
47
|
+
handlers. An embedding loop that drives `tick` directly owns its signal handling.
|
|
48
|
+
`close` disconnects observers so a failed renderer/output consumer cannot prevent
|
|
49
|
+
process cleanup. `term_timeout:` exists for deterministic embedded tests; CLI
|
|
50
|
+
shutdown always uses five seconds. `state(name)` exposes lifecycle state for
|
|
51
|
+
rendering; callers should not mutate it.
|
|
52
|
+
|
|
53
|
+
`LogStore` uses per-process `Ring` instances and an aggregate `Ring`, each with O(1)
|
|
54
|
+
append, eviction, record replacement, and identity lookup. Immutable `Data` records
|
|
55
|
+
are shared. A partial record has one sequence identity; completing it replaces
|
|
56
|
+
retained references without re-inserting an already evicted aggregate record.
|
|
57
|
+
Sequence order represents first observation, not line completion time. Actual
|
|
58
|
+
memory use depends on line length and ring capacity; retained text is bounded by
|
|
59
|
+
approximately `(process_count + 1) * capacity * 16 KiB`, plus fixed per-stream and
|
|
60
|
+
per-record overhead, with overlap shared between rings.
|
|
61
|
+
|
|
62
|
+
`Output` bounds each stream, handles partial records, and takes style snapshots.
|
|
63
|
+
`ANSI::Decoder` incrementally decodes UTF-8 and suppresses terminal controls;
|
|
64
|
+
`ANSI::Styles` keeps bounded independent style properties across rows. A long line
|
|
65
|
+
is split into completed records at the size limit.
|
|
66
|
+
|
|
67
|
+
The TUI loads only in interactive mode. `Terminal` owns raw input and alternate
|
|
68
|
+
screen restoration. `Keyboard` decodes fragmented key sequences. `State` stores
|
|
69
|
+
selection/help/feedback, and each tab owns a `Viewport` anchored by record identity.
|
|
70
|
+
`Renderer` composes framed process navigation, a log pane with a scrollbar, and a
|
|
71
|
+
shortcut bar. Its shared log-height calculation keeps paging consistent with the
|
|
72
|
+
responsive layout. `Text` measures graphemes through `unicode-display_width` and
|
|
73
|
+
clips rows safely. `Theme` uses the terminal's ANSI palette, default foreground and
|
|
74
|
+
background, and reverse-video selection, with a monochrome fallback. It does not
|
|
75
|
+
read OS theme files or override palette entries; child SGR resets restore terminal
|
|
76
|
+
defaults. The header uses the engine's `procfile_path` to identify the loaded file.
|
|
77
|
+
`LogFormatter` aligns timestamps,
|
|
78
|
+
process names, stream markers, and content. Complete frames reset styles at each row. `Application` connects input and lifecycle
|
|
79
|
+
commands to the engine and limits drawing to 30 FPS. Resize handling reads current
|
|
80
|
+
terminal dimensions each loop; it does not replace an application's WINCH handler.
|
|
81
|
+
|
|
82
|
+
`Plain` prints completed records and runs to natural completion. `Diagnostics`
|
|
83
|
+
keeps internal exceptions separate from process logs, using a private temporary
|
|
84
|
+
file in interactive mode and stderr otherwise.
|
data/docs/ASSUMPTIONS.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Inputs and implementation defaults
|
|
2
|
+
|
|
3
|
+
The implementation workspace was empty. The RFC referenced by the supplied plan,
|
|
4
|
+
its requested file list, and earlier preference selections were not present. The
|
|
5
|
+
supplied Foreman-based MVP plan is the implementation contract available here.
|
|
6
|
+
The missing RFC was requested during implementation; these choices fill the gaps:
|
|
7
|
+
|
|
8
|
+
- Default command `start`; also `start PROCESS`, `check`, `version`, `help`.
|
|
9
|
+
- Foreman's `-f`, `-d`, `-e`, `-p` aliases; `--log-lines`, `--no-tui`, `--no-dotenv`.
|
|
10
|
+
- Default capacity 10,000 records per ring.
|
|
11
|
+
- Process tabs in entry order, then `all`; select `all` initially.
|
|
12
|
+
- `all` reserved as a process name to avoid an ambiguous aggregate tab.
|
|
13
|
+
- Vim-style and arrow navigation, digits, `f`, Space, `r`/`R`, `s`/`S`, `?`, `q`.
|
|
14
|
+
- Generated `PS=name.1`, as in Foreman; no other generated variables.
|
|
15
|
+
- Relative file paths resolved against invocation directory or explicit `-d`.
|
|
16
|
+
- Header whitespace after a Procfile colon is a separator; command-internal and
|
|
17
|
+
trailing whitespace is preserved.
|
|
18
|
+
- 30 FPS maximum; 16 KiB records; 64 KiB total read budget per event-loop iteration.
|
|
19
|
+
- No public repository URL is invented for the unpublished Foruiman project.
|
|
20
|
+
|
|
21
|
+
See README for the actual public interface. These defaults can be reconciled with
|
|
22
|
+
the original RFC when it becomes available; missing requirements are not claimed
|
|
23
|
+
as verified.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Intentional differences from Foreman
|
|
2
|
+
|
|
3
|
+
Foruiman is a fork-derived MVP, not a drop-in replacement for every Foreman feature.
|
|
4
|
+
|
|
5
|
+
| Area | Foruiman behavior |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Identity | `foruiman` gem/executable, `Foruiman` namespace, version 0.1.2 |
|
|
8
|
+
| Runtime | Ruby 3.2+, POSIX process groups; Linux CI |
|
|
9
|
+
| CLI | Thor-based `start [PROCESS]`, `check`, `version`, and `help` |
|
|
10
|
+
| Excluded features | No export, scaling/formation, `run`, `.foreman` YAML, custom shutdown timeout, forced color, or timestamp toggle |
|
|
11
|
+
| Procfile validation | Reject malformed lines, duplicates, empty commands, and reserved `all`; report line numbers |
|
|
12
|
+
| Working directory | Invocation directory unless `-d`; moving a Procfile does not change child cwd |
|
|
13
|
+
| Environment | Optional `.env` then explicit env file; parent `ENV` stays untouched |
|
|
14
|
+
| Ports | 5000 or `-p`, plus 100 per original entry; `PORT` in the environment is overridden |
|
|
15
|
+
| Expansion | `/bin/sh -c` performs shell expansion; no Ruby string substitution |
|
|
16
|
+
| Instances | Exactly one instance per entry; `PS=name.1` |
|
|
17
|
+
| Failures | Independent process failures; peers keep running |
|
|
18
|
+
| Restart | Stop only the affected group, wait for descendants and output, then replace |
|
|
19
|
+
| Shutdown | TERM, five-second grace, KILL; track groups after leader exit; restore prior signal handlers |
|
|
20
|
+
| Output | Separate stdout/stderr metadata; bounded logs and live partial records |
|
|
21
|
+
| Terminal | Tabs, independent scroll/follow, restart and stop controls; no child stdin |
|
|
22
|
+
| Plain exit | Wait for all processes; status 0/1; explicit orderly shutdown returns 0 |
|
|
23
|
+
| Interactive exit | Remain open after all processes exit; quit explicitly |
|
|
24
|
+
|
|
25
|
+
Commands needing a PTY, interactive child input, or background daemonization are
|
|
26
|
+
outside this release. Remote process control, persistence across Foruiman sessions,
|
|
27
|
+
search, horizontal scrolling, and log export are not provided.
|
data/docs/RAILS.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Using Foruiman with Rails
|
|
2
|
+
|
|
3
|
+
Install the locally built Foruiman gem as a development tool using the README's
|
|
4
|
+
instructions. Keep it outside the Rails application's Gemfile when using a global
|
|
5
|
+
development-tool installation. The Rails processes can still run under `bundle exec`.
|
|
6
|
+
|
|
7
|
+
A typical `Procfile.dev`:
|
|
8
|
+
|
|
9
|
+
```procfile
|
|
10
|
+
web: bin/rails server -p "$PORT"
|
|
11
|
+
worker: bundle exec sidekiq
|
|
12
|
+
css: yarn build:css --watch
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Include only entries used by your app. For a Vite application, replace the CSS entry
|
|
16
|
+
with your project's foreground Vite command, for example `vite: bin/vite dev`.
|
|
17
|
+
Use your app's existing port configuration for tools that do not read `PORT`.
|
|
18
|
+
|
|
19
|
+
An optional `.env` in the Rails project root:
|
|
20
|
+
|
|
21
|
+
```dotenv
|
|
22
|
+
RAILS_ENV=development
|
|
23
|
+
REDIS_URL=redis://localhost:6379/0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Keep secrets out of version control. Foruiman reads this file as assignments; it
|
|
27
|
+
does not source it as a shell script.
|
|
28
|
+
|
|
29
|
+
From the Rails root:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
foruiman start -f Procfile.dev
|
|
33
|
+
foruiman start -f Procfile.dev -p 3000
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The second example assigns 3000 to `web`, 3100 to `worker`, and 3200 to `css`.
|
|
37
|
+
Only programs that consume their generated `PORT` use those values. The base port
|
|
38
|
+
is set with `-p`; a `PORT` assignment in `.env` does not set it.
|
|
39
|
+
|
|
40
|
+
An optional `bin/dev`:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
#!/bin/sh
|
|
44
|
+
exec foruiman start -f Procfile.dev "$@"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Select the web tab and press `r` to restart Rails without interrupting the worker
|
|
48
|
+
or asset watcher. On the `all` tab, `r` restarts all entries; `R` does so from any
|
|
49
|
+
tab. The header shows the active Procfile. `q` or Ctrl-C stops all owned process
|
|
50
|
+
groups, including grandchildren, before returning to the shell.
|
|
51
|
+
|
|
52
|
+
Redirect output with `foruiman start -f Procfile.dev > development.log` to use plain
|
|
53
|
+
mode. A failed worker will not stop Rails; plain mode finishes only after every
|
|
54
|
+
entry exits or Foruiman receives a shutdown signal.
|
data/docs/UPSTREAM.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Foreman provenance
|
|
2
|
+
|
|
3
|
+
- Repository: https://github.com/ddollar/foreman
|
|
4
|
+
- Revision: `f65ddba83932bd4670e014389d6e27ea1e20b469` (`update docs`)
|
|
5
|
+
- Source archive: https://codeload.github.com/ddollar/foreman/tar.gz/f65ddba83932bd4670e014389d6e27ea1e20b469
|
|
6
|
+
- License: MIT, Copyright (c) 2012 David Dollar; preserved verbatim in `LICENSE`.
|
|
7
|
+
|
|
8
|
+
The initial local import copied `lib/foreman/{procfile,env,process,engine,cli}.rb`
|
|
9
|
+
into the renamed namespace before applying the MVP changes. The Procfile
|
|
10
|
+
reader/writer, environment assignment/quoting parser, process execution wrapper,
|
|
11
|
+
ordered engine registration and lookup, pipe/signal approach, and Thor command
|
|
12
|
+
foundation derive from that source. The supervisor loop and terminal components
|
|
13
|
+
were refactored or written for Foruiman's independent process and log semantics.
|
|
14
|
+
|
|
15
|
+
Relevant upstream test files are retained verbatim as review references in
|
|
16
|
+
`docs/upstream/*_spec.rb.txt` (not part of the packaged gem). Active tests in
|
|
17
|
+
`spec/foruiman/procfile_spec.rb`, `env_spec.rb`, and `process_spec.rb` adapt the
|
|
18
|
+
applicable upstream examples to real temporary files and managed Ruby fixtures.
|
|
19
|
+
The CLI and engine tests cover the new contract rather than upstream concurrency,
|
|
20
|
+
export, command substitution, or stop-on-first-exit behavior.
|
|
21
|
+
|
|
22
|
+
`docs/upstream/SHA256SUMS` records the original imported source/test checksums.
|
|
23
|
+
Exporters, distribution code, scaling, and obsolete runtime compatibility code are
|
|
24
|
+
not included in the distributed implementation. There is no runtime dependency
|
|
25
|
+
on the Foreman gem and no claim of complete Foreman compatibility.
|
|
Binary file
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foruiman::ANSI
|
|
4
|
+
SGR = /\e\[[0-9;:]*m/
|
|
5
|
+
RESET = "\e[0m"
|
|
6
|
+
|
|
7
|
+
# A bounded style snapshot makes each retained row independently renderable.
|
|
8
|
+
class Styles
|
|
9
|
+
def initialize
|
|
10
|
+
@values = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def apply(sequence)
|
|
14
|
+
values = sequence.delete_prefix("\e[").delete_suffix("m").split(";")
|
|
15
|
+
values = ["0"] if values.empty?
|
|
16
|
+
until values.empty?
|
|
17
|
+
value = values.shift
|
|
18
|
+
code = value.to_i
|
|
19
|
+
case code
|
|
20
|
+
when 0 then @values.clear
|
|
21
|
+
when 1, 2 then @values[:intensity] = value
|
|
22
|
+
when 3 then @values[:italic] = value
|
|
23
|
+
when 4, 21 then @values[:underline] = value
|
|
24
|
+
when 5, 6 then @values[:blink] = value
|
|
25
|
+
when 7 then @values[:inverse] = value
|
|
26
|
+
when 8 then @values[:conceal] = value
|
|
27
|
+
when 9 then @values[:strike] = value
|
|
28
|
+
when 22 then @values.delete(:intensity)
|
|
29
|
+
when 23 then @values.delete(:italic)
|
|
30
|
+
when 24 then @values.delete(:underline)
|
|
31
|
+
when 25 then @values.delete(:blink)
|
|
32
|
+
when 27 then @values.delete(:inverse)
|
|
33
|
+
when 28 then @values.delete(:conceal)
|
|
34
|
+
when 29 then @values.delete(:strike)
|
|
35
|
+
when 30..37, 90..97 then @values[:foreground] = value
|
|
36
|
+
when 40..47, 100..107 then @values[:background] = value
|
|
37
|
+
when 38, 48 then color(code, value, values)
|
|
38
|
+
when 39 then @values.delete(:foreground)
|
|
39
|
+
when 49 then @values.delete(:background)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def prefix
|
|
45
|
+
@values.empty? ? "" : "\e[#{@values.values.join(';')}m"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def color(code, value, values)
|
|
51
|
+
if value.include?(":")
|
|
52
|
+
color = value
|
|
53
|
+
else
|
|
54
|
+
mode = values.shift
|
|
55
|
+
return unless %w[2 5].include?(mode)
|
|
56
|
+
|
|
57
|
+
count = mode == "2" ? 3 : 1
|
|
58
|
+
return if values.size < count
|
|
59
|
+
|
|
60
|
+
color = [value, mode, *values.shift(count)].join(";")
|
|
61
|
+
end
|
|
62
|
+
@values[code == 38 ? :foreground : :background] = color
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Incremental byte parser: only text, newlines, and SGR can leave this class.
|
|
67
|
+
# OSC (including clipboard), DCS, cursor controls, and other escapes are dropped.
|
|
68
|
+
class Decoder
|
|
69
|
+
def initialize
|
|
70
|
+
@state = :text
|
|
71
|
+
@escape = +""
|
|
72
|
+
@pending = +"".b
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def feed(bytes, eof: false)
|
|
76
|
+
output = +"".b
|
|
77
|
+
bytes.each_byte { |byte| consume(byte, output) }
|
|
78
|
+
output = @pending + output
|
|
79
|
+
@pending = +"".b
|
|
80
|
+
unless eof
|
|
81
|
+
length = incomplete_suffix(output)
|
|
82
|
+
@pending = output.slice!(-length, length) if length.positive?
|
|
83
|
+
end
|
|
84
|
+
output.force_encoding(Encoding::UTF_8).scrub.delete("\u0080-\u009f")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def consume(byte, output)
|
|
90
|
+
case @state
|
|
91
|
+
when :text
|
|
92
|
+
case byte
|
|
93
|
+
when 27 then @state = :escape
|
|
94
|
+
when 9 then output << " "
|
|
95
|
+
when 10, 32..126, 128..255 then output << byte
|
|
96
|
+
end
|
|
97
|
+
when :escape
|
|
98
|
+
@state = case byte
|
|
99
|
+
when 91 then :csi
|
|
100
|
+
when 93 then :osc
|
|
101
|
+
when 80, 88, 94, 95 then :string
|
|
102
|
+
when 32..47 then :intermediate
|
|
103
|
+
else :text
|
|
104
|
+
end
|
|
105
|
+
@escape.clear
|
|
106
|
+
when :intermediate
|
|
107
|
+
@state = :text if byte >= 48
|
|
108
|
+
when :csi
|
|
109
|
+
if byte.between?(64, 126)
|
|
110
|
+
output << "\e[#{@escape}m" if byte == 109 && @escape.match?(/\A[0-9;:]*\z/)
|
|
111
|
+
@state = :text
|
|
112
|
+
elsif @escape.bytesize < 96
|
|
113
|
+
@escape << byte
|
|
114
|
+
else
|
|
115
|
+
@state = :discard_csi
|
|
116
|
+
end
|
|
117
|
+
when :discard_csi
|
|
118
|
+
@state = :text if byte.between?(64, 126)
|
|
119
|
+
when :osc, :string
|
|
120
|
+
if byte == 27
|
|
121
|
+
@string_state = @state
|
|
122
|
+
@state = :string_escape
|
|
123
|
+
elsif byte == 7 && @state == :osc
|
|
124
|
+
@state = :text
|
|
125
|
+
end
|
|
126
|
+
when :string_escape
|
|
127
|
+
@state = byte == 92 ? :text : @string_state
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def incomplete_suffix(bytes)
|
|
132
|
+
index = bytes.bytesize - 1
|
|
133
|
+
index -= 1 while index >= 0 && bytes.getbyte(index).between?(128, 191)
|
|
134
|
+
return 0 if index.negative?
|
|
135
|
+
|
|
136
|
+
lead = bytes.getbyte(index)
|
|
137
|
+
expected = case lead
|
|
138
|
+
when 194..223 then 2
|
|
139
|
+
when 224..239 then 3
|
|
140
|
+
when 240..244 then 4
|
|
141
|
+
else 1
|
|
142
|
+
end
|
|
143
|
+
actual = bytes.bytesize - index
|
|
144
|
+
actual < expected ? actual : 0
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
data/lib/foruiman/cli.rb
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "foruiman"
|
|
5
|
+
require_relative "plain"
|
|
6
|
+
require_relative "diagnostics"
|
|
7
|
+
|
|
8
|
+
# Foreman's Thor command structure, reduced to the MVP surface.
|
|
9
|
+
class Foruiman::CLI < Thor
|
|
10
|
+
map ["-v", "--version"] => :version
|
|
11
|
+
default_task :start
|
|
12
|
+
check_unknown_options!
|
|
13
|
+
remove_command :tree
|
|
14
|
+
|
|
15
|
+
class_option :procfile, type: :string, aliases: "-f", default: "Procfile", desc: "Procfile to read"
|
|
16
|
+
class_option :root, type: :string, aliases: "-d", desc: "Working directory (default: invocation directory)"
|
|
17
|
+
class_option :env, type: :string, aliases: "-e", desc: "Environment file layered after .env"
|
|
18
|
+
class_option :dotenv, type: :boolean, default: true, desc: "Load optional .env from working directory"
|
|
19
|
+
class_option :port, type: :string, aliases: "-p", default: "5000", desc: "Base port (increments by 100)"
|
|
20
|
+
class_option :log_lines, type: :string, default: "10000", desc: "Maximum records per process and in all"
|
|
21
|
+
|
|
22
|
+
def self.exit_on_failure?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc "start [PROCESS]", "Run the Procfile with process tabs, or stream logs without a TTY"
|
|
27
|
+
method_option :tui, type: :boolean, default: true, desc: "Use the terminal interface when stdin and stdout are TTYs"
|
|
28
|
+
def start(process = nil)
|
|
29
|
+
engine = build_engine
|
|
30
|
+
engine.select(process) if process
|
|
31
|
+
interactive = options[:tui] && $stdin.tty? && $stdout.tty?
|
|
32
|
+
diagnostics = Foruiman::Diagnostics.new(interactive: interactive)
|
|
33
|
+
code = if interactive
|
|
34
|
+
require_relative "tui/application"
|
|
35
|
+
Foruiman::TUI::Application.new(engine).run
|
|
36
|
+
else
|
|
37
|
+
Foruiman::Plain.new(engine).run
|
|
38
|
+
end
|
|
39
|
+
exit(code)
|
|
40
|
+
rescue Foruiman::Error, SystemCallError => e
|
|
41
|
+
raise Thor::Error, e.message
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
diagnostics&.error(e)
|
|
44
|
+
warn "foruiman: #{e.message}#{" (details: #{diagnostics.path})" if diagnostics&.path}"
|
|
45
|
+
exit(1)
|
|
46
|
+
ensure
|
|
47
|
+
engine&.close
|
|
48
|
+
diagnostics&.close
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "check", "Validate the Procfile, environment files, ports, and log capacity without starting processes"
|
|
52
|
+
def check
|
|
53
|
+
engine = build_engine
|
|
54
|
+
puts "valid Procfile (#{engine.process_names.join(', ')})"
|
|
55
|
+
rescue Foruiman::Error, SystemCallError => e
|
|
56
|
+
raise Thor::Error, e.message
|
|
57
|
+
ensure
|
|
58
|
+
engine&.close
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc "version", "Display Foruiman gem version"
|
|
62
|
+
def version
|
|
63
|
+
puts Foruiman::VERSION
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def integer_option(name)
|
|
69
|
+
text = options.fetch(name).to_s
|
|
70
|
+
raise Foruiman::Error, "#{name.to_s.tr('_', '-')} must be a positive integer" unless text.match?(/\A[0-9]+\z/)
|
|
71
|
+
|
|
72
|
+
text.to_i
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_engine
|
|
76
|
+
root = File.expand_path(options[:root] || Dir.pwd)
|
|
77
|
+
procfile = File.expand_path(options[:procfile], root)
|
|
78
|
+
file = File.expand_path(options[:env], root) if options[:env]
|
|
79
|
+
env = Foruiman::Env.load(root: root, file: file, dotenv: options[:dotenv])
|
|
80
|
+
Foruiman::Engine.new(procfile: procfile, root: root, env: env,
|
|
81
|
+
port: integer_option(:port), log_lines: integer_option(:log_lines))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Foruiman::Diagnostics
|
|
4
|
+
attr_reader :path
|
|
5
|
+
|
|
6
|
+
def initialize(interactive:, stderr: $stderr)
|
|
7
|
+
@interactive = interactive
|
|
8
|
+
@temporary = nil
|
|
9
|
+
@output = stderr
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def error(exception)
|
|
13
|
+
if @interactive && !@temporary
|
|
14
|
+
require "tempfile"
|
|
15
|
+
@temporary = Tempfile.create(["foruiman-", ".log"])
|
|
16
|
+
@temporary.chmod(0o600)
|
|
17
|
+
@output = @temporary
|
|
18
|
+
@path = @temporary.path
|
|
19
|
+
end
|
|
20
|
+
@output.puts(exception.full_message(highlight: false))
|
|
21
|
+
@output.flush
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def close
|
|
25
|
+
@temporary&.close
|
|
26
|
+
end
|
|
27
|
+
end
|