proctide 0.1.0__tar.gz
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.
- proctide-0.1.0/LICENSE +21 -0
- proctide-0.1.0/PKG-INFO +136 -0
- proctide-0.1.0/README.md +109 -0
- proctide-0.1.0/pyproject.toml +42 -0
- proctide-0.1.0/setup.cfg +4 -0
- proctide-0.1.0/src/proctide/__init__.py +6 -0
- proctide-0.1.0/src/proctide/__main__.py +6 -0
- proctide-0.1.0/src/proctide/cli.py +108 -0
- proctide-0.1.0/src/proctide/core.py +89 -0
- proctide-0.1.0/src/proctide/runner.py +148 -0
- proctide-0.1.0/src/proctide.egg-info/PKG-INFO +136 -0
- proctide-0.1.0/src/proctide.egg-info/SOURCES.txt +14 -0
- proctide-0.1.0/src/proctide.egg-info/dependency_links.txt +1 -0
- proctide-0.1.0/src/proctide.egg-info/entry_points.txt +2 -0
- proctide-0.1.0/src/proctide.egg-info/top_level.txt +1 -0
- proctide-0.1.0/tests/test_core.py +108 -0
proctide-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 proctide contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
proctide-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proctide
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run every process in your Procfile at once in one terminal, each output line prefixed with a colored process name; a clean Ctrl-C shuts them all down. Like foreman/overmind but zero-dependency and dual-runtime — no Ruby, no tmux.
|
|
5
|
+
Author: yyfjj
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jjdoor/proctide-py
|
|
8
|
+
Project-URL: Repository, https://github.com/jjdoor/proctide-py
|
|
9
|
+
Project-URL: Issues, https://github.com/jjdoor/proctide-py/issues
|
|
10
|
+
Keywords: procfile,foreman,overmind,process,process-manager,concurrent,runner,dev,devtools,cli,monorepo
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: Unix
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# proctide
|
|
29
|
+
|
|
30
|
+
**Run every process in your `Procfile` together, in one terminal.** Each output
|
|
31
|
+
line is prefixed with a colored process name so you can tell `web` from `worker`
|
|
32
|
+
from `css` at a glance — and a single Ctrl-C takes the whole lot down cleanly.
|
|
33
|
+
|
|
34
|
+
Zero dependencies (pure Python stdlib). No Ruby. No tmux.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pipx run proctide
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
web | listening on http://localhost:3000
|
|
42
|
+
worker | polling jobs queue…
|
|
43
|
+
css | rebuilt app.css in 42ms
|
|
44
|
+
web | GET / 200 11ms
|
|
45
|
+
worker | processed job #1841
|
|
46
|
+
^C
|
|
47
|
+
proctide: SIGINT received, shutting down…
|
|
48
|
+
web | exited (signal SIGTERM)
|
|
49
|
+
worker | exited (signal SIGTERM)
|
|
50
|
+
css | exited (signal SIGTERM)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Why another Procfile runner?
|
|
54
|
+
|
|
55
|
+
[foreman](https://github.com/ddollar/foreman) is the classic, but it's a **Ruby
|
|
56
|
+
gem** — one more runtime to install on a Node/Python box.
|
|
57
|
+
[overmind](https://github.com/DarthSim/overmind) is great but needs **tmux**.
|
|
58
|
+
[`concurrently`](https://www.npmjs.com/package/concurrently) is excellent and
|
|
59
|
+
mature — but it's an npm dependency, and you spell your processes out on the
|
|
60
|
+
command line instead of in a checked-in `Procfile`.
|
|
61
|
+
|
|
62
|
+
`proctide` is the small middle ground: a real `Procfile`, prefixed interleaved
|
|
63
|
+
output, clean shutdown — and **nothing to install but the tool itself**, on
|
|
64
|
+
whichever of Python or Node you already have.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pipx run proctide # no install, run on demand
|
|
70
|
+
pip install proctide # or install the `proctide` command
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
There's an identical Node build too: `npx proctide` / `npm i -g proctide`
|
|
74
|
+
(see [proctide](https://github.com/jjdoor/proctide)). Both ports parse the same
|
|
75
|
+
Procfile and print the same prefix format — their pure cores are tested against
|
|
76
|
+
the same vectors.
|
|
77
|
+
|
|
78
|
+
## The Procfile
|
|
79
|
+
|
|
80
|
+
One process per line, `name: command`. Blank lines and `#` comments are ignored.
|
|
81
|
+
The command runs through your shell, so pipes, `&&`, env vars, and globs all work.
|
|
82
|
+
|
|
83
|
+
```Procfile
|
|
84
|
+
# Procfile
|
|
85
|
+
web: python server.py
|
|
86
|
+
worker: python worker.py
|
|
87
|
+
css: npx tailwindcss -i app.css -o public/app.css --watch
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
proctide # runs ./Procfile
|
|
92
|
+
proctide -f Procfile.dev # or point somewhere else
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
proctide [options]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Option | Description |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `-f, --file <path>` | Procfile to read (default: `./Procfile`). |
|
|
104
|
+
| `--no-color` | Disable the colored name prefixes (e.g. when piping to a file). |
|
|
105
|
+
| `-h, --help` | Show help. |
|
|
106
|
+
| `-v, --version` | Print version. |
|
|
107
|
+
|
|
108
|
+
### Behavior
|
|
109
|
+
|
|
110
|
+
- **Concurrent.** Every process starts at once; their stdout *and* stderr are
|
|
111
|
+
merged into one stream, each line tagged `name | …`. The name column is padded
|
|
112
|
+
to the longest process name so the `|` separators line up.
|
|
113
|
+
- **Clean shutdown.** Ctrl-C (SIGINT) — or SIGTERM — forwards SIGTERM to every
|
|
114
|
+
child's process group, waits briefly, then exits. A child that ignores SIGTERM
|
|
115
|
+
gets SIGKILL as a backstop.
|
|
116
|
+
- **One dies, all stop.** If any process exits on its own, the rest are shut down
|
|
117
|
+
too — that's the foreman/overmind contract for a dev stack.
|
|
118
|
+
- **Honest exit code.** `proctide` exits non-zero if any child exited non-zero,
|
|
119
|
+
so it behaves in CI and `Makefile`s.
|
|
120
|
+
|
|
121
|
+
## Design notes
|
|
122
|
+
|
|
123
|
+
- **A pure core, an IO shell.** `parse_procfile`, `color_for`, and `prefix_line`
|
|
124
|
+
are pure functions — no spawn, no clock, no signals — which is what lets the
|
|
125
|
+
Python and Node ports be proven identical against one shared vector table. The
|
|
126
|
+
spawning/streaming/signal-forwarding lives in a separate runner module and is
|
|
127
|
+
covered by an integration smoke test instead (live output is nondeterministic).
|
|
128
|
+
- **Prefixes align by construction.** The name is padded to `width` *before* any
|
|
129
|
+
ANSI color is applied, so the escape codes never throw off the columns.
|
|
130
|
+
- **Zero dependencies.** Python uses `subprocess.Popen` + one reader thread per
|
|
131
|
+
process behind a shared lock so prefixed lines never interleave mid-line; Node
|
|
132
|
+
uses `child_process.spawn` + `readline`.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
proctide-0.1.0/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# proctide
|
|
2
|
+
|
|
3
|
+
**Run every process in your `Procfile` together, in one terminal.** Each output
|
|
4
|
+
line is prefixed with a colored process name so you can tell `web` from `worker`
|
|
5
|
+
from `css` at a glance — and a single Ctrl-C takes the whole lot down cleanly.
|
|
6
|
+
|
|
7
|
+
Zero dependencies (pure Python stdlib). No Ruby. No tmux.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx run proctide
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
web | listening on http://localhost:3000
|
|
15
|
+
worker | polling jobs queue…
|
|
16
|
+
css | rebuilt app.css in 42ms
|
|
17
|
+
web | GET / 200 11ms
|
|
18
|
+
worker | processed job #1841
|
|
19
|
+
^C
|
|
20
|
+
proctide: SIGINT received, shutting down…
|
|
21
|
+
web | exited (signal SIGTERM)
|
|
22
|
+
worker | exited (signal SIGTERM)
|
|
23
|
+
css | exited (signal SIGTERM)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Why another Procfile runner?
|
|
27
|
+
|
|
28
|
+
[foreman](https://github.com/ddollar/foreman) is the classic, but it's a **Ruby
|
|
29
|
+
gem** — one more runtime to install on a Node/Python box.
|
|
30
|
+
[overmind](https://github.com/DarthSim/overmind) is great but needs **tmux**.
|
|
31
|
+
[`concurrently`](https://www.npmjs.com/package/concurrently) is excellent and
|
|
32
|
+
mature — but it's an npm dependency, and you spell your processes out on the
|
|
33
|
+
command line instead of in a checked-in `Procfile`.
|
|
34
|
+
|
|
35
|
+
`proctide` is the small middle ground: a real `Procfile`, prefixed interleaved
|
|
36
|
+
output, clean shutdown — and **nothing to install but the tool itself**, on
|
|
37
|
+
whichever of Python or Node you already have.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pipx run proctide # no install, run on demand
|
|
43
|
+
pip install proctide # or install the `proctide` command
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
There's an identical Node build too: `npx proctide` / `npm i -g proctide`
|
|
47
|
+
(see [proctide](https://github.com/jjdoor/proctide)). Both ports parse the same
|
|
48
|
+
Procfile and print the same prefix format — their pure cores are tested against
|
|
49
|
+
the same vectors.
|
|
50
|
+
|
|
51
|
+
## The Procfile
|
|
52
|
+
|
|
53
|
+
One process per line, `name: command`. Blank lines and `#` comments are ignored.
|
|
54
|
+
The command runs through your shell, so pipes, `&&`, env vars, and globs all work.
|
|
55
|
+
|
|
56
|
+
```Procfile
|
|
57
|
+
# Procfile
|
|
58
|
+
web: python server.py
|
|
59
|
+
worker: python worker.py
|
|
60
|
+
css: npx tailwindcss -i app.css -o public/app.css --watch
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
proctide # runs ./Procfile
|
|
65
|
+
proctide -f Procfile.dev # or point somewhere else
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
proctide [options]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Option | Description |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `-f, --file <path>` | Procfile to read (default: `./Procfile`). |
|
|
77
|
+
| `--no-color` | Disable the colored name prefixes (e.g. when piping to a file). |
|
|
78
|
+
| `-h, --help` | Show help. |
|
|
79
|
+
| `-v, --version` | Print version. |
|
|
80
|
+
|
|
81
|
+
### Behavior
|
|
82
|
+
|
|
83
|
+
- **Concurrent.** Every process starts at once; their stdout *and* stderr are
|
|
84
|
+
merged into one stream, each line tagged `name | …`. The name column is padded
|
|
85
|
+
to the longest process name so the `|` separators line up.
|
|
86
|
+
- **Clean shutdown.** Ctrl-C (SIGINT) — or SIGTERM — forwards SIGTERM to every
|
|
87
|
+
child's process group, waits briefly, then exits. A child that ignores SIGTERM
|
|
88
|
+
gets SIGKILL as a backstop.
|
|
89
|
+
- **One dies, all stop.** If any process exits on its own, the rest are shut down
|
|
90
|
+
too — that's the foreman/overmind contract for a dev stack.
|
|
91
|
+
- **Honest exit code.** `proctide` exits non-zero if any child exited non-zero,
|
|
92
|
+
so it behaves in CI and `Makefile`s.
|
|
93
|
+
|
|
94
|
+
## Design notes
|
|
95
|
+
|
|
96
|
+
- **A pure core, an IO shell.** `parse_procfile`, `color_for`, and `prefix_line`
|
|
97
|
+
are pure functions — no spawn, no clock, no signals — which is what lets the
|
|
98
|
+
Python and Node ports be proven identical against one shared vector table. The
|
|
99
|
+
spawning/streaming/signal-forwarding lives in a separate runner module and is
|
|
100
|
+
covered by an integration smoke test instead (live output is nondeterministic).
|
|
101
|
+
- **Prefixes align by construction.** The name is padded to `width` *before* any
|
|
102
|
+
ANSI color is applied, so the escape codes never throw off the columns.
|
|
103
|
+
- **Zero dependencies.** Python uses `subprocess.Popen` + one reader thread per
|
|
104
|
+
process behind a shared lock so prefixed lines never interleave mid-line; Node
|
|
105
|
+
uses `child_process.spawn` + `readline`.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "proctide"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Run every process in your Procfile at once in one terminal, each output line prefixed with a colored process name; a clean Ctrl-C shuts them all down. Like foreman/overmind but zero-dependency and dual-runtime — no Ruby, no tmux."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "yyfjj" }]
|
|
13
|
+
keywords = ["procfile", "foreman", "overmind", "process", "process-manager", "concurrent", "runner", "dev", "devtools", "cli", "monorepo"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: POSIX",
|
|
21
|
+
"Operating System :: MacOS",
|
|
22
|
+
"Operating System :: Unix",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Topic :: Software Development :: Build Tools",
|
|
25
|
+
"Topic :: System :: Systems Administration",
|
|
26
|
+
"Topic :: Utilities",
|
|
27
|
+
]
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/jjdoor/proctide-py"
|
|
32
|
+
Repository = "https://github.com/jjdoor/proctide-py"
|
|
33
|
+
Issues = "https://github.com/jjdoor/proctide-py/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
proctide = "proctide.cli:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
package-dir = { "" = "src" }
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
proctide-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""proctide command-line interface — read a Procfile, hand it to the runner."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from . import core, runner
|
|
7
|
+
|
|
8
|
+
VERSION = "0.1.0"
|
|
9
|
+
|
|
10
|
+
_COLOR = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _c(code, s):
|
|
14
|
+
return f"\x1b[{code}m{s}\x1b[0m" if _COLOR else s
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def bold(s): return _c("1", s)
|
|
18
|
+
def dim(s): return _c("2", s)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
HELP = f"""{bold('proctide')} — run your Procfile's processes together, one colored prefix each.
|
|
22
|
+
|
|
23
|
+
{bold('Usage')}
|
|
24
|
+
proctide [options] # runs ./Procfile
|
|
25
|
+
proctide -f Procfile.dev
|
|
26
|
+
|
|
27
|
+
proctide # web | … / worker | … / css | … interleaved
|
|
28
|
+
proctide --no-color | tee dev.log
|
|
29
|
+
|
|
30
|
+
{bold('Options')}
|
|
31
|
+
-f, --file <path> Procfile to read (default: ./Procfile)
|
|
32
|
+
--no-color disable colored name prefixes
|
|
33
|
+
-h, --help show this help
|
|
34
|
+
-v, --version print version
|
|
35
|
+
|
|
36
|
+
{bold('Procfile format')}
|
|
37
|
+
{dim('# one process per line: name: command')}
|
|
38
|
+
web: python server.py
|
|
39
|
+
worker: python worker.py
|
|
40
|
+
css: npx tailwindcss -i in.css -o out.css --watch
|
|
41
|
+
|
|
42
|
+
{bold('What it does')}
|
|
43
|
+
- spawns every process at once; each output line is prefixed {_c('36', 'web')} | … {_c('32', 'worker')} | …
|
|
44
|
+
- Ctrl-C sends SIGTERM to all children, waits, then exits
|
|
45
|
+
- exits non-zero if any process exited non-zero
|
|
46
|
+
- zero dependencies — like foreman, but no Ruby and no tmux
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def die(msg):
|
|
51
|
+
sys.stderr.write(f"proctide: {msg}\n")
|
|
52
|
+
sys.exit(2)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _value(argv, i):
|
|
56
|
+
if i + 1 >= len(argv):
|
|
57
|
+
die(f"{argv[i]} needs a value")
|
|
58
|
+
return argv[i + 1]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_args(argv):
|
|
62
|
+
opts = {"file": "Procfile", "color": None}
|
|
63
|
+
i = 0
|
|
64
|
+
while i < len(argv):
|
|
65
|
+
a = argv[i]
|
|
66
|
+
if a in ("-f", "--file"):
|
|
67
|
+
opts["file"] = _value(argv, i); i += 2; continue
|
|
68
|
+
if a == "--color":
|
|
69
|
+
opts["color"] = True; i += 1; continue
|
|
70
|
+
if a == "--no-color":
|
|
71
|
+
opts["color"] = False; i += 1; continue
|
|
72
|
+
die(f'unknown option "{a}" (try --help)')
|
|
73
|
+
return opts
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv=None):
|
|
77
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
78
|
+
if "-h" in argv or "--help" in argv:
|
|
79
|
+
sys.stdout.write(HELP)
|
|
80
|
+
return 0
|
|
81
|
+
if "-v" in argv or "--version" in argv:
|
|
82
|
+
sys.stdout.write(VERSION + "\n")
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
opts = parse_args(argv)
|
|
86
|
+
|
|
87
|
+
if opts["color"] is None:
|
|
88
|
+
opts["color"] = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
with open(opts["file"], "r", encoding="utf-8") as f:
|
|
92
|
+
text = f.read()
|
|
93
|
+
except FileNotFoundError:
|
|
94
|
+
die(f'no Procfile at "{os.path.abspath(opts["file"])}" (use -f to point elsewhere)')
|
|
95
|
+
except OSError as e:
|
|
96
|
+
die(f'could not read "{opts["file"]}": {e}')
|
|
97
|
+
|
|
98
|
+
entries, errors = core.parse_procfile(text)
|
|
99
|
+
for err in errors:
|
|
100
|
+
sys.stderr.write(dim(f'proctide: ignoring {opts["file"]}:{err["line"]} — {err["reason"]}') + "\n")
|
|
101
|
+
if not entries:
|
|
102
|
+
die(f'no runnable processes found in "{opts["file"]}"')
|
|
103
|
+
|
|
104
|
+
return runner.run(entries, color=opts["color"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
sys.exit(main())
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""proctide core — pure Procfile parsing & line-prefix formatting.
|
|
2
|
+
|
|
3
|
+
No spawn, no fs, no clock, no signals, no streams.
|
|
4
|
+
|
|
5
|
+
The whole tool is one idea: run every process in a Procfile at once in a single
|
|
6
|
+
terminal, and tag each line of output with a colored, padded process name so you
|
|
7
|
+
can tell ``web`` from ``worker`` from ``css`` at a glance — then a clean Ctrl-C
|
|
8
|
+
takes the whole lot down. foreman does this in Ruby; overmind needs tmux.
|
|
9
|
+
proctide is zero-dependency and ships for both Node and Python.
|
|
10
|
+
|
|
11
|
+
Everything in this file is a pure function of its arguments, so this port and
|
|
12
|
+
the Node one are checked against the exact same input -> output vectors
|
|
13
|
+
(tests/test_core.py and test/core.test.js share one VECTORS table). The live
|
|
14
|
+
runner — concurrent spawn, interleaved streams, signal forwarding — is
|
|
15
|
+
inherently nondeterministic and lives in runner.py, behavior-aligned but not
|
|
16
|
+
vector-tested.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Deterministic ANSI foreground palette, cycled by process index. Picked to be
|
|
20
|
+
# readable on a dark terminal and distinct from each other. Codes are shared
|
|
21
|
+
# verbatim with the Node port. (No red — red reads as "error".)
|
|
22
|
+
PALETTE = ["36", "32", "33", "34", "35", "96", "92", "93"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_procfile(text):
|
|
26
|
+
"""Parse Procfile text into an ordered list of ``{"name", "cmd"}`` dicts.
|
|
27
|
+
|
|
28
|
+
Format (foreman-compatible subset): one process per line, ``name: command``.
|
|
29
|
+
- blank lines and lines whose first non-space char is ``#`` are skipped
|
|
30
|
+
- the name is everything before the FIRST colon; the command is the rest,
|
|
31
|
+
so a command may freely contain extra colons (``sh -c 'date: now'``)
|
|
32
|
+
- name and command are both trimmed
|
|
33
|
+
- a line with no colon, an empty name, or an empty command is ignored
|
|
34
|
+
(it cannot be run) and recorded in the returned ``errors`` list
|
|
35
|
+
|
|
36
|
+
Returns ``(entries, errors)`` where ``entries`` is a list of dicts and
|
|
37
|
+
``errors`` is a list of ``{"line", "text", "reason"}`` dicts. Mirrors the
|
|
38
|
+
Node port, whose array carries the same errors as an attached property.
|
|
39
|
+
"""
|
|
40
|
+
entries = []
|
|
41
|
+
errors = []
|
|
42
|
+
lines = str(text).split("\n")
|
|
43
|
+
for i, raw in enumerate(lines):
|
|
44
|
+
# Match Node's /\r?\n/ split: drop a trailing CR if present.
|
|
45
|
+
if raw.endswith("\r"):
|
|
46
|
+
raw = raw[:-1]
|
|
47
|
+
line_no = i + 1
|
|
48
|
+
stripped = raw.strip()
|
|
49
|
+
if stripped == "" or stripped[0] == "#":
|
|
50
|
+
continue
|
|
51
|
+
colon = raw.find(":")
|
|
52
|
+
if colon == -1:
|
|
53
|
+
errors.append({"line": line_no, "text": stripped, "reason": 'no colon (expected "name: command")'})
|
|
54
|
+
continue
|
|
55
|
+
name = raw[:colon].strip()
|
|
56
|
+
cmd = raw[colon + 1:].strip()
|
|
57
|
+
if name == "":
|
|
58
|
+
errors.append({"line": line_no, "text": stripped, "reason": "empty process name"})
|
|
59
|
+
continue
|
|
60
|
+
if cmd == "":
|
|
61
|
+
errors.append({"line": line_no, "text": stripped, "reason": "empty command"})
|
|
62
|
+
continue
|
|
63
|
+
entries.append({"name": name, "cmd": cmd})
|
|
64
|
+
return entries, errors
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def color_for(index):
|
|
68
|
+
"""Deterministic ANSI color code for the Nth process, cycling the palette.
|
|
69
|
+
|
|
70
|
+
Negative indices wrap too (defensive; the runner only ever passes >= 0).
|
|
71
|
+
"""
|
|
72
|
+
n = len(PALETTE)
|
|
73
|
+
i = (int(index) % n + n) % n
|
|
74
|
+
return PALETTE[i]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def prefix_line(name, line, width, color_code, use_color):
|
|
78
|
+
"""Format one line of a child's output for our stdout::
|
|
79
|
+
|
|
80
|
+
web | listening on :3000
|
|
81
|
+
|
|
82
|
+
The name is right-padded to ``width`` (the longest process name, so the
|
|
83
|
+
``|`` separators line up) and, when ``use_color``, wrapped in the given ANSI
|
|
84
|
+
code. Padding is added BEFORE coloring so the visible columns stay aligned
|
|
85
|
+
and the escape sequences never count toward width. Pure: no globals, no IO.
|
|
86
|
+
"""
|
|
87
|
+
padded = str(name).ljust(width)
|
|
88
|
+
label = f"\x1b[{color_code}m{padded}\x1b[0m" if use_color else padded
|
|
89
|
+
return f"{label} | {line}"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""proctide runner — the IO half: spawn every process concurrently, prefix each
|
|
2
|
+
line of their interleaved stdout/stderr, forward signals, and report exits.
|
|
3
|
+
|
|
4
|
+
This module is deliberately NOT vector-tested: live interleaved process output is
|
|
5
|
+
nondeterministic. It is behavior-aligned with the Node port (same Procfile ->
|
|
6
|
+
same prefix format) and exercised by the integration smoke test instead. All the
|
|
7
|
+
deterministic string work lives in core.py.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import signal
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
from . import core
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run(entries, color=False, out=None):
|
|
21
|
+
"""Run a list of ``{"name", "cmd"}`` entries concurrently.
|
|
22
|
+
|
|
23
|
+
Returns the process exit code to use (non-zero if any child exited non-zero).
|
|
24
|
+
"""
|
|
25
|
+
if out is None:
|
|
26
|
+
out = sys.stdout
|
|
27
|
+
if not entries:
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
width = max(len(e["name"]) for e in entries)
|
|
31
|
+
lock = threading.Lock() # serialize writes so prefixed lines never tear
|
|
32
|
+
state = {"worst": 0, "shutting_down": False}
|
|
33
|
+
procs = [] # list of (entry, color_code, Popen)
|
|
34
|
+
posix = os.name == "posix"
|
|
35
|
+
|
|
36
|
+
def write(s):
|
|
37
|
+
with lock:
|
|
38
|
+
out.write(s)
|
|
39
|
+
out.flush()
|
|
40
|
+
|
|
41
|
+
def dim(s):
|
|
42
|
+
return f"\x1b[2m{s}\x1b[0m" if color else s
|
|
43
|
+
|
|
44
|
+
def emit(name, color_code, line):
|
|
45
|
+
write(core.prefix_line(name, line, width, color_code, color) + "\n")
|
|
46
|
+
|
|
47
|
+
def signal_proc(proc, sig):
|
|
48
|
+
if proc.poll() is not None:
|
|
49
|
+
return
|
|
50
|
+
# Signal the whole process group (start_new_session=True made the child a
|
|
51
|
+
# group leader), so a `sh -c 'sleep 30'` takes its `sleep` grandchild
|
|
52
|
+
# down too instead of orphaning it. Fall back to the direct child.
|
|
53
|
+
try:
|
|
54
|
+
if posix:
|
|
55
|
+
os.killpg(proc.pid, sig)
|
|
56
|
+
else:
|
|
57
|
+
proc.send_signal(sig)
|
|
58
|
+
except (ProcessLookupError, OSError):
|
|
59
|
+
try:
|
|
60
|
+
proc.send_signal(sig)
|
|
61
|
+
except (ProcessLookupError, OSError):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def shutdown(sig=signal.SIGTERM):
|
|
65
|
+
if state["shutting_down"]:
|
|
66
|
+
return
|
|
67
|
+
state["shutting_down"] = True
|
|
68
|
+
for _e, _c, p in procs:
|
|
69
|
+
signal_proc(p, sig)
|
|
70
|
+
|
|
71
|
+
def reader(entry, color_code, proc):
|
|
72
|
+
# One thread per process; bufsize=1 + text gives us line-at-a-time reads.
|
|
73
|
+
for line in proc.stdout:
|
|
74
|
+
emit(entry["name"], color_code, line.rstrip("\n"))
|
|
75
|
+
proc.wait()
|
|
76
|
+
code = proc.returncode
|
|
77
|
+
if code is not None and code < 0:
|
|
78
|
+
shown = f"signal {signal.Signals(-code).name}"
|
|
79
|
+
else:
|
|
80
|
+
shown = f"code {code}"
|
|
81
|
+
write(dim(core.prefix_line(entry["name"], f"exited ({shown})", width, color_code, False)) + "\n")
|
|
82
|
+
with lock:
|
|
83
|
+
if code and code > state["worst"]:
|
|
84
|
+
state["worst"] = code
|
|
85
|
+
# A process that *crashes* on its own (non-zero, or killed by a signal we
|
|
86
|
+
# didn't send) tears the rest down too — the foreman/overmind contract
|
|
87
|
+
# for a dev stack. But a clean exit (code 0) is just a one-shot step
|
|
88
|
+
# finishing; it must NOT kill the long-running siblings, and the reader
|
|
89
|
+
# above has already drained this child's pipe to EOF before we get here.
|
|
90
|
+
if not state["shutting_down"] and code not in (0, None):
|
|
91
|
+
shutdown(signal.SIGTERM)
|
|
92
|
+
|
|
93
|
+
# ---- spawn everything ----
|
|
94
|
+
threads = []
|
|
95
|
+
for idx, entry in enumerate(entries):
|
|
96
|
+
color_code = core.color_for(idx)
|
|
97
|
+
popen_kwargs = dict(
|
|
98
|
+
shell=True,
|
|
99
|
+
stdout=subprocess.PIPE,
|
|
100
|
+
stderr=subprocess.STDOUT,
|
|
101
|
+
bufsize=1,
|
|
102
|
+
universal_newlines=True,
|
|
103
|
+
)
|
|
104
|
+
if posix:
|
|
105
|
+
popen_kwargs["start_new_session"] = True # own process group
|
|
106
|
+
try:
|
|
107
|
+
proc = subprocess.Popen(entry["cmd"], **popen_kwargs)
|
|
108
|
+
except OSError as e:
|
|
109
|
+
emit(entry["name"], color_code, f"proctide: failed to start: {e}")
|
|
110
|
+
with lock:
|
|
111
|
+
if state["worst"] == 0:
|
|
112
|
+
state["worst"] = 1
|
|
113
|
+
continue
|
|
114
|
+
procs.append((entry, color_code, proc))
|
|
115
|
+
t = threading.Thread(target=reader, args=(entry, color_code, proc), daemon=True)
|
|
116
|
+
threads.append(t)
|
|
117
|
+
t.start()
|
|
118
|
+
|
|
119
|
+
# ---- forward signals from the foreground ----
|
|
120
|
+
def handle(signum, _frame):
|
|
121
|
+
name = signal.Signals(signum).name
|
|
122
|
+
sys.stderr.write(dim(f"\nproctide: {name} received, shutting down…") + "\n")
|
|
123
|
+
sys.stderr.flush()
|
|
124
|
+
shutdown(signal.SIGTERM)
|
|
125
|
+
|
|
126
|
+
old_int = signal.signal(signal.SIGINT, handle)
|
|
127
|
+
old_term = signal.signal(signal.SIGTERM, handle)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
for t in threads:
|
|
131
|
+
t.join()
|
|
132
|
+
# Backstop: if anything is still alive (ignored SIGTERM), SIGKILL it.
|
|
133
|
+
deadline = time.time() + 5
|
|
134
|
+
for _e, _c, p in procs:
|
|
135
|
+
remaining = max(0.0, deadline - time.time())
|
|
136
|
+
try:
|
|
137
|
+
p.wait(timeout=remaining)
|
|
138
|
+
except subprocess.TimeoutExpired:
|
|
139
|
+
signal_proc(p, signal.SIGKILL)
|
|
140
|
+
try:
|
|
141
|
+
p.wait(timeout=2)
|
|
142
|
+
except subprocess.TimeoutExpired:
|
|
143
|
+
pass
|
|
144
|
+
finally:
|
|
145
|
+
signal.signal(signal.SIGINT, old_int)
|
|
146
|
+
signal.signal(signal.SIGTERM, old_term)
|
|
147
|
+
|
|
148
|
+
return state["worst"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proctide
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run every process in your Procfile at once in one terminal, each output line prefixed with a colored process name; a clean Ctrl-C shuts them all down. Like foreman/overmind but zero-dependency and dual-runtime — no Ruby, no tmux.
|
|
5
|
+
Author: yyfjj
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jjdoor/proctide-py
|
|
8
|
+
Project-URL: Repository, https://github.com/jjdoor/proctide-py
|
|
9
|
+
Project-URL: Issues, https://github.com/jjdoor/proctide-py/issues
|
|
10
|
+
Keywords: procfile,foreman,overmind,process,process-manager,concurrent,runner,dev,devtools,cli,monorepo
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: Unix
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# proctide
|
|
29
|
+
|
|
30
|
+
**Run every process in your `Procfile` together, in one terminal.** Each output
|
|
31
|
+
line is prefixed with a colored process name so you can tell `web` from `worker`
|
|
32
|
+
from `css` at a glance — and a single Ctrl-C takes the whole lot down cleanly.
|
|
33
|
+
|
|
34
|
+
Zero dependencies (pure Python stdlib). No Ruby. No tmux.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pipx run proctide
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
web | listening on http://localhost:3000
|
|
42
|
+
worker | polling jobs queue…
|
|
43
|
+
css | rebuilt app.css in 42ms
|
|
44
|
+
web | GET / 200 11ms
|
|
45
|
+
worker | processed job #1841
|
|
46
|
+
^C
|
|
47
|
+
proctide: SIGINT received, shutting down…
|
|
48
|
+
web | exited (signal SIGTERM)
|
|
49
|
+
worker | exited (signal SIGTERM)
|
|
50
|
+
css | exited (signal SIGTERM)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Why another Procfile runner?
|
|
54
|
+
|
|
55
|
+
[foreman](https://github.com/ddollar/foreman) is the classic, but it's a **Ruby
|
|
56
|
+
gem** — one more runtime to install on a Node/Python box.
|
|
57
|
+
[overmind](https://github.com/DarthSim/overmind) is great but needs **tmux**.
|
|
58
|
+
[`concurrently`](https://www.npmjs.com/package/concurrently) is excellent and
|
|
59
|
+
mature — but it's an npm dependency, and you spell your processes out on the
|
|
60
|
+
command line instead of in a checked-in `Procfile`.
|
|
61
|
+
|
|
62
|
+
`proctide` is the small middle ground: a real `Procfile`, prefixed interleaved
|
|
63
|
+
output, clean shutdown — and **nothing to install but the tool itself**, on
|
|
64
|
+
whichever of Python or Node you already have.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pipx run proctide # no install, run on demand
|
|
70
|
+
pip install proctide # or install the `proctide` command
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
There's an identical Node build too: `npx proctide` / `npm i -g proctide`
|
|
74
|
+
(see [proctide](https://github.com/jjdoor/proctide)). Both ports parse the same
|
|
75
|
+
Procfile and print the same prefix format — their pure cores are tested against
|
|
76
|
+
the same vectors.
|
|
77
|
+
|
|
78
|
+
## The Procfile
|
|
79
|
+
|
|
80
|
+
One process per line, `name: command`. Blank lines and `#` comments are ignored.
|
|
81
|
+
The command runs through your shell, so pipes, `&&`, env vars, and globs all work.
|
|
82
|
+
|
|
83
|
+
```Procfile
|
|
84
|
+
# Procfile
|
|
85
|
+
web: python server.py
|
|
86
|
+
worker: python worker.py
|
|
87
|
+
css: npx tailwindcss -i app.css -o public/app.css --watch
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
proctide # runs ./Procfile
|
|
92
|
+
proctide -f Procfile.dev # or point somewhere else
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
proctide [options]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Option | Description |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `-f, --file <path>` | Procfile to read (default: `./Procfile`). |
|
|
104
|
+
| `--no-color` | Disable the colored name prefixes (e.g. when piping to a file). |
|
|
105
|
+
| `-h, --help` | Show help. |
|
|
106
|
+
| `-v, --version` | Print version. |
|
|
107
|
+
|
|
108
|
+
### Behavior
|
|
109
|
+
|
|
110
|
+
- **Concurrent.** Every process starts at once; their stdout *and* stderr are
|
|
111
|
+
merged into one stream, each line tagged `name | …`. The name column is padded
|
|
112
|
+
to the longest process name so the `|` separators line up.
|
|
113
|
+
- **Clean shutdown.** Ctrl-C (SIGINT) — or SIGTERM — forwards SIGTERM to every
|
|
114
|
+
child's process group, waits briefly, then exits. A child that ignores SIGTERM
|
|
115
|
+
gets SIGKILL as a backstop.
|
|
116
|
+
- **One dies, all stop.** If any process exits on its own, the rest are shut down
|
|
117
|
+
too — that's the foreman/overmind contract for a dev stack.
|
|
118
|
+
- **Honest exit code.** `proctide` exits non-zero if any child exited non-zero,
|
|
119
|
+
so it behaves in CI and `Makefile`s.
|
|
120
|
+
|
|
121
|
+
## Design notes
|
|
122
|
+
|
|
123
|
+
- **A pure core, an IO shell.** `parse_procfile`, `color_for`, and `prefix_line`
|
|
124
|
+
are pure functions — no spawn, no clock, no signals — which is what lets the
|
|
125
|
+
Python and Node ports be proven identical against one shared vector table. The
|
|
126
|
+
spawning/streaming/signal-forwarding lives in a separate runner module and is
|
|
127
|
+
covered by an integration smoke test instead (live output is nondeterministic).
|
|
128
|
+
- **Prefixes align by construction.** The name is padded to `width` *before* any
|
|
129
|
+
ANSI color is applied, so the escape codes never throw off the columns.
|
|
130
|
+
- **Zero dependencies.** Python uses `subprocess.Popen` + one reader thread per
|
|
131
|
+
process behind a shared lock so prefixed lines never interleave mid-line; Node
|
|
132
|
+
uses `child_process.spawn` + `readline`.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/proctide/__init__.py
|
|
5
|
+
src/proctide/__main__.py
|
|
6
|
+
src/proctide/cli.py
|
|
7
|
+
src/proctide/core.py
|
|
8
|
+
src/proctide/runner.py
|
|
9
|
+
src/proctide.egg-info/PKG-INFO
|
|
10
|
+
src/proctide.egg-info/SOURCES.txt
|
|
11
|
+
src/proctide.egg-info/dependency_links.txt
|
|
12
|
+
src/proctide.egg-info/entry_points.txt
|
|
13
|
+
src/proctide.egg-info/top_level.txt
|
|
14
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
proctide
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from proctide.core import parse_procfile, color_for, prefix_line, PALETTE
|
|
4
|
+
|
|
5
|
+
ESC = "\x1b"
|
|
6
|
+
|
|
7
|
+
# ===================================================================
|
|
8
|
+
# SHARED VECTORS. The Node port (test/core.test.js) runs the exact same
|
|
9
|
+
# three tables, so both languages are proven to agree byte-for-byte on
|
|
10
|
+
# the pure functions. (The live runner is nondeterministic and gets a
|
|
11
|
+
# separate integration smoke test, not vectors.)
|
|
12
|
+
# ===================================================================
|
|
13
|
+
|
|
14
|
+
# parse_procfile: text -> (entries, errors)
|
|
15
|
+
PARSE_VECTORS = [
|
|
16
|
+
(
|
|
17
|
+
"basic two processes",
|
|
18
|
+
"web: node server.js\nworker: node worker.js",
|
|
19
|
+
[{"name": "web", "cmd": "node server.js"}, {"name": "worker", "cmd": "node worker.js"}],
|
|
20
|
+
[],
|
|
21
|
+
),
|
|
22
|
+
(
|
|
23
|
+
"skips blanks and # comments",
|
|
24
|
+
"# my Procfile\n\nweb: rackup\n\n # indented comment\njob: ./run\n",
|
|
25
|
+
[{"name": "web", "cmd": "rackup"}, {"name": "job", "cmd": "./run"}],
|
|
26
|
+
[],
|
|
27
|
+
),
|
|
28
|
+
(
|
|
29
|
+
"trims name and command whitespace",
|
|
30
|
+
" web : node server.js ",
|
|
31
|
+
[{"name": "web", "cmd": "node server.js"}],
|
|
32
|
+
[],
|
|
33
|
+
),
|
|
34
|
+
(
|
|
35
|
+
"command may contain extra colons",
|
|
36
|
+
"clock: sh -c 'echo time: now; sleep 1'",
|
|
37
|
+
[{"name": "clock", "cmd": "sh -c 'echo time: now; sleep 1'"}],
|
|
38
|
+
[],
|
|
39
|
+
),
|
|
40
|
+
(
|
|
41
|
+
"line without a colon is an error",
|
|
42
|
+
"web: ok\nthis has no colon\njob: ok2",
|
|
43
|
+
[{"name": "web", "cmd": "ok"}, {"name": "job", "cmd": "ok2"}],
|
|
44
|
+
[{"line": 2, "text": "this has no colon", "reason": 'no colon (expected "name: command")'}],
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"empty name and empty command are errors",
|
|
48
|
+
": nameless\nweb: \nworker: real",
|
|
49
|
+
[{"name": "worker", "cmd": "real"}],
|
|
50
|
+
[
|
|
51
|
+
{"line": 1, "text": ": nameless", "reason": "empty process name"},
|
|
52
|
+
{"line": 2, "text": "web:", "reason": "empty command"},
|
|
53
|
+
],
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"empty input yields no entries",
|
|
57
|
+
"",
|
|
58
|
+
[],
|
|
59
|
+
[],
|
|
60
|
+
),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# color_for: index -> ANSI code (deterministic palette cycling).
|
|
64
|
+
COLOR_VECTORS = [
|
|
65
|
+
(0, "36"),
|
|
66
|
+
(1, "32"),
|
|
67
|
+
(2, "33"),
|
|
68
|
+
(3, "34"),
|
|
69
|
+
(4, "35"),
|
|
70
|
+
(5, "96"),
|
|
71
|
+
(6, "92"),
|
|
72
|
+
(7, "93"),
|
|
73
|
+
(8, "36"), # wraps
|
|
74
|
+
(9, "32"),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# prefix_line: (name, line, width, color_code, use_color) -> string
|
|
78
|
+
PREFIX_VECTORS = [
|
|
79
|
+
("pads name to width, no color", ("web", "listening on :3000", 6, "36", False), "web | listening on :3000"),
|
|
80
|
+
("longer name, no padding needed", ("worker", "job done", 6, "32", False), "worker | job done"),
|
|
81
|
+
("name longer than width is not truncated", ("database", "ready", 3, "33", False), "database | ready"),
|
|
82
|
+
("color wraps the padded name only", ("web", "up", 6, "36", True), f"{ESC}[36mweb {ESC}[0m | up"),
|
|
83
|
+
("empty line still gets a prefix", ("web", "", 3, "34", False), "web | "),
|
|
84
|
+
("width zero pads nothing", ("x", "hi", 0, "35", False), "x | hi"),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("name,raw,entries,errors", PARSE_VECTORS, ids=[v[0] for v in PARSE_VECTORS])
|
|
89
|
+
def test_parse_procfile(name, raw, entries, errors):
|
|
90
|
+
got_entries, got_errors = parse_procfile(raw)
|
|
91
|
+
assert got_entries == entries
|
|
92
|
+
assert got_errors == errors
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.parametrize("index,expected", COLOR_VECTORS, ids=[str(v[0]) for v in COLOR_VECTORS])
|
|
96
|
+
def test_color_for(index, expected):
|
|
97
|
+
assert color_for(index) == expected
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.parametrize("name,args,expected", PREFIX_VECTORS, ids=[v[0] for v in PREFIX_VECTORS])
|
|
101
|
+
def test_prefix_line(name, args, expected):
|
|
102
|
+
assert prefix_line(*args) == expected
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_palette_has_no_red():
|
|
106
|
+
# red reads as "error"; keep it out of the process palette
|
|
107
|
+
assert "31" not in PALETTE
|
|
108
|
+
assert "91" not in PALETTE
|