scout-essentials 1.8.7 → 1.9.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/.vimproject +26 -12
- data/README.md +83 -112
- data/VERSION +1 -1
- data/doc/Improvements.md +226 -0
- data/doc/StartHere.md +122 -0
- data/doc/developer/AnnotationSystem.md +184 -0
- data/doc/developer/Architecture.md +147 -0
- data/doc/developer/Configuration.md +238 -0
- data/doc/developer/CoreUtilities.md +265 -0
- data/doc/developer/DesignPrinciples.md +129 -0
- data/doc/developer/ErrorHandling.md +203 -0
- data/doc/developer/LockingAndConcurrency.md +157 -0
- data/doc/developer/PathResolution.md +200 -0
- data/doc/developer/PersistenceAndResources.md +119 -0
- data/doc/developer/StreamingModel.md +236 -0
- data/doc/user/AnnotatingData.md +202 -0
- data/doc/user/CachingResults.md +183 -0
- data/doc/user/CommandLineOptions.md +189 -0
- data/doc/user/Cookbook.md +211 -0
- data/doc/user/HandlingStreams.md +236 -0
- data/doc/user/LoggingAndProgress.md +158 -0
- data/doc/user/ProducingResources.md +177 -0
- data/doc/user/RemoteData.md +157 -0
- data/doc/user/RunningCommands.md +218 -0
- data/doc/user/WorkingWithFiles.md +217 -0
- data/lib/scout/cmd.rb +343 -40
- data/lib/scout/concurrent_stream.rb +14 -1
- data/lib/scout/indiferent_hash.rb +1 -1
- data/lib/scout/log/fingerprint.rb +13 -8
- data/lib/scout/log/progress/report.rb +1 -1
- data/lib/scout/log.rb +4 -1
- data/lib/scout/misc/digest.rb +6 -5
- data/lib/scout/misc/format.rb +24 -0
- data/lib/scout/named_array.rb +1 -1
- data/lib/scout/open/stream.rb +2 -2
- data/lib/scout/open/util.rb +8 -4
- data/lib/scout/open.rb +3 -3
- data/lib/scout/path/find.rb +3 -2
- data/lib/scout/persist.rb +14 -10
- data/lib/scout/resource/produce.rb +9 -1
- data/research/annotations-data-analysis.md +206 -0
- data/research/behavior-probes.md +1925 -0
- data/research/commands-streaming-analysis.md +272 -0
- data/research/design-philosophy-analysis.md +383 -0
- data/research/doc-audit-findings.md +294 -0
- data/research/ecosystem-attribution.md +118 -0
- data/research/implementation-inventory-core.md +1029 -0
- data/research/implementation-inventory-open.md +417 -0
- data/research/implementation-inventory-path-persist-resource.md +774 -0
- data/research/io-paths-analysis.md +228 -0
- data/research/persistence-resources-analysis.md +244 -0
- data/research/synthesis-report.md +80 -0
- data/scout-essentials.gemspec +37 -15
- data/test/scout/open/test_remote.rb +1 -2
- data/test/scout/test_cmd.rb +411 -0
- metadata +36 -14
- data/doc/Annotation.md +0 -352
- data/doc/CMD.md +0 -363
- data/doc/ConcurrentStream.md +0 -163
- data/doc/IndiferentHash.md +0 -240
- data/doc/Log.md +0 -235
- data/doc/NamedArray.md +0 -174
- data/doc/Open.md +0 -331
- data/doc/Path.md +0 -217
- data/doc/Persist.md +0 -214
- data/doc/Resource.md +0 -229
- data/doc/SimpleOPT.md +0 -236
- data/doc/TmpFile.md +0 -154
data/doc/CMD.md
DELETED
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
# CMD
|
|
2
|
-
|
|
3
|
-
CMD provides a convenience layer for running external commands, capturing/streaming their IO, integrating with the framework's ConcurrentStream and Open helpers, and for tool discovery/installation helpers. It wraps Open3.popen3 and adds standard patterns for piping, feeding stdin, logging stderr, auto-joining producer threads/processes, and error handling.
|
|
4
|
-
|
|
5
|
-
Key features:
|
|
6
|
-
- Run commands (synchronously or as streams) with flexible options.
|
|
7
|
-
- Pipe command output as ConcurrentStream-enabled IO so consumers can read and then join/wait for producers.
|
|
8
|
-
- Feed data into command stdin from String/IO.
|
|
9
|
-
- Collect and log stderr, optionally saving it.
|
|
10
|
-
- Auto-join producer threads/PIDs and surface process failures as exceptions.
|
|
11
|
-
- Tool discovery/installation helpers (TOOLS registry, get_tool, conda, scan version).
|
|
12
|
-
- Convenience helpers: bash, cmd_pid, cmd_log.
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## Basic usage
|
|
17
|
-
|
|
18
|
-
- CMD.cmd(command_or_tool, cmd_fragment_or_options = nil, options = {}) -> returns:
|
|
19
|
-
- When run with `:pipe => true` returns an IO-like stream (ConcurrentStream-enabled) that you can read from; caller should join or let autojoin close/join.
|
|
20
|
-
- When `:pipe => false` (default) returns a StringIO containing stdout (collected), after waiting for process completion.
|
|
21
|
-
|
|
22
|
-
Examples:
|
|
23
|
-
```ruby
|
|
24
|
-
# simple capture
|
|
25
|
-
out = CMD.cmd("echo '{opt}' test").read # => "test\n"
|
|
26
|
-
# with options processed into the command
|
|
27
|
-
out = CMD.cmd("cut", "-f" => 2, "-d" => ' ', :in => "a b").read # => "b\n"
|
|
28
|
-
|
|
29
|
-
# pipe mode (stream returned)
|
|
30
|
-
stream = CMD.cmd("tail -f /var/log/syslog", :pipe => true)
|
|
31
|
-
puts stream.read # streaming consumption
|
|
32
|
-
stream.join # wait for producers and check exit status
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Common gotchas
|
|
36
|
-
|
|
37
|
-
These are the most common sources of confusion when using `CMD.cmd`:
|
|
38
|
-
|
|
39
|
-
- **String vs Symbol as the first argument**:
|
|
40
|
-
- `CMD.cmd('mytool ...')` runs exactly that shell command.
|
|
41
|
-
- `CMD.cmd(:mytool, ...)` goes through `CMD.get_tool(:mytool)` first (tool registry / bootstrap). This is useful when you want
|
|
42
|
-
tool discovery/installation behavior.
|
|
43
|
-
- If you do *not* rely on the tool registry, prefer passing the command as a String.
|
|
44
|
-
|
|
45
|
-
- **Pipe mode needs a `join` (for correct error detection)**:
|
|
46
|
-
- With `:pipe => true`, you typically do `io = CMD.cmd(..., pipe: true)` and then `io.read`.
|
|
47
|
-
- To reliably detect failures (non-zero exit), call `io.join` (unless you deliberately set `no_fail: true`).
|
|
48
|
-
|
|
49
|
-
- **Non-pipe mode returns a `StringIO`**:
|
|
50
|
-
- With `:pipe => false` (default), CMD collects stdout and returns a `StringIO` *after* the process completes.
|
|
51
|
-
- This is convenient, but can be memory-heavy for large outputs.
|
|
52
|
-
|
|
53
|
-
- **`save_stderr` vs logging**:
|
|
54
|
-
- `log: true` (default in many contexts) logs stderr lines as they arrive.
|
|
55
|
-
- `save_stderr: true` additionally accumulates stderr into `io.std_err` (useful for raising helpful exceptions).
|
|
56
|
-
|
|
57
|
-
- **`no_fail` suppresses exceptions**:
|
|
58
|
-
- If you pass `no_fail: true`, CMD will not raise `ProcessFailed` / `ConcurrentStreamProcessFailed` on non-zero exits.
|
|
59
|
-
- This is useful for "try it" probes, but make sure you explicitly check `io.exit_status` (or parse output) when you need correctness.
|
|
60
|
-
|
|
61
|
-
- **`{opt}` placeholder**:
|
|
62
|
-
- If the command string contains the exact substring `'{opt}'`, CMD replaces it with the processed options string.
|
|
63
|
-
- Otherwise, options are appended to the end of the command.
|
|
64
|
-
|
|
65
|
-
- **Second argument ambiguity**:
|
|
66
|
-
- `CMD.cmd(tool, {...})` means the second argument is treated as options and `cmd_fragment_or_options` becomes nil.
|
|
67
|
-
- If you intended to pass a command fragment, pass it as a String, e.g. `CMD.cmd('cut', "-f 2", in: "a b")`.
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## Important options
|
|
72
|
-
|
|
73
|
-
All options are passed as an options Hash (converted with IndiferentHash), and many are special keys:
|
|
74
|
-
|
|
75
|
-
### Understanding `CMD.cmd` arguments
|
|
76
|
-
|
|
77
|
-
`CMD.cmd` has a flexible signature:
|
|
78
|
-
|
|
79
|
-
- `CMD.cmd(tool_or_cmd, cmd_fragment_or_options = nil, options = {})`
|
|
80
|
-
|
|
81
|
-
Common calling styles:
|
|
82
|
-
|
|
83
|
-
```ruby
|
|
84
|
-
# 1) Single string command
|
|
85
|
-
io = CMD.cmd("echo hello")
|
|
86
|
-
|
|
87
|
-
# 2) Tool + command fragment
|
|
88
|
-
io = CMD.cmd("cut", "-f 2 -d ' '", in: "a b")
|
|
89
|
-
|
|
90
|
-
# 3) Tool + options hash (options are converted to CLI flags)
|
|
91
|
-
io = CMD.cmd("cut", {"-f" => 2, "-d" => " "}, in: "a b")
|
|
92
|
-
|
|
93
|
-
# 4) Tool registry symbol (uses CMD.get_tool first)
|
|
94
|
-
io = CMD.cmd(:python, "--version")
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Notes:
|
|
98
|
-
- If the *second* argument is a Hash, it is treated as option flags and `cmd_fragment_or_options` becomes nil.
|
|
99
|
-
- Options are shell-quoted; values containing single quotes are escaped.
|
|
100
|
-
|
|
101
|
-
- :pipe (boolean) — if true, return a stream you can read from; otherwise CMD returns a StringIO after the process completes.
|
|
102
|
-
- :in — input to feed to the command:
|
|
103
|
-
- String will be wrapped by StringIO and streamed to process stdin.
|
|
104
|
-
- IO/StringIO passed will be consumed using `readpartial` in a background thread.
|
|
105
|
-
- :stderr — controls stderr logging/handling:
|
|
106
|
-
- Integer severity → Log.log writes at that severity.
|
|
107
|
-
- true → maps to Log::HIGH.
|
|
108
|
-
- If stderr is enabled, stderr lines are logged as they arrive.
|
|
109
|
-
- :post — callable (proc) run after command finishes (attached as stream callback in pipe mode).
|
|
110
|
-
- :log — boolean to enable logging of stderr to Log (default true in many paths). Passing true/false toggles logging.
|
|
111
|
-
- :no_fail (or :nofail) — if true do not raise on non-zero exit in pipe-mode setup; if omitted errors raise ProcessFailed or ConcurrentStreamProcessFailed.
|
|
112
|
-
- :autojoin — when true, the returned stream will auto-join producers on EOF/close (defaults in many calls to match :no_wait).
|
|
113
|
-
- :no_wait — don't wait for process to finish (used to set autojoin).
|
|
114
|
-
- :xvfb — if true or string, wrap command in xvfb-run with server args (helper for GUI/CMD).
|
|
115
|
-
- :progress_bar / :bar — pass a ProgressBar object to process stderr lines via bar.process.
|
|
116
|
-
- :save_stderr — if true, collect stderr lines into stream.std_err.
|
|
117
|
-
- :dont_close_in — when feeding :in IO, do not close the source IO after streaming to stdin.
|
|
118
|
-
- :log, :autojoin, :no_fail, :pipe, :in etc. are all processed and removed from the command string.
|
|
119
|
-
|
|
120
|
-
Command option helpers:
|
|
121
|
-
- CMD.process_cmd_options(options_hash) → returns CLI options string:
|
|
122
|
-
- If `:add_option_dashes` key set, keys without leading dashes are prefixed with `--`.
|
|
123
|
-
- Values are quoted and single quotes escaped.
|
|
124
|
-
- Handles boolean flags (true/false/nil).
|
|
125
|
-
|
|
126
|
-
Examples:
|
|
127
|
-
```ruby
|
|
128
|
-
CMD.process_cmd_options("--user-agent" => "firefox")
|
|
129
|
-
# => "--user-agent 'firefox'"
|
|
130
|
-
|
|
131
|
-
CMD.process_cmd_options("--user-agent=" => "firefox")
|
|
132
|
-
# => "--user-agent='firefox'"
|
|
133
|
-
|
|
134
|
-
CMD.process_cmd_options("-q" => true)
|
|
135
|
-
# => "-q"
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
## Streaming mode internals
|
|
141
|
-
|
|
142
|
-
When `:pipe => true`:
|
|
143
|
-
- CMD uses Open3.popen3 to spawn the process and receives sin (stdin), sout (stdout), serr (stderr), wait_thr.
|
|
144
|
-
- If `:in` is provided and is an IO/StringIO, a background thread writes it into process stdin (unless `dont_close_in`).
|
|
145
|
-
- Stderr is consumed in a background thread (either logged via Log at the provided severity, or passed to ProgressBar if provided, or collected if `save_stderr`).
|
|
146
|
-
- `ConcurrentStream.setup` is called on the returned `sout` with threads and pids plus options like `autojoin` and `no_fail`.
|
|
147
|
-
- That allows consumers to call `sout.read`, `sout.join`, or rely on `autojoin` to close/join automatically.
|
|
148
|
-
- `sout.callback` can be set to `post` callable to run after successful join.
|
|
149
|
-
|
|
150
|
-
Error handling:
|
|
151
|
-
- For pipe mode the library will detect non-zero process exit and raise `ConcurrentStreamProcessFailed` on join unless `no_fail` is true.
|
|
152
|
-
- If `:no_fail` is passed true, failures are logged but not raised.
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
## Non-pipe mode internals
|
|
157
|
-
|
|
158
|
-
When `:pipe` is false (default):
|
|
159
|
-
- CMD still uses Open3.popen3, but it reads all stdout into a StringIO and waits for process completion before returning.
|
|
160
|
-
- Stderr is read in a background thread and optionally logged/collected; after process completion, if process exit is non-zero, a `ProcessFailed` exception is raised (unless `no_fail`).
|
|
161
|
-
- This mode is convenient for quick synchronous captures.
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## Tool discovery & installation helpers
|
|
166
|
-
|
|
167
|
-
- CMD.tool(name, claim = nil, test = nil, cmd = nil, &block)
|
|
168
|
-
- Register tools with metadata: claim (Resource or Path), a test command, install block/command and optional fallback cmd string.
|
|
169
|
-
|
|
170
|
-
- CMD.get_tool(tool)
|
|
171
|
-
- Check if tool is available (runs `test` or `cmd --help`); if not, attempts to produce claim or run registered block to install.
|
|
172
|
-
- Caches result in @@init_cmd_tool to avoid repeated checks.
|
|
173
|
-
- Attempts to read version by trying `--version`, `-version`, `--help`, etc., and parsing text via `CMD.scan_version_text`.
|
|
174
|
-
|
|
175
|
-
- CMD.scan_version_text(text, cmd = nil) → returns matched version string or nil.
|
|
176
|
-
- Heuristics to find version substrings related to the command name.
|
|
177
|
-
|
|
178
|
-
- CMD.conda(tool, env = nil, channel = 'bioconda')
|
|
179
|
-
- Convenience to install with conda in either a given env or the login shell.
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
|
-
## Convenience wrappers
|
|
184
|
-
|
|
185
|
-
- CMD.bash(command_string)
|
|
186
|
-
- Runs the given commands inside `bash -l` (login shell) and returns the resulting stream (pipe) — helpful when you need shell initialization (e.g., conda).
|
|
187
|
-
|
|
188
|
-
- CMD.cmd_pid(...) / CMD.cmd_log(...)
|
|
189
|
-
- `cmd_pid` runs a pipe command while streaming stdout to STDERR (or logs) and returns nil; it handles progress bars and returns after join.
|
|
190
|
-
- `cmd_log` is a thin wrapper around `cmd_pid` that simply returns nil.
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
## Error types
|
|
195
|
-
|
|
196
|
-
- ProcessFailed — raised for non-zero exit in synchronous mode or when explicitly checked.
|
|
197
|
-
- ConcurrentStreamProcessFailed — raised when pipe-mode join detects failing producer subprocess (non-zero exit) and `no_fail` is not set.
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## Examples (from tests)
|
|
202
|
-
|
|
203
|
-
- Basic command capture:
|
|
204
|
-
```ruby
|
|
205
|
-
CMD.cmd("echo '{opt}' test").read # -> "test\n"
|
|
206
|
-
CMD.cmd("cut", "-f" => 2, "-d" => ' ', :in => "one two").read # -> "two\n"
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
- Pipe usage:
|
|
210
|
-
```ruby
|
|
211
|
-
stream = CMD.cmd("echo test", :pipe => true)
|
|
212
|
-
puts stream.read # "test\n"
|
|
213
|
-
stream.join
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
- Piped pipeline:
|
|
217
|
-
```ruby
|
|
218
|
-
f = Open.open(file)
|
|
219
|
-
io = CMD.cmd('tail -n 10', :in => f, :pipe => true)
|
|
220
|
-
io2 = CMD.cmd('head -n 10', :in => io, :pipe => true)
|
|
221
|
-
io3 = CMD.cmd('head -n 10', :in => io2, :pipe => true)
|
|
222
|
-
puts io3.read.split("\n").length # => 10
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
- Handling errors:
|
|
226
|
-
```ruby
|
|
227
|
-
# Raises ProcessFailed for missing command
|
|
228
|
-
CMD.cmd('fake-command')
|
|
229
|
-
|
|
230
|
-
# In pipe mode you may get ConcurrentStreamProcessFailed on join or read/join
|
|
231
|
-
CMD.cmd('grep . NONEXISTINGFILE', :pipe => true).join
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
- Use `:no_fail => true` to suppress exceptions on failure and just log.
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
## Recommendations & patterns
|
|
239
|
-
|
|
240
|
-
### Robust error-handling pattern
|
|
241
|
-
|
|
242
|
-
A common robust pattern is:
|
|
243
|
-
|
|
244
|
-
```ruby
|
|
245
|
-
io = CMD.cmd("SomeTool", "--flag value", log: true, save_stderr: true, no_fail: true)
|
|
246
|
-
|
|
247
|
-
# Decide how to handle failure
|
|
248
|
-
if io.exit_status != 0
|
|
249
|
-
raise ScoutException, io.read + "
|
|
250
|
-
" + io.std_err.to_s
|
|
251
|
-
end
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
- `no_fail: true` prevents immediate exceptions (useful so you can include stderr in your own error message).
|
|
255
|
-
- If you want CMD to raise automatically, omit `no_fail` and rely on `ProcessFailed` / `ConcurrentStreamProcessFailed`.
|
|
256
|
-
|
|
257
|
-
### Large outputs
|
|
258
|
-
|
|
259
|
-
- Prefer `:pipe => true` when you want to stream and transform output without buffering everything in memory.
|
|
260
|
-
- If you need to keep the full output, write it to a file as you consume it, and return/keep only a small summary in memory.
|
|
261
|
-
|
|
262
|
-
### Pipelines
|
|
263
|
-
|
|
264
|
-
When composing multiple commands, use pipe mode and pass the upstream stream as `:in`:
|
|
265
|
-
|
|
266
|
-
```ruby
|
|
267
|
-
io1 = CMD.cmd("tool1", "--emit", pipe: true)
|
|
268
|
-
io2 = CMD.cmd("tool2", "--filter", in: io1, pipe: true)
|
|
269
|
-
out = io2.read
|
|
270
|
-
io2.join
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
- Prefer `:pipe => true` + ConcurrentStream when you want streaming processing without waiting for full output in memory.
|
|
274
|
-
- Provide `:in` as an IO to stream large inputs into a subprocess.
|
|
275
|
-
- Use `:autojoin => true` to automatically join producers on EOF/close (useful for simple consumers).
|
|
276
|
-
- Register tools via `CMD.tool` and use `CMD.get_tool` to locate or auto-install/produce required tools.
|
|
277
|
-
- Always check or propagate exceptions from `join` for pipe-mode streams to detect failing subprocesses.
|
|
278
|
-
|
|
279
|
-
---
|
|
280
|
-
|
|
281
|
-
## Quick API reference
|
|
282
|
-
|
|
283
|
-
- CMD.cmd(tool_or_cmd, cmd_fragment_or_options = nil, options = {}) => StringIO or ConcurrentStream (when pipe)
|
|
284
|
-
- CMD.process_cmd_options(options_hash) => option string appended to command
|
|
285
|
-
- CMD.setup tool registry:
|
|
286
|
-
- CMD.tool(name, claim=nil, test=nil, cmd=nil, &block)
|
|
287
|
-
- CMD.get_tool(name)
|
|
288
|
-
- CMD.scan_version_text(text, cmd = nil)
|
|
289
|
-
- CMD.versions -> hash of detected versions
|
|
290
|
-
- CMD.bash(cmd_string) — run in bash -l
|
|
291
|
-
- CMD.cmd_pid / CMD.cmd_log — helpers for logging and running commands that stream stdout to logs
|
|
292
|
-
- CMD.conda(tool, env=nil, channel='bioconda') — convenience installer wrapper
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
|
-
CMD centralizes robust process execution patterns needed throughout the framework: streaming, joining, logging, error detection and tool bootstrap. Use its options to control behavior for production-grade command invocation.
|
|
297
|
-
|
|
298
|
-
---
|
|
299
|
-
|
|
300
|
-
## Designing command wrappers for workflows and agents
|
|
301
|
-
|
|
302
|
-
When you wrap external CLI tools inside workflow tasks (or expose them to agents), use a pattern that maximizes reproducibility and keeps outputs small for downstream consumers:
|
|
303
|
-
|
|
304
|
-
- Backend task (tool runner)
|
|
305
|
-
- Run the binary, write all full outputs to `step.files_dir` (use `file('name')` helpers).
|
|
306
|
-
- Return a compact JSON summary with:
|
|
307
|
-
- `files`: list of important output file paths (full paths inside `.files/`).
|
|
308
|
-
- `params`: what parameters were used (seeds, time, sample_count, fixed clamps, etc.).
|
|
309
|
-
- optional small parsed snippets (e.g. final probabilities) if tiny.
|
|
310
|
-
- Use `CMD.cmd('Binary', ...)` (string) unless the tool is registered; include `save_stderr: true` so errors are captured.
|
|
311
|
-
|
|
312
|
-
- Analysis task (summary)
|
|
313
|
-
- `dep` on the backend task and parse only the necessary outputs into a compact summary suitable for the interactive/LLM context (JSON, small tables, phenotype probs).
|
|
314
|
-
- Echo the backend `params` in the returned result so cached runs are auditable.
|
|
315
|
-
|
|
316
|
-
Benefits:
|
|
317
|
-
- Clear cache boundaries: the backend is the expensive, cacheable step; the analysis is cheap and reproducible given the backend outputs.
|
|
318
|
-
- Agents never have to load entire trace files; they get a compact summary.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
## Quick checklist when using CMD in workflows
|
|
322
|
-
|
|
323
|
-
- Prefer `CMD.cmd('Binary', ...)` string invocation unless you intentionally want the tool registry/install behavior via symbol form.
|
|
324
|
-
- For long-running streaming tasks use `:pipe => true` and **always** `join` the returned stream (or rely on `autojoin`) to detect failures.
|
|
325
|
-
- If you must ignore process failures for probing, use `no_fail: true` but explicitly check exit code or outputs later.
|
|
326
|
-
- Use `save_stderr: true` when you will raise on error: include `io.std_err` in exception messages.
|
|
327
|
-
- Avoid returning raw `StringIO` of huge outputs from tasks — write to `step.files_dir` instead and return a small summary.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
## Minimal patterns/examples
|
|
331
|
-
|
|
332
|
-
Read-only capture (small output):
|
|
333
|
-
```ruby
|
|
334
|
-
out = CMD.cmd("echo hello").read # synchronous, small stdout
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Streaming safe consumption:
|
|
338
|
-
```ruby
|
|
339
|
-
stream = CMD.cmd("tail -f /some/log", pipe: true)
|
|
340
|
-
begin
|
|
341
|
-
data = stream.read
|
|
342
|
-
ensure
|
|
343
|
-
stream.join # ensure you detect non-zero exit and collect stderr
|
|
344
|
-
end
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
Pass an IO to stdin safely:
|
|
348
|
-
```ruby
|
|
349
|
-
f = Open.open('input.txt')
|
|
350
|
-
io = CMD.cmd('someprog', :in => f, pipe: true)
|
|
351
|
-
puts io.read
|
|
352
|
-
io.join
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
Tool registry vs direct call:
|
|
356
|
-
```ruby
|
|
357
|
-
# use tool registry (only if the tool is registered in CMD.tool)
|
|
358
|
-
CMD.cmd(:my_registered_tool, '--version')
|
|
359
|
-
# prefer direct call when portability is desired
|
|
360
|
-
CMD.cmd('my_tool --version')
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
For more advanced patterns and examples see the `CMD` implementation and tests in the codebase.
|
data/doc/ConcurrentStream.md
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# ConcurrentStream
|
|
2
|
-
|
|
3
|
-
ConcurrentStream is a mixin that augments IO-like stream objects (pipes returned by subprocess wrappers, in-memory streams, etc.) with concurrency-aware lifecycle management: tracking threads and child PIDs that produce/consume the stream, coordinated joining, aborting, callbacks, and safe cleanup. It is used throughout the framework for streams returned by commands (CMD.cmd) and by tee/pipe helpers in Open.
|
|
4
|
-
|
|
5
|
-
There is also a tiny AbortedStream helper to mark a stream as aborted and attach an exception.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## What it does
|
|
10
|
-
|
|
11
|
-
When a stream is set up with ConcurrentStream.setup, the stream is extended with methods and attributes to:
|
|
12
|
-
- record producer threads and child PIDs (threads/pids),
|
|
13
|
-
- automatically join and check producer exit status,
|
|
14
|
-
- attach callbacks to run once production is complete,
|
|
15
|
-
- abort producers/consumers cleanly (raise exceptions in threads, kill PIDs),
|
|
16
|
-
- support autjoining/auto-closing when the consumer finishes reading,
|
|
17
|
-
- attach a lock object that will be unlocked after join,
|
|
18
|
-
- carry metadata: filename, log, paired stream (pair), next stream in pipeline, and more.
|
|
19
|
-
|
|
20
|
-
This lets a consumer read from a stream and then reliably wait for producers to finish and detect failures (non-zero exit), or abort the whole pipeline on errors.
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
## Setup
|
|
25
|
-
|
|
26
|
-
ConcurrentStream.setup(stream, options = {}, &block)
|
|
27
|
-
|
|
28
|
-
- Extends `stream` with ConcurrentStream methods (unless already extended).
|
|
29
|
-
- Options (recognized):
|
|
30
|
-
- :threads — thread or array of threads that produce or manage this stream.
|
|
31
|
-
- :pids — pid or array of child process ids to wait for.
|
|
32
|
-
- :callback — proc to call after successful join (can also be provided as block).
|
|
33
|
-
- :abort_callback — proc to call on abort.
|
|
34
|
-
- :filename — textual name for logging/error messages.
|
|
35
|
-
- :autojoin — boolean, if true join on close/read EOF and auto-unlock lock.
|
|
36
|
-
- :lock — Lockfile instance to unlock after join.
|
|
37
|
-
- :no_fail — boolean; if true treat non-zero child exit as non-fatal.
|
|
38
|
-
- :pair — paired stream (e.g., the other side of a pipe) so aborts propagate.
|
|
39
|
-
- :next — next stream in pipeline when teeing/forwarding
|
|
40
|
-
- :log, :std_err — metadata captured for error messages
|
|
41
|
-
- If a block is given it is appended to the stream callback.
|
|
42
|
-
|
|
43
|
-
Example:
|
|
44
|
-
```ruby
|
|
45
|
-
ConcurrentStream.setup(io, threads: [t], pids: [pid], autojoin: true, filename: "ls-out")
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## Important attributes & predicates
|
|
51
|
-
|
|
52
|
-
The stream object gets attributes:
|
|
53
|
-
- threads, pids — lists of threads and subprocess PIDs to manage.
|
|
54
|
-
- callback, abort_callback — procs to call on success/abort.
|
|
55
|
-
- filename — friendly name used in logs and error messages.
|
|
56
|
-
- joined? — true after join completed.
|
|
57
|
-
- aborted? — true after abort called.
|
|
58
|
-
- autjoin — whether to auto-join on EOF/close.
|
|
59
|
-
- lock — optional Lockfile to unlock after join.
|
|
60
|
-
- stream_exception — exception captured that should be re-raised by readers/joins.
|
|
61
|
-
- no_fail — allow ignoring nonzero child exit.
|
|
62
|
-
|
|
63
|
-
AbortedStream.setup(obj, exception = nil) can be used to mark a stream aborted and attach exception (helper used internally).
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## Joining / waiting
|
|
68
|
-
|
|
69
|
-
- join_threads
|
|
70
|
-
- Joins registered threads, and if a thread's return value is a Process::Status checks success (unless no_fail). If a thread represented a subprocess and exit status indicates failure, raises ConcurrentStreamProcessFailed.
|
|
71
|
-
|
|
72
|
-
- join_pids
|
|
73
|
-
- Waits for PIDs via Process.waitpid and raises on non-zero exit unless no_fail.
|
|
74
|
-
|
|
75
|
-
- join_callback
|
|
76
|
-
- Runs `callback` once and clears it.
|
|
77
|
-
|
|
78
|
-
- join
|
|
79
|
-
- Calls join_threads, join_pids, raises stored stream_exception if set, runs callback, closes stream (if not closed) and marks joined. Also unlocks `lock` if present. Any exceptions are propagated after unlocking.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Aborting
|
|
84
|
-
|
|
85
|
-
- abort(exception=nil)
|
|
86
|
-
- Mark stream aborted, store exception in stream_exception, call abort_callback, abort threads (raise into threads) and kill PIDs with SIGINT (best-effort), clear callbacks, close stream, and unlock lock if held. Also propagate abort to `pair` stream if present.
|
|
87
|
-
|
|
88
|
-
- abort_threads(exception=nil)
|
|
89
|
-
- Raises exception (or Aborted) into producer threads and joins them.
|
|
90
|
-
|
|
91
|
-
- abort_pids
|
|
92
|
-
- Kills pids with INT.
|
|
93
|
-
|
|
94
|
-
Use abort to ensure fast cleanup on error and to signal paired streams.
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## Reading & closing
|
|
99
|
-
|
|
100
|
-
- read(*args)
|
|
101
|
-
- Wraps normal `read` with exception capture: on error stores `stream_exception` and re-raises. If `autojoin` is enabled and EOF reached, `close` is called automatically.
|
|
102
|
-
|
|
103
|
-
- close(*args)
|
|
104
|
-
- If `autojoin` is true, `close` will try to `join` (ensuring producers have finished) and then close; on exceptions it aborts, joins, and re-raises.
|
|
105
|
-
|
|
106
|
-
`joined?` and `aborted?` reflect the stream's lifecycle.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Callbacks
|
|
111
|
-
|
|
112
|
-
- add_callback(&block)
|
|
113
|
-
- Attaches an additional callback executed after producers finish. Multiple callbacks stack and are executed in order at join time.
|
|
114
|
-
|
|
115
|
-
- callback and abort_callback may be set in setup — used by caller to run cleanup or post-processing.
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## Error propagation
|
|
120
|
-
|
|
121
|
-
- stream_raise_exception(exception)
|
|
122
|
-
- Stores `stream_exception`, raises it into all producer threads (so they can abort), and calls `abort`.
|
|
123
|
-
|
|
124
|
-
- When join discovers a failing child process, it raises a ConcurrentStreamProcessFailed. Tests exercise this by running `grep` on a nonexisting file and asserting the exception is raised.
|
|
125
|
-
|
|
126
|
-
When `no_fail` is set true, non-zero exits or join errors are logged but not raised.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## Utilities
|
|
131
|
-
|
|
132
|
-
- annotate(stream) — copy the current stream's threads/pids/callback/etc. onto another stream (useful when creating derived streams).
|
|
133
|
-
- filename — returns stored filename or a fallback derived from stream.inspect.
|
|
134
|
-
- process_stream(stream, close: true, join: true, message: "...") { ... }
|
|
135
|
-
- Class method wrapper that sets up the stream and ensures the block runs, then closes and joins the stream as requested. On exceptions it aborts the stream and re-raises.
|
|
136
|
-
|
|
137
|
-
Example usage (from framework):
|
|
138
|
-
- `CMD.cmd(..., pipe: true, autojoin: true)` returns a ConcurrentStream-enabled IO. Consumer reads from the stream; when EOF reached or read finishes, the stream auto-joins producers and raises errors on non-zero exit.
|
|
139
|
-
|
|
140
|
-
Test examples:
|
|
141
|
-
```ruby
|
|
142
|
-
# success case
|
|
143
|
-
io = CMD.cmd("ls", pipe: true, autojoin: true)
|
|
144
|
-
io.read
|
|
145
|
-
io.close
|
|
146
|
-
|
|
147
|
-
# failure case raises ConcurrentStreamProcessFailed
|
|
148
|
-
io = CMD.cmd("grep . NONEXISTINGFILE", pipe: true, autojoin: true)
|
|
149
|
-
io.read # raises ConcurrentStreamProcessFailed
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## Typical patterns and recommendations
|
|
155
|
-
|
|
156
|
-
- When producing streams from background threads or subprocesses, call `ConcurrentStream.setup(stream, threads: t, pids: [pid], autojoin: true, filename: name)` so readers can join/observe failures.
|
|
157
|
-
- When teeing a stream to multiple outputs, register the splitter thread as a producer on each out-stream so each consumer can join independently.
|
|
158
|
-
- Use `abort(exception)` to stop whole pipeline on error and ensure all producers/consumers are signaled and cleaned up.
|
|
159
|
-
- Use `process_stream` helper to wrap a block that processes a stream and ensure it is closed and joined safely.
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
ConcurrentStream centralizes safe management of concurrent IO pipelines: shared producers (threads/PIDs), stream cleanup, callback semantics, and error propagation. It is a core primitive used by Open, CMD, and persistence/teeing helpers to make streaming robust in multi-threaded / multi-process contexts.
|