scout-essentials 1.9.1 → 1.9.3
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 +18 -1
- data/Rakefile +9 -0
- data/VERSION +1 -1
- data/doc/Improvements.md +20 -0
- data/doc/StartHere.md +7 -4
- data/doc/developer/AnnotationSystem.md +10 -15
- data/doc/developer/Configuration.md +20 -30
- data/doc/developer/CoreUtilities.md +52 -22
- data/doc/developer/DesignPrinciples.md +7 -9
- data/doc/developer/DocumentationPractices.md +476 -0
- data/doc/developer/ErrorHandling.md +11 -20
- data/doc/developer/LockingAndConcurrency.md +20 -13
- data/doc/developer/StreamingModel.md +13 -14
- data/doc/developer/Testing.md +389 -0
- data/doc/user/AnnotatingData.md +9 -13
- data/doc/user/CommandLineOptions.md +9 -12
- data/doc/user/Cookbook.md +7 -10
- data/doc/user/HandlingStreams.md +22 -29
- data/doc/user/LoggingAndProgress.md +9 -11
- data/doc/user/ProducingResources.md +0 -1
- data/doc/user/RemoteData.md +10 -13
- data/doc/user/RunningCommands.md +12 -17
- data/doc/user/WorkingWithFiles.md +10 -2
- data/lib/scout/config.rb +1 -0
- data/lib/scout/indiferent_hash.rb +8 -2
- data/lib/scout/log/fingerprint.rb +1 -1
- data/lib/scout/named_array.rb +4 -0
- data/lib/scout/open/final.rb +1 -1
- data/lib/scout/open/stream.rb +14 -13
- data/lib/scout/path/util.rb +1 -1
- data/lib/scout/testing/availability.rb +264 -0
- data/lib/scout/testing/scout_state.rb +193 -0
- data/lib/scout/testing.rb +82 -0
- data/research/doc-audit-findings.md +7 -5
- data/research/doc-structure.md +89 -0
- data/research/lock-namespace-blindness.md +73 -0
- data/research/probing-guide.md +311 -0
- data/research/testing-availability-scout-ai-handoff.md +235 -0
- data/research/testing-availability-scout-rig-handoff.md +355 -0
- data/scout-essentials.gemspec +24 -3
- data/test/scout/annotation/test_dup_clone.rb +31 -0
- data/test/scout/indiferent_hash/test_case_insensitive_staleness.rb +27 -0
- data/test/scout/indiferent_hash/test_string2hash_false.rb +13 -0
- data/test/scout/misc/test_timespan.rb +30 -0
- data/test/scout/open/test_compression_detection.rb +32 -0
- data/test/scout/open/test_forced_gzip_broken.rb +25 -0
- data/test/scout/test_named_array_resolution.rb +49 -0
- data/test/scout/test_testing.rb +195 -0
- data/test/scout/test_tmpfile_cleanup.rb +23 -0
- data/test/scout/testing/test_availability.rb +222 -0
- data/test/scout/testing/test_state_bindings.rb +163 -0
- data/test/test_helper.rb +26 -12
- metadata +23 -2
data/doc/user/Cookbook.md
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
# Cookbook
|
|
2
2
|
|
|
3
3
|
Short, self-contained recipes that combine the scout-essentials modules.
|
|
4
|
-
Every snippet here was executed
|
|
5
|
-
heading and live in `tmp/rewrite_C/` (older probes in `tmp/rewrite_A/`,
|
|
6
|
-
`tmp/rewrite_B/` are referenced by their P-numbers, which map to
|
|
7
|
-
`research/behavior-probes.md`).
|
|
4
|
+
Every snippet here was executed against the current `lib/scout/`.
|
|
8
5
|
|
|
9
6
|
The theme of the library: plain objects annotated with provenance, paths
|
|
10
7
|
resolved from a declaration, work cached on disk, streams piped without
|
|
@@ -31,8 +28,8 @@ produced (a type like `:string`/`:proc`, literal contents, or a block); the
|
|
|
31
28
|
claimed `Path` is annotated and its `produce` materialises it into the
|
|
32
29
|
resource tree (`lib/scout/resource/path.rb:2`,
|
|
33
30
|
`lib/scout/resource/produce.rb`). `produce_and_find` produces when needed and
|
|
34
|
-
returns `self.find
|
|
35
|
-
|
|
31
|
+
returns `self.find` (first and second call return the same path, `Open.read`
|
|
32
|
+
yields the claimed content).
|
|
36
33
|
|
|
37
34
|
## Cache invalidation with `:update` and `:check`
|
|
38
35
|
|
|
@@ -51,7 +48,7 @@ d = Persist.persist('dependent', :marshal,
|
|
|
51
48
|
```
|
|
52
49
|
|
|
53
50
|
`:update => true` always re-runs the block; `:check` names a file whose mtime
|
|
54
|
-
invalidates the entry
|
|
51
|
+
invalidates the entry (a == b, block not re-run; `:update`
|
|
55
52
|
re-runs; `:check` path resolves).
|
|
56
53
|
|
|
57
54
|
## Fetch a remote file
|
|
@@ -95,7 +92,7 @@ restored.tissue # => "Liver"
|
|
|
95
92
|
|
|
96
93
|
`Annotation.setup(obj, "A|B", hash)` is the module-level deserialiser; the
|
|
97
94
|
`"A|B"` string is split on `|` and each name is looked up as a constant. An
|
|
98
|
-
unknown type name is **warned about and skipped** (
|
|
95
|
+
unknown type name is **warned about and skipped** (the run prints
|
|
99
96
|
`Annotation NoSuchAnnotation not defined` on STDERR, then
|
|
100
97
|
`Annotation.setup('S004', 'NoSuchAnnotation', ...)` returns the plain
|
|
101
98
|
un-annotated string). `serialize` produces a plain `Hash` with `:literal`,
|
|
@@ -138,7 +135,7 @@ rescue Aborted, AbortedStream
|
|
|
138
135
|
end
|
|
139
136
|
```
|
|
140
137
|
|
|
141
|
-
|
|
138
|
+
(open_pipe data, tee_stream producing a real gzip file).
|
|
142
139
|
See [Handling Streams](HandlingStreams.md) and the
|
|
143
140
|
[Streaming Model](../developer/StreamingModel.md).
|
|
144
141
|
|
|
@@ -193,7 +190,7 @@ SOPT.require(options, :organism) # ParameterException when nil
|
|
|
193
190
|
```
|
|
194
191
|
|
|
195
192
|
The `*` marks an option as **taking a string value**; without it the option is
|
|
196
|
-
a boolean.
|
|
193
|
+
a boolean. (options hash, `argv_left == ["positional"]`,
|
|
197
194
|
`SOPT.require` raising). See
|
|
198
195
|
[Command-Line Options](CommandLineOptions.md).
|
|
199
196
|
|
data/doc/user/HandlingStreams.md
CHANGED
|
@@ -24,7 +24,7 @@ Attributes come from `ConcurrentStream.setup` (concurrent_stream.rb:12-70) and
|
|
|
24
24
|
`:no_fail` on the returned stream. `exit_status` exists but is *not* reliable
|
|
25
25
|
after a normal read+join — it stays `nil` because only `join_pids` sets it, and
|
|
26
26
|
that method empties `pids` when it runs; see
|
|
27
|
-
[Running Commands](RunningCommands.md)
|
|
27
|
+
[Running Commands](RunningCommands.md).
|
|
28
28
|
|
|
29
29
|
## `close` vs `join` vs abort
|
|
30
30
|
|
|
@@ -45,7 +45,7 @@ that method empties `pids` when it runs; see
|
|
|
45
45
|
`Aborted.new` raised in them, so producer bodies should rescue `Aborted`.
|
|
46
46
|
- **`force_close` does not exist** in this repo. The only reference is a dead
|
|
47
47
|
`respond_to?` guard inside `Open.grep` (open/util.rb:26); a plain IO has no
|
|
48
|
-
such method (
|
|
48
|
+
such method (`IO#respond_to?
|
|
49
49
|
(:force_close) => false`). The early-close tool is `abort`.
|
|
50
50
|
|
|
51
51
|
Consumers should follow this rescue contract:
|
|
@@ -67,9 +67,8 @@ end
|
|
|
67
67
|
## Callbacks
|
|
68
68
|
|
|
69
69
|
- `stream.add_callback(&block)` **composes**: it wraps the existing callback so
|
|
70
|
-
the *new* block runs *after* the old one (
|
|
71
|
-
`
|
|
72
|
-
[:first, :second]`). `ConcurrentStream.setup(stream, &block)` also composes in
|
|
70
|
+
the *new* block runs *after* the old one (observed order
|
|
71
|
+
`[:first, :second]`). `ConcurrentStream.setup(stream, &block)` also composes in
|
|
73
72
|
that order, which is why calling setup twice on the same stream is safe.
|
|
74
73
|
- `callback` / `abort_callback` are plain accessors over single chained procs
|
|
75
74
|
(built by setup when you pass `:callback` / `:abort_callback` options or a
|
|
@@ -96,8 +95,9 @@ there; `into_close` (default true) closes `into` when it responds to `close`.
|
|
|
96
95
|
On `Aborted` or any exception it aborts the source, closes `into`, **removes the
|
|
97
96
|
partial output file** and re-raises. With `in_thread: true` the whole drain runs
|
|
98
97
|
in a new thread that is pushed onto `io.threads`. The block runs after a
|
|
99
|
-
successful drain.
|
|
100
|
-
return: "data"`, `consume_stream into path: "into-file\n"`)
|
|
98
|
+
successful drain. (`consume_stream
|
|
99
|
+
return: "data"`, `consume_stream into path: "into-file\n"`)
|
|
100
|
+
.
|
|
101
101
|
|
|
102
102
|
### `Open.sensible_write(path, content, options = {}, &block)`
|
|
103
103
|
|
|
@@ -114,24 +114,20 @@ behaviours for streams:
|
|
|
114
114
|
- After a successful copy the content stream is joined (but not if it is a
|
|
115
115
|
`Path` or already joined).
|
|
116
116
|
|
|
117
|
-
Verified: P25/P33 in `research/behavior-probes.md` ("Aborted in
|
|
118
|
-
sensible_write: NOT raised (swallowed); partial tmp left: 0").
|
|
119
|
-
|
|
120
117
|
### `Open.open_pipe(do_fork = false, close = true, &block)`
|
|
121
118
|
|
|
122
119
|
Creates a pipe and returns the **read end** (`sout`), with the block executed in
|
|
123
120
|
a producer thread that writes the other end (`sin`).
|
|
124
121
|
|
|
125
122
|
- **Block arity**: the block always receives `sin` whether it declares a
|
|
126
|
-
parameter or not (arity
|
|
127
|
-
`probe_20_open_pipe_arity.rb` (`arity-0 block: "arity0\n"`, `arity-1: "w\n"`).
|
|
123
|
+
parameter or not (arity-0 blocks simply ignore it; `arity-0 block: "arity0\n"`, `arity-1: "w\n"`).
|
|
128
124
|
- **No block** raises `RuntimeError "No block given"`.
|
|
129
125
|
- **Fork mode** (`do_fork: true`) runs the block in a child process instead of
|
|
130
126
|
a thread: the child purges registered input pipes, closes `sout`, yields,
|
|
131
127
|
`exit! 0`; the parent closes `sin` and sets the pid on `sout` via
|
|
132
128
|
`ConcurrentStream.setup(sout, :pids => [pid])` — no threads, no callbacks.
|
|
133
129
|
`close: false` in the child leaves `sin` open after the block returns
|
|
134
|
-
(
|
|
130
|
+
(same results in fork mode with and without `close`).
|
|
135
131
|
- **Thread mode** pairs the two ends (`pair`), runs the block through
|
|
136
132
|
`ConcurrentStream.process_stream` (close+join on exit, abort on error), and
|
|
137
133
|
registers the thread on both ends. An exception in the block aborts the
|
|
@@ -141,8 +137,7 @@ a producer thread that writes the other end (`sin`).
|
|
|
141
137
|
|
|
142
138
|
Takes **no arguments** and returns the raw `[sout, sin]` pair from `IO.pipe`
|
|
143
139
|
(plus registering `sin` in `OPEN_PIPE_IN`). Calling it with a positional
|
|
144
|
-
argument raises `ArgumentError
|
|
145
|
-
`Open.pipe positional: ArgumentError`). There is no multi-command helper here —
|
|
140
|
+
argument raises `ArgumentError`. There is no multi-command helper here —
|
|
146
141
|
chain commands by feeding one stream into `:in` of the next `CMD.cmd`.
|
|
147
142
|
|
|
148
143
|
### `Open.tee_stream(stream)` / `tee_stream_thread_multiple(stream, num)`
|
|
@@ -152,8 +147,9 @@ Returns an **Array** of streams (`num` copies, default 2): the first is the
|
|
|
152
147
|
reads the source once and writes every chunk to all copies; the main copy's
|
|
153
148
|
callback joins the source and closes the extra write ends, and its
|
|
154
149
|
`abort_callback` propagates an abort to the source and the other copies.
|
|
155
|
-
|
|
156
|
-
`tee[0].autojoin => true`, `tee[1].autojoin => nil`)
|
|
150
|
+
(`tee_stream count: 2`,
|
|
151
|
+
`tee[0].autojoin => true`, `tee[1].autojoin => nil`)
|
|
152
|
+
.
|
|
157
153
|
|
|
158
154
|
```ruby
|
|
159
155
|
main, copy = Open.tee_stream(CMD.cmd('gzip -c', :pipe => true, :in => input))
|
|
@@ -167,14 +163,13 @@ Builds a tee, then a monitor thread reads the monitor copy line by line calling
|
|
|
167
163
|
consumes the returned stream, not after. Failures in the block abort the monitor
|
|
168
164
|
and are re-raised into the returned stream (`out.raise $!` when supported). The
|
|
169
165
|
returned stream is the second copy, annotated from the source and set up with
|
|
170
|
-
the monitor thread.
|
|
166
|
+
the monitor thread.
|
|
171
167
|
|
|
172
168
|
### `Open.read_stream(stream, size)`
|
|
173
169
|
|
|
174
170
|
Blocking read of exactly `size` bytes (plain `stream.read(missing)` loop,
|
|
175
171
|
`lib/scout/open/stream.rb:401`), raising `ClosedStream` if EOF is reached
|
|
176
|
-
first
|
|
177
|
-
`read_stream(4) => "0123"`.
|
|
172
|
+
first; useful for binary framing: `read_stream(4) => "0123"`.
|
|
178
173
|
|
|
179
174
|
### `Open.sort_stream(stream, header_hash: '#', cmd_args: nil, memory: false)`
|
|
180
175
|
|
|
@@ -183,8 +178,7 @@ Streams `header_hash`-prefixed lines straight through, then sorts the rest.
|
|
|
183
178
|
<cmd_args>` (`-u` by default when `cmd_args` is nil) and consumes that stream
|
|
184
179
|
into the output; `memory: true` reads the whole remainder, sorts it in Ruby and
|
|
185
180
|
writes it out. Everything runs inside `ConcurrentStream.process_stream`, so the
|
|
186
|
-
source is closed+joined and aborted on error.
|
|
187
|
-
probe_19_streaming_apis.rb: feeding `"# header\nc\na\nb\n"` returns
|
|
181
|
+
source is closed+joined and aborted on error. feeding `"# header\nc\na\nb\n"` returns
|
|
188
182
|
`"# header\na\nb\nc\n"` — the header passes through untouched, the rest is
|
|
189
183
|
sorted.
|
|
190
184
|
|
|
@@ -193,7 +187,7 @@ sorted.
|
|
|
193
187
|
Merges consecutive lines sharing the same first field, joining the other
|
|
194
188
|
columns with `|` (or dropping empty parts when `compact: true`). An optional
|
|
195
189
|
block receives the accumulated column array and its return value becomes the
|
|
196
|
-
row payload.
|
|
190
|
+
row payload.
|
|
197
191
|
|
|
198
192
|
## `Open.open` block form and `DontClose`
|
|
199
193
|
|
|
@@ -207,16 +201,15 @@ end
|
|
|
207
201
|
`Open.open` yields and **always closes and joins** the IO afterwards (the
|
|
208
202
|
`ensure` at open.rb:70-77). Raising `DontClose` with a payload makes the block
|
|
209
203
|
form *return* the payload instead of the IO, while still closing — it is an
|
|
210
|
-
early-return mechanism, not a way to keep the handle
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
early-return mechanism, not a way to keep the handle (`DontClose returns
|
|
205
|
+
payload: "payload"`, `closed after DontClose: true`). Any other exception
|
|
206
|
+
aborts, joins and re-raises the stream.
|
|
213
207
|
|
|
214
208
|
## Progress bars
|
|
215
209
|
|
|
216
210
|
Pass `:progress_bar` (a `Log::ProgressBar`, `lib/scout/log/progress.rb`; the option key is `:progress_bar` — there is no `:bar` key)
|
|
217
|
-
to `CMD.cmd` and each stderr line ticks it (`bar.process(line)` at cmd.rb:643):
|
|
218
|
-
|
|
219
|
-
non-pipe mode. With `:log => true` (`CMD.cmd_log`) the stderr text is also
|
|
211
|
+
to `CMD.cmd` and each stderr line ticks it (`bar.process(line)` at cmd.rb:643): 2 ticks for a 2-line stderr, in both
|
|
212
|
+
pipe and non-pipe mode. With `:log => true` (`CMD.cmd_log`) the stderr text is also
|
|
220
213
|
recorded in `stream.log`. `Log::ProgressBar` itself supports `:process =>
|
|
221
214
|
proc{|elem| elem.length}` so a tick can be weighted per element.
|
|
222
215
|
|
|
@@ -19,8 +19,6 @@ unrecognised value — falls back to `Log.default_severity`, which reads
|
|
|
19
19
|
`~/.scout/etc/log_severity` if that file exists and is `INFO` otherwise
|
|
20
20
|
(log.rb:23-34).
|
|
21
21
|
|
|
22
|
-
Verified by `tmp/rewrite_C/probe_04_log_severity.rb` (out04.txt): ladder
|
|
23
|
-
order, `Log.severity == 4` with no env/file, `Log.with_severity(level){}`
|
|
24
22
|
restoring the original value after the block.
|
|
25
23
|
|
|
26
24
|
Change it at runtime with `Log.severity = Log::DEBUG`, or scope a block with
|
|
@@ -38,7 +36,7 @@ Change it at runtime with `Log.severity = Log::DEBUG`, or scope a block with
|
|
|
38
36
|
- `Log::LAST` is one shared mutable `String` (starts as `"log"`, never frozen)
|
|
39
37
|
used as a protocol between `logn` and progress-bar printing so bars know how
|
|
40
38
|
many lines to move up and which kind of output came last
|
|
41
|
-
(`progress/report.rb` `print`/`report`).
|
|
39
|
+
(`progress/report.rb` `print`/`report`).
|
|
42
40
|
`class == String`, `frozen? == false`.
|
|
43
41
|
|
|
44
42
|
## Message forms
|
|
@@ -46,8 +44,8 @@ Change it at runtime with `Log.severity = Log::DEBUG`, or scope a block with
|
|
|
46
44
|
- `Log.debug "msg"` .. `Log.error "msg"` print when severity allows; the top
|
|
47
45
|
of the ladder is the `Log::NONE` constant, not a method.
|
|
48
46
|
- `Log.debug { "expensive " + build }` — **the block is lazy message
|
|
49
|
-
evaluation, not a timer**: it is only called if the level passes.
|
|
50
|
-
|
|
47
|
+
evaluation, not a timer**: it is only called if the level passes. A
|
|
48
|
+
counter incremented inside a block passed to `Log.debug`
|
|
51
49
|
stays at 0 under ERROR severity and reaches 1 under DEBUG severity.
|
|
52
50
|
- `Log.exception(e)` (log.rb:254) prints `BACKTRACE` lines derived from
|
|
53
51
|
`e.backtrace` after fingerprinting messages longer than 1000 chars; it
|
|
@@ -55,7 +53,7 @@ Change it at runtime with `Log.severity = Log::DEBUG`, or scope a block with
|
|
|
55
53
|
it contains `NOSTACK`.
|
|
56
54
|
- `Log.stack(stack)` (log.rb:318) prints a magenta header then the stack
|
|
57
55
|
**reversed** (innermost frame first) unless `SCOUT_ORIGINAL_STACK=true`.
|
|
58
|
-
|
|
56
|
+
A caller list `a.rb:1, b.rb:2, c.rb:3` prints as
|
|
59
57
|
`c.rb:3`, `b.rb:2`, `a.rb:1`.
|
|
60
58
|
|
|
61
59
|
## Top-level debug helpers (defined on Object)
|
|
@@ -66,7 +64,7 @@ DEBUG. `ddd/lll/mmm/iii/wwww/eee` inspect an object at the matching severity,
|
|
|
66
64
|
and the `f`-suffixed variants (`ddf`, `llf`, `mmf`, `iif`, `wwwf`, `eef`)
|
|
67
65
|
fingerprint it first. `sss(level[, &blk])` sets or scopes severity. `ccc`
|
|
68
66
|
(log.rb:439) enables `$scout_debug_log` around a block so nested `ccc` calls
|
|
69
|
-
print.
|
|
67
|
+
print. All five are top-level methods defined inside
|
|
70
68
|
`lib/scout/log.rb`.
|
|
71
69
|
|
|
72
70
|
## Colors
|
|
@@ -90,7 +88,7 @@ show first/middle/last elements; hashes longer than 10 fall back to
|
|
|
90
88
|
keys+values fingerprints; floats get 1/3/6 decimals depending on magnitude.
|
|
91
89
|
|
|
92
90
|
The signature is `fingerprint(obj)` — **one positional argument**; there are no
|
|
93
|
-
`max_length`/`sep` options
|
|
91
|
+
`max_length`/`sep` options . Hash rendering separates pairs with a
|
|
94
92
|
space, not a comma: `Log.fingerprint({a: 1, b: 2, c: 3})` gives
|
|
95
93
|
`"{:a=>1 :b=>2 :c=>3}"`.
|
|
96
94
|
|
|
@@ -130,13 +128,13 @@ bytes/process/callback/severity` accessors.
|
|
|
130
128
|
movement. `report_msg` shows **elapsed time and rate** and, when `max` is
|
|
131
129
|
set, an ETA: `· <rate> per sec. -- <dots> <pct>% <eta> => <elapsed> -
|
|
132
130
|
<ticks> of <max> items · <desc>`; with no `max` the ETA part is replaced
|
|
133
|
-
by `<ticks> items
|
|
131
|
+
by `<ticks> items`; both forms occur.
|
|
134
132
|
- `add_offset` / `remove_offset` / `offset` (util.rb:9-28) indent bars created
|
|
135
133
|
in nested threads.
|
|
136
134
|
- `file:` option: `save` writes the bar state to YAML (`:desc, :last_count,
|
|
137
135
|
:last_percent, :last_time, :max, :start, :ticks`) and `done`/`error` remove
|
|
138
136
|
the file, so a bar given a `file:` **resumes across runs** — the persisted
|
|
139
|
-
YAML is reloaded into `ticks` (
|
|
137
|
+
YAML is reloaded into `ticks` (round-trips 3 ticks).
|
|
140
138
|
|
|
141
139
|
All bar bookkeeping is guarded by `BAR_MUTEX`; per-bar `tick` state is not
|
|
142
140
|
synchronized and races by design.
|
|
@@ -147,7 +145,7 @@ synchronized and races by design.
|
|
|
147
145
|
`Hash`. In a plain `require 'scout-essentials'` process neither `TSV` nor
|
|
148
146
|
`Step` exists, so the `when TSV` arm raises `NameError`, the rescue turns it
|
|
149
147
|
into `nil`, and **`guess_obj_max` always returns `nil`** — i.e.
|
|
150
|
-
`get_obj_bar(list, true).max == nil`
|
|
148
|
+
`get_obj_bar(list, true).max == nil` . Pass an explicit Numeric or a
|
|
151
149
|
`:max` hash, or define those constants in the embedding repo, to get a real
|
|
152
150
|
max.
|
|
153
151
|
|
data/doc/user/RemoteData.md
CHANGED
|
@@ -5,9 +5,8 @@ Fetching remote files over HTTP(S)/FTP/SSH, the URL cache that backs
|
|
|
5
5
|
Source: `lib/scout/open/remote.rb`, `lib/scout/open/sync.rb`,
|
|
6
6
|
`lib/scout/resource/sync.rb`.
|
|
7
7
|
|
|
8
|
-
Executed examples
|
|
9
|
-
|
|
10
|
-
transcripts live in `tmp/rewrite_D/BATCH_D_PROBE_TRANSCRIPTS.txt`.
|
|
8
|
+
Executed examples run against a throwaway local `python3 -m http.server`
|
|
9
|
+
serving real URLs.
|
|
11
10
|
|
|
12
11
|
For the local side of `Open` see [WorkingWithFiles.md](WorkingWithFiles.md);
|
|
13
12
|
for `Resource` itself see [ProducingResources.md](ProducingResources.md).
|
|
@@ -25,8 +24,7 @@ Open.ssh?('http://a') # => false
|
|
|
25
24
|
```
|
|
26
25
|
|
|
27
26
|
Both are pure regexes (`/^(?:https?|ftp|ssh):\/\//` and `/^ssh:\/\//`). A
|
|
28
|
-
path without a scheme is never remote.
|
|
29
|
-
`tmp/rewrite_D/probe_07_remote.rb`.
|
|
27
|
+
path without a scheme is never remote.
|
|
30
28
|
|
|
31
29
|
## `Open.ssh` — reading over SSH
|
|
32
30
|
|
|
@@ -46,20 +44,19 @@ behaviour is to serve from that cache without touching the network.
|
|
|
46
44
|
|
|
47
45
|
```ruby
|
|
48
46
|
Open.remote_cache_dir # $HOME/.scout/var/cache/open-remote
|
|
49
|
-
Open.remote_cache_dir = dir # override (
|
|
47
|
+
Open.remote_cache_dir = dir # override (e.g. a tmpdir)
|
|
50
48
|
Open.cache_file(url, options) # <cache_dir>/<digest>
|
|
51
49
|
```
|
|
52
50
|
|
|
53
51
|
The digest (`Open.digest_url`) covers the URL, the `--post-data` value and
|
|
54
52
|
the **sorted lines** of `--post-file`. Different post-data therefore gets a
|
|
55
|
-
different cache entry (
|
|
56
|
-
paths).
|
|
53
|
+
different cache entry (three URLs/options => three distinct cache paths).
|
|
57
54
|
|
|
58
55
|
### No TTL, no expiry
|
|
59
56
|
|
|
60
57
|
There is no timestamp check and no maximum age. Once a URL is cached, a
|
|
61
58
|
later `Open.wget` of the same URL/options returns the cached bytes even if
|
|
62
|
-
the server has changed
|
|
59
|
+
the server has changed: the file can be edited on
|
|
63
60
|
the server three times (`HELLO` → `FIRST` → `SECOND` → `THIRD`) while the
|
|
64
61
|
second `Open.wget` still reported the old cached content.
|
|
65
62
|
|
|
@@ -72,7 +69,7 @@ Open.remove_from_cache(url, options) # delete the cache entry
|
|
|
72
69
|
```
|
|
73
70
|
|
|
74
71
|
**Exactly `'update'`** re-fetches and re-writes the cache, then *reads it
|
|
75
|
-
back
|
|
72
|
+
back*: after `Open.wget(url, :nocache => 'update')` the returned file
|
|
76
73
|
is the cache file again, not the raw pipe). Any other truthy `nocache`
|
|
77
74
|
value takes the other branch and returns the raw wget pipe **without**
|
|
78
75
|
touching the cache. `:force` only bypasses the initial `in_cache?` lookup,
|
|
@@ -90,7 +87,7 @@ wrapped in `OpenURLError` with the message
|
|
|
90
87
|
|
|
91
88
|
`Open.download(url, file)` runs `wget '<url>' -O '<file>'` via
|
|
92
89
|
`CMD.cmd_log`; on failure it removes the partial output file and re-raises
|
|
93
|
-
the original error
|
|
90
|
+
the original error: downloading a live URL writes the current
|
|
94
91
|
server bytes (no cache involved).
|
|
95
92
|
|
|
96
93
|
`Open.scp(source_file, target_file, target:, source:)` first creates the
|
|
@@ -123,7 +120,7 @@ directory itself. A local-to-local sync of the identical path logs a warning
|
|
|
123
120
|
and returns.
|
|
124
121
|
|
|
125
122
|
`Open.sync(...)` is a literal alias: `Open.sync` and `Open.rsync` are the
|
|
126
|
-
same implementation forwarded with Ruby's `...` (
|
|
123
|
+
same implementation forwarded with Ruby's `...` (same owner). Use
|
|
127
124
|
either name — they are interchangeable.
|
|
128
125
|
|
|
129
126
|
## `Resource.sync(path, map, options)`
|
|
@@ -143,7 +140,7 @@ Open.wait(lag, key = nil)
|
|
|
143
140
|
|
|
144
141
|
A tiny rate limiter backed by the module-level `LAST_TIME` hash. If the
|
|
145
142
|
last call *for that key* was less than `lag` seconds ago, it sleeps the
|
|
146
|
-
remainder. `Open.wget` uses it via `:nice` / `:nice_key`.
|
|
143
|
+
remainder. `Open.wget` uses it via `:nice` / `:nice_key`. Two
|
|
147
144
|
`Open.wait(0.3, :k)` calls in a row took 0.3 s wall clock.
|
|
148
145
|
|
|
149
146
|
Note the process-local `LAST_TIME` map is shared mutable state with no
|
data/doc/user/RunningCommands.md
CHANGED
|
@@ -26,7 +26,6 @@ CMD.cmd('echo', 'arg2', '-n' => true, '-r' => true).read
|
|
|
26
26
|
- **Hash-only** (`CMD.cmd({'echo' => 'x'})`) is *not* a command form; the Hash is
|
|
27
27
|
treated as options, `cmd`/`tool` stay nil and the call fails.
|
|
28
28
|
|
|
29
|
-
Verified: probe `tmp/rewrite_B/probe_15_cmd_forms.rb` (string, string+cmd,
|
|
30
29
|
array, array+string, symbol tool with nil cmd, ProcessFailed on hash-only).
|
|
31
30
|
|
|
32
31
|
### The `'{opt}'` placeholder
|
|
@@ -44,8 +43,6 @@ CMD.cmd("echo '{opt}' a 1", 'x' => 1).read # => "a 1\n" (note: trailing space)
|
|
|
44
43
|
|
|
45
44
|
Unquoted `{opt}` is left untouched — the substitution matches `'\{opt\}'`
|
|
46
45
|
exactly. Without a placeholder the option string is appended to the command.
|
|
47
|
-
Verified: probe `probe_14_cmd_opts.rb` lines 10-11 and the C8 fix in
|
|
48
|
-
`research/doc_audit/RunningCommands.md`.
|
|
49
46
|
|
|
50
47
|
## Option quoting: `process_cmd_options`
|
|
51
48
|
|
|
@@ -71,10 +68,10 @@ raises `Invalid option key` before anything runs.
|
|
|
71
68
|
**Arrays are not expanded.** An Array value is stringified (`to_s`) and quoted
|
|
72
69
|
like any other value — `"#{value}"` produces `n '["a", "b"]'` in String mode
|
|
73
70
|
and `['n', '["a", "b"]']` in array mode. Pass separate calls or build the
|
|
74
|
-
command yourself if you need repeated flags.
|
|
71
|
+
command yourself if you need repeated flags.
|
|
75
72
|
lines 4-5 and the cmd-level Array check.
|
|
76
73
|
|
|
77
|
-
|
|
74
|
+
(dashes, `=`, nil/false, true,
|
|
78
75
|
apostrophes, invalid key, arrays).
|
|
79
76
|
|
|
80
77
|
## stderr: severities, capture and files
|
|
@@ -83,7 +80,7 @@ apostrophes, invalid key, arrays).
|
|
|
83
80
|
(added by `cmd.rb:181`) is **`Log::DEBUG`** — stderr lines are only logged at
|
|
84
81
|
debug severity, i.e. invisible unless you raise `Log.severity` above it.
|
|
85
82
|
|
|
86
|
-
-
|
|
83
|
+
- :stderr => true is normalised to `Log::HIGH`.
|
|
87
84
|
- Any other Integer is a `Log` severity: the ladder is
|
|
88
85
|
`DEBUG=0, LOW=1, MEDIUM=2, HIGH=3, INFO=4, WARN=5, ERROR=6, NONE=7`
|
|
89
86
|
(`lib/scout/log.rb` `SEVERITY_NAMES`), so `:stderr => Log::MEDIUM` shows
|
|
@@ -118,9 +115,7 @@ CMD.cmd(..., :save_stderr => dst) # dst stays open
|
|
|
118
115
|
Implementation: `cmd.rb:194-217` (destination setup), the writer thread /
|
|
119
116
|
inline writer, and the `ensure` at `cmd.rb:600-612` that flushes and closes a
|
|
120
117
|
CMD-owned file. Verified by `test/scout/test_cmd_save_stderr.rb` (13 tests,
|
|
121
|
-
incl. live `tail -f` polling)
|
|
122
|
-
(boolean/String/Path/Pathname/IO/StringIO, pipe and non-pipe, truncation) and
|
|
123
|
-
`probe_16_exitstatus.rb` (`std_err` populated in both modes).
|
|
118
|
+
incl. live `tail -f` polling).
|
|
124
119
|
|
|
125
120
|
## Exit status, `no_fail` and failure
|
|
126
121
|
|
|
@@ -129,19 +124,19 @@ incl. live `tail -f` polling) and probes `probe_17_save_stderr.rb`
|
|
|
129
124
|
`:nofail`) is given.
|
|
130
125
|
- `:no_fail => true` **suppresses** `ProcessFailed`/`ConcurrentStreamProcessFailed`
|
|
131
126
|
— and `exit_status` then stays `nil`, in pipe mode too. If you need the code,
|
|
132
|
-
call `join_pids` yourself.
|
|
127
|
+
call `join_pids` yourself.
|
|
133
128
|
(`pipe read+join exit_status: nil`, `explicit join_pids exit_status: 0`,
|
|
134
129
|
`non-pipe exit_status: 0`).
|
|
135
130
|
- `exit_status` is only ever set by `ConcurrentStream#join_pids`
|
|
136
131
|
(`concurrent_stream.rb:127`), which also empties `pids`, so it can only be
|
|
137
132
|
used once. A stream that is read and joined normally has `exit_status == nil`
|
|
138
|
-
(
|
|
133
|
+
(`read+join: es=nil` with `joined?` true; only
|
|
139
134
|
an explicit early `join_pids` yields `0`). Do not rely on `stream.exit_status`.
|
|
140
135
|
- If the process never starts (bad executable, no such file) `ProcessFailed` is
|
|
141
136
|
raised immediately — also suppressed by `no_fail`, which then returns `nil`.
|
|
142
137
|
- A failed *producer thread* in pipe mode surfaces as
|
|
143
138
|
`ConcurrentStreamProcessFailed` when the consumer closes/joins the stream
|
|
144
|
-
|
|
139
|
+
|
|
145
140
|
|
|
146
141
|
## Timeout
|
|
147
142
|
|
|
@@ -182,7 +177,8 @@ returns only entries matching `/\d+\./`.
|
|
|
182
177
|
|
|
183
178
|
| key | effect |
|
|
184
179
|
|---|---|
|
|
185
|
-
| `:in` | stdin: a String is written by a thread; an IO/StringIO is read; a ConcurrentStream is streamed (and closed unless `:dont_close_in`). Also `:in_pipe` for a pipe-backed writer.
|
|
180
|
+
| `:in` | stdin: a String is written by a thread; an IO/StringIO is read; a ConcurrentStream is streamed (and closed unless `:dont_close_in`). Also `:in_pipe` for a pipe-backed writer. (String, IO, stream, `in_pipe` returns an IO)
|
|
181
|
+
. |
|
|
186
182
|
| `:pipe` | return a ConcurrentStream instead of the text/StringIO |
|
|
187
183
|
| `:stderr` | severity for stderr logging (default `Log::DEBUG`); `true` → `Log::HIGH` |
|
|
188
184
|
| `:save_stderr` | `true` / path / IO — see above |
|
|
@@ -191,7 +187,7 @@ returns only entries matching `/\d+\./`.
|
|
|
191
187
|
| `:no_wait` | alias used to default `autojoin` (`autojoin = no_wait if autojoin.nil?`) |
|
|
192
188
|
| `:timeout` | seconds; watchdog; only runtime bound |
|
|
193
189
|
| `:post` | proc run after the command/stream finishes (teardown, forcing upstream closes) |
|
|
194
|
-
| `:progress_bar` | a `Log::ProgressBar`; stderr lines tick it (
|
|
190
|
+
| `:progress_bar` | a `Log::ProgressBar`; stderr lines tick it (2 ticks for a 2-line stderr, pipe and non-pipe) |
|
|
195
191
|
| `:log` | defaults to `true` (`log = true if log.nil?`, cmd.rb:224): pipe-mode stderr lines are `Log.log`ged at the chosen `:stderr` severity. `:log => false` silences that logging. Not the same as `CMD.cmd_log` (a separate helper that forces `:pipe`/`:log`). |
|
|
196
192
|
| `:sudo`, `:xvfb` | prefix the command |
|
|
197
193
|
| `:dont_close_in` | keep the `:in` stream open |
|
|
@@ -201,13 +197,12 @@ returns only entries matching `/\d+\./`.
|
|
|
201
197
|
`CMD.cmd` in this repo — `cmd.rb` deletes none of them, so they would be
|
|
202
198
|
forwarded to `process_cmd_options` and end up as command-line text. `:canfail`
|
|
203
199
|
exists on `Persist` (`persist.rb:133`) and on `Resource` claims, not here.
|
|
204
|
-
Verified by grepping `lib/` for the four names
|
|
200
|
+
Verified by grepping `lib/` for the four names.
|
|
205
201
|
|
|
206
202
|
## The block is ignored
|
|
207
203
|
|
|
208
204
|
`CMD.cmd(...) { ... }` accepts a block but **never calls it** — the only block
|
|
209
|
-
invocation inside `cmd.rb` is in `CMD.tool`.
|
|
210
|
-
`tmp/docaudit/probe_hs_c15_block.rb`: in both pipe and non-pipe mode the block
|
|
205
|
+
invocation inside `cmd.rb` is in `CMD.tool`. In both pipe and non-pipe mode the block
|
|
211
206
|
body never runs, while `:post` and `stream.add_callback` do. Use
|
|
212
207
|
`:post => proc{}` or stream callbacks for post-join work.
|
|
213
208
|
|
|
@@ -48,12 +48,20 @@ Open.read('file.gz.bak')# raw gzip bytes — NOT detected
|
|
|
48
48
|
Open.gzip?('file.tgz') # => false
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
To decompress content that does not carry a recognised name, run
|
|
52
|
+
`Open.gunzip` yourself — it is `zcat` behind a `CMD.cmd` pipe:
|
|
52
53
|
|
|
53
54
|
```ruby
|
|
54
|
-
Open.
|
|
55
|
+
Open.read('file.tgz', :gzip => false, :noz => true) # raw bytes, if that is what you want
|
|
56
|
+
stream = Open.open('file.tgz', :noz => true) # raw gzip bytes as a stream
|
|
57
|
+
Open.read(Open.gunzip(stream)) # decompressed
|
|
55
58
|
```
|
|
56
59
|
|
|
60
|
+
Note that the historical `:gzip => true` option is **broken as a
|
|
61
|
+
decompression force**: it is forwarded to `CMD.cmd('zcat', …)` as a
|
|
62
|
+
command-line flag, the command fails, and — because `:no_fail` is set —
|
|
63
|
+
you get an empty string instead of an error.
|
|
64
|
+
|
|
57
65
|
## Grepping lines
|
|
58
66
|
|
|
59
67
|
`:grep` and `:invert_grep` select lines using the system `grep`. **With a
|
data/lib/scout/config.rb
CHANGED
|
@@ -79,10 +79,16 @@ module IndiferentHash
|
|
|
79
79
|
case key
|
|
80
80
|
when Symbol, Module
|
|
81
81
|
v = super(key)
|
|
82
|
-
|
|
82
|
+
vn = super(key.to_s)
|
|
83
|
+
v.nil? ? vn : v
|
|
83
84
|
when String
|
|
84
85
|
v = super(key)
|
|
85
|
-
|
|
86
|
+
vn = begin
|
|
87
|
+
super(key.to_sym)
|
|
88
|
+
rescue
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
v.nil? ? vn : v
|
|
86
92
|
else
|
|
87
93
|
super(key)
|
|
88
94
|
end
|
data/lib/scout/named_array.rb
CHANGED
data/lib/scout/open/final.rb
CHANGED
|
@@ -147,7 +147,7 @@ module Open
|
|
|
147
147
|
info_file = file + '.info'
|
|
148
148
|
if File.exist?(info_file) && defined?(Step)
|
|
149
149
|
done = Persist.memory([info_file, File.mtime(info_file)], persist: true ) do
|
|
150
|
-
|
|
150
|
+
Step.load_info(info_file)[:done]
|
|
151
151
|
end
|
|
152
152
|
return done if done
|
|
153
153
|
end
|
data/lib/scout/open/stream.rb
CHANGED
|
@@ -16,14 +16,14 @@ module Open
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def self.consume_stream(io, in_thread = false, into = nil, into_close = true, &block)
|
|
19
|
+
def self.consume_stream(io, in_thread = false, into = nil, into_close = true, clean = true, &block)
|
|
20
20
|
return if Path === io
|
|
21
21
|
return unless io.respond_to? :read
|
|
22
22
|
|
|
23
|
-
if io.respond_to? :closed? and io.closed?
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
23
|
+
#if io.respond_to? :closed? and io.closed?
|
|
24
|
+
# io.join if io.respond_to? :join
|
|
25
|
+
# return
|
|
26
|
+
#end
|
|
27
27
|
|
|
28
28
|
if in_thread
|
|
29
29
|
consumer_thread = Thread.new(Thread.current) do |parent|
|
|
@@ -54,10 +54,13 @@ module Open
|
|
|
54
54
|
|
|
55
55
|
into_close = false unless into.respond_to? :close
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
begin
|
|
58
|
+
while c = io.readpartial(BLOCK_SIZE)
|
|
59
|
+
into << c if into
|
|
60
|
+
last_c = c if c
|
|
61
|
+
break if io.closed?
|
|
62
|
+
end
|
|
63
|
+
rescue EOFError, IOError
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
io.join if io.respond_to? :join
|
|
@@ -72,7 +75,7 @@ module Open
|
|
|
72
75
|
Log.low "Consume stream Aborted #{Log.fingerprint io} into #{into_path || into}"
|
|
73
76
|
io.abort $! if io.respond_to? :abort
|
|
74
77
|
into.close if into.respond_to?(:closed?) && ! into.closed?
|
|
75
|
-
FileUtils.rm into_path if into_path and File.exist?(into_path)
|
|
78
|
+
FileUtils.rm into_path if into_path and clean and File.exist?(into_path)
|
|
76
79
|
rescue Exception
|
|
77
80
|
Thread.current["exception"] = true
|
|
78
81
|
Log.low "Consume stream Exception reading #{Log.fingerprint io} into #{into_path || into} - #{$!.message}"
|
|
@@ -80,9 +83,7 @@ module Open
|
|
|
80
83
|
io.abort exception if io.respond_to? :abort
|
|
81
84
|
into.close if into.respond_to?(:closed?) && ! into.closed?
|
|
82
85
|
into_path = into if into_path.nil? && String === into
|
|
83
|
-
if into_path and File.exist?(into_path)
|
|
84
|
-
FileUtils.rm into_path
|
|
85
|
-
end
|
|
86
|
+
FileUtils.rm into_path if into_path and clean and File.exist?(into_path)
|
|
86
87
|
raise exception
|
|
87
88
|
end
|
|
88
89
|
end
|
data/lib/scout/path/util.rb
CHANGED
|
@@ -42,7 +42,7 @@ module Path
|
|
|
42
42
|
|
|
43
43
|
post_fix = "--#{filename.length}--#{Misc.digest(filename)[0..4]}" + extension
|
|
44
44
|
|
|
45
|
-
filename = filename[0..(length - post_fix.length - 1)] << post_fix
|
|
45
|
+
filename = Annotation.purge(filename.to_s)[0..(length - post_fix.length - 1)] << post_fix
|
|
46
46
|
else
|
|
47
47
|
filename
|
|
48
48
|
end
|