scout-essentials 1.8.8 → 1.9.1
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/filesystem.rb +2 -2
- 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 +4 -0
- data/lib/scout/path/find.rb +3 -2
- data/lib/scout/persist.rb +14 -10
- 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/misc/test_filesystem.rb +10 -0
- 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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Logging and Progress
|
|
2
|
+
|
|
3
|
+
`Log` is the single reporting surface: a severity-gated logger that writes to
|
|
4
|
+
STDERR, plus a stack of progress bars that render in the same place.
|
|
5
|
+
`lib/scout/log.rb`, `log/color.rb`, `log/fingerprint.rb`, `log/progress.rb`,
|
|
6
|
+
`log/progress/util.rb` and `log/progress/report.rb` are the whole
|
|
7
|
+
implementation.
|
|
8
|
+
|
|
9
|
+
## Severity ladder
|
|
10
|
+
|
|
11
|
+
`Log::SEVERITY_NAMES` is `%w(DEBUG LOW MEDIUM HIGH INFO WARN ERROR NONE)` with
|
|
12
|
+
integer constants `DEBUG=0 .. NONE=7`. `INFO` sits in the middle (4), not at
|
|
13
|
+
the top: `LOW`/`MEDIUM`/`HIGH` are *more verbose than info*.
|
|
14
|
+
|
|
15
|
+
The severity is set at require time from `ENV['SCOUT_LOG']` first
|
|
16
|
+
(`lib/scout/log.rb:36-54`): the known names (`DEBUG`, `LOW`, `MEDIUM`, `HIGH`,
|
|
17
|
+
`WARN`, `ERROR`, `NONE`) are honoured, and anything else — including an
|
|
18
|
+
unrecognised value — falls back to `Log.default_severity`, which reads
|
|
19
|
+
`~/.scout/etc/log_severity` if that file exists and is `INFO` otherwise
|
|
20
|
+
(log.rb:23-34).
|
|
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
|
+
restoring the original value after the block.
|
|
25
|
+
|
|
26
|
+
Change it at runtime with `Log.severity = Log::DEBUG`, or scope a block with
|
|
27
|
+
`Log.with_severity(level){ ... }`, or use the top-level `sss(level)` /
|
|
28
|
+
`sss(level){ ... }` helper (log.rb:430).
|
|
29
|
+
|
|
30
|
+
## Where output goes, and how it is protected
|
|
31
|
+
|
|
32
|
+
- `Log.log_write` / `Log.log_puts` wrap every write in `Log::MUTEX`
|
|
33
|
+
(log.rb:141-163). If a logfile was installed, output goes there; otherwise
|
|
34
|
+
STDERR (IOError swallowed).
|
|
35
|
+
- **`Log.logfile(path)` sets the logfile; `Log.logfile` with no argument does
|
|
36
|
+
NOT read it back — it resets it to `nil`** (log.rb:110-113). There is no
|
|
37
|
+
reader; the accessor is only `Log.logfile=` (`attr_writer`).
|
|
38
|
+
- `Log::LAST` is one shared mutable `String` (starts as `"log"`, never frozen)
|
|
39
|
+
used as a protocol between `logn` and progress-bar printing so bars know how
|
|
40
|
+
many lines to move up and which kind of output came last
|
|
41
|
+
(`progress/report.rb` `print`/`report`). Probe out04.txt confirms
|
|
42
|
+
`class == String`, `frozen? == false`.
|
|
43
|
+
|
|
44
|
+
## Message forms
|
|
45
|
+
|
|
46
|
+
- `Log.debug "msg"` .. `Log.error "msg"` print when severity allows; the top
|
|
47
|
+
of the ladder is the `Log::NONE` constant, not a method.
|
|
48
|
+
- `Log.debug { "expensive " + build }` — **the block is lazy message
|
|
49
|
+
evaluation, not a timer**: it is only called if the level passes. Probe
|
|
50
|
+
(out05.txt): a counter incremented inside a block passed to `Log.debug`
|
|
51
|
+
stays at 0 under ERROR severity and reaches 1 under DEBUG severity.
|
|
52
|
+
- `Log.exception(e)` (log.rb:254) prints `BACKTRACE` lines derived from
|
|
53
|
+
`e.backtrace` after fingerprinting messages longer than 1000 chars; it
|
|
54
|
+
returns early when the message contains `NOLOG` and drops the backtrace when
|
|
55
|
+
it contains `NOSTACK`.
|
|
56
|
+
- `Log.stack(stack)` (log.rb:318) prints a magenta header then the stack
|
|
57
|
+
**reversed** (innermost frame first) unless `SCOUT_ORIGINAL_STACK=true`.
|
|
58
|
+
Probe (out05.txt): a caller list `a.rb:1, b.rb:2, c.rb:3` prints as
|
|
59
|
+
`c.rb:3`, `b.rb:2`, `a.rb:1`.
|
|
60
|
+
|
|
61
|
+
## Top-level debug helpers (defined on Object)
|
|
62
|
+
|
|
63
|
+
`ppp(msg)` (log.rb:358) prints a cyan `PRINT:` banner plus the caller line and
|
|
64
|
+
pretty-prints the message. `fff(obj)` (log.rb:374) logs the fingerprint at
|
|
65
|
+
DEBUG. `ddd/lll/mmm/iii/wwww/eee` inspect an object at the matching severity,
|
|
66
|
+
and the `f`-suffixed variants (`ddf`, `llf`, `mmf`, `iif`, `wwwf`, `eef`)
|
|
67
|
+
fingerprint it first. `sss(level[, &blk])` sets or scopes severity. `ccc`
|
|
68
|
+
(log.rb:439) enables `$scout_debug_log` around a block so nested `ccc` calls
|
|
69
|
+
print. Probe out05.txt confirms all five are top-level methods defined inside
|
|
70
|
+
`lib/scout/log.rb`.
|
|
71
|
+
|
|
72
|
+
## Colors
|
|
73
|
+
|
|
74
|
+
- `Log.color(:red, "text")` — symbol color name. Produces `"\e[31mtext\e[0m"`.
|
|
75
|
+
- Passing a *String* name does not resolve the color table; the string is used
|
|
76
|
+
as the literal prefix and an escape is appended (`"redred\e[0m"`). Use
|
|
77
|
+
symbols.
|
|
78
|
+
- `Log.uncolor(str)` strips ANSI codes. `Log.nocolor` is true when
|
|
79
|
+
`ENV['SCOUT_NOCOLOR'] == 'true'` (exact string), which also blanks the cursor helpers
|
|
80
|
+
`up_lines`/`down_lines`/`return_line`/`clear_line`.
|
|
81
|
+
|
|
82
|
+
## Fingerprints
|
|
83
|
+
|
|
84
|
+
`Log.fingerprint(obj)` (log/fingerprint.rb:18) builds a stable, truncated text
|
|
85
|
+
representation for logging big values. It dispatches: objects responding to
|
|
86
|
+
`fingerprint` are delegated to; `nil/true/false/Symbol` are rendered
|
|
87
|
+
literally; strings longer than `FP_MAX_STRING` (150) are truncated to a
|
|
88
|
+
digest-marked middle (`"xxx<...400 - 0d723...>xxx"`); arrays longer than 20
|
|
89
|
+
show first/middle/last elements; hashes longer than 10 fall back to
|
|
90
|
+
keys+values fingerprints; floats get 1/3/6 decimals depending on magnitude.
|
|
91
|
+
|
|
92
|
+
The signature is `fingerprint(obj)` — **one positional argument**; there are no
|
|
93
|
+
`max_length`/`sep` options (probe_05). Hash rendering separates pairs with a
|
|
94
|
+
space, not a comma: `Log.fingerprint({a: 1, b: 2, c: 3})` gives
|
|
95
|
+
`"{:a=>1 :b=>2 :c=>3}"`.
|
|
96
|
+
|
|
97
|
+
## Progress bars
|
|
98
|
+
|
|
99
|
+
`Log::ProgressBar` instances are created with the class helpers in
|
|
100
|
+
`progress/util.rb`:
|
|
101
|
+
|
|
102
|
+
- `Log::ProgressBar.new_bar(max, options = {})` — also accepts a plain Hash
|
|
103
|
+
alone (`new_bar(:max => 50)`); `cleanup_bars` runs first and `:depth`
|
|
104
|
+
defaults to the current bar-stack depth plus offset. `new_bar(true)` treats
|
|
105
|
+
`true` as "no max" (`max = nil if TrueClass === max`, progress.rb:38).
|
|
106
|
+
- `Log::ProgressBar.with_bar(max = nil, options = {})` (util.rb:85) — wraps a
|
|
107
|
+
block; `KeepBar` keeps the bar alive, any other exception marks it errored
|
|
108
|
+
and re-raises.
|
|
109
|
+
- `Log::ProgressBar.with_obj_bar(obj, bar = true, &block)` (util.rb:167) —
|
|
110
|
+
the *second* argument (not `obj`) picks the bar: a String is the
|
|
111
|
+
description, `true` calls `guess_obj_max(obj)` (which — see the caveat
|
|
112
|
+
below — returns `nil` in a bare scout-essentials process), a Numeric is the
|
|
113
|
+
explicit max, a Hash carries `:max` and other options, and an existing
|
|
114
|
+
`Log::ProgressBar` is reused
|
|
115
|
+
(util.rb:139-162). **The block receives only the bar**:
|
|
116
|
+
`with_obj_bar(list, 'Processing'){ |bar| ... }`.
|
|
117
|
+
- `Log.no_bar` / `Log.no_bar=` / `SCOUT_NO_PROGRESS=true` disable ticking
|
|
118
|
+
entirely (progress.rb:5-11). There is no `Log.bar` module method.
|
|
119
|
+
|
|
120
|
+
Instance API (`progress.rb`): `init`, `tick(step = 1)`, `pos(pos)`,
|
|
121
|
+
`process(elem)`, `percent`, and the `max/ticks/frequency/depth/desc/file/
|
|
122
|
+
bytes/process/callback/severity` accessors.
|
|
123
|
+
|
|
124
|
+
- `percent` returns 0 with no ticks, **100 when `max == 0`**, and integer
|
|
125
|
+
division otherwise.
|
|
126
|
+
- `tick` is a no-op when `Log.no_bar`; it reports when `diff >= frequency`
|
|
127
|
+
(default 2 s) or when the percent advanced and `diff > 0.3` — that ~0.3 s
|
|
128
|
+
throttle plus the frequency gate is the render throttling.
|
|
129
|
+
- `report(io = STDERR)` redraws the whole active-bar stack with cursor
|
|
130
|
+
movement. `report_msg` shows **elapsed time and rate** and, when `max` is
|
|
131
|
+
set, an ETA: `· <rate> per sec. -- <dots> <pct>% <eta> => <elapsed> -
|
|
132
|
+
<ticks> of <max> items · <desc>`; with no `max` the ETA part is replaced
|
|
133
|
+
by `<ticks> items` (probe_12 shows both forms).
|
|
134
|
+
- `add_offset` / `remove_offset` / `offset` (util.rb:9-28) indent bars created
|
|
135
|
+
in nested threads.
|
|
136
|
+
- `file:` option: `save` writes the bar state to YAML (`:desc, :last_count,
|
|
137
|
+
:last_percent, :last_time, :max, :start, :ticks`) and `done`/`error` remove
|
|
138
|
+
the file, so a bar given a `file:` **resumes across runs** — the persisted
|
|
139
|
+
YAML is reloaded into `ticks` (probe_06 round-trips 3 ticks).
|
|
140
|
+
|
|
141
|
+
All bar bookkeeping is guarded by `BAR_MUTEX`; per-bar `tick` state is not
|
|
142
|
+
synchronized and races by design.
|
|
143
|
+
|
|
144
|
+
### A scout-essentials-only caveat
|
|
145
|
+
|
|
146
|
+
`guess_obj_max` (util.rb:101-136) matches `TSV` and `Step` *before* `Array` and
|
|
147
|
+
`Hash`. In a plain `require 'scout-essentials'` process neither `TSV` nor
|
|
148
|
+
`Step` exists, so the `when TSV` arm raises `NameError`, the rescue turns it
|
|
149
|
+
into `nil`, and **`guess_obj_max` always returns `nil`** — i.e.
|
|
150
|
+
`get_obj_bar(list, true).max == nil` (probe_06). Pass an explicit Numeric or a
|
|
151
|
+
`:max` hash, or define those constants in the embedding repo, to get a real
|
|
152
|
+
max.
|
|
153
|
+
|
|
154
|
+
## Related
|
|
155
|
+
|
|
156
|
+
- [Cookbook](Cookbook.md) — progress bars inside real recipes.
|
|
157
|
+
- [Architecture](../developer/Architecture.md) — where `Log` sits and
|
|
158
|
+
cross-repo attribution.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Producing Resources
|
|
2
|
+
|
|
3
|
+
A **resource** is a logical file — a name like `data/config.yaml` — that a
|
|
4
|
+
software package can materialize on demand. This page explains the `claim`
|
|
5
|
+
syntax, how `produce` uses claims to write files, and the helpers around
|
|
6
|
+
them.
|
|
7
|
+
|
|
8
|
+
## Declaring a resource
|
|
9
|
+
|
|
10
|
+
Any module that `extend`s `Resource` becomes a resource package:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
require 'scout-essentials'
|
|
14
|
+
|
|
15
|
+
module MyApp
|
|
16
|
+
extend Resource
|
|
17
|
+
annotation :pkgdir
|
|
18
|
+
self.pkgdir = 'myapp'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
MyApp.claim MyApp.data.config, :string, "key=value\n"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Inside such a module the idiomatic form (note: no `self.` prefix on `claim`)
|
|
25
|
+
is:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
module MyApp
|
|
29
|
+
extend Resource
|
|
30
|
+
annotation :pkgdir
|
|
31
|
+
self.pkgdir = 'myapp'
|
|
32
|
+
|
|
33
|
+
claim data.file, :string, "content\n"
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
- `claim(path, type, content = nil, &block)` — `type` is a **mandatory
|
|
39
|
+
positional** argument (`claim path { }` raises `ArgumentError`). Passing
|
|
40
|
+
only the path plus a block is not a valid call.
|
|
41
|
+
- `type` is one of `:string`, `:url`, `:proc`, `:rake`, `:install`.
|
|
42
|
+
`:csv` is an unimplemented stub that raises `RuntimeError
|
|
43
|
+
"TSV/CSV Not implemented yet"` when produced. There is **no `:annotation`
|
|
44
|
+
claim type**.
|
|
45
|
+
- `path` is normally a `Path` built with `method_missing` segments
|
|
46
|
+
(`MyApp.data.config`); a plain `String` is also accepted.
|
|
47
|
+
|
|
48
|
+
## Claim types
|
|
49
|
+
|
|
50
|
+
| Type | `content` | Effect |
|
|
51
|
+
|------|-----------|--------|
|
|
52
|
+
| `:string` | String | written verbatim |
|
|
53
|
+
| `:url` | source path/URL | the source is opened (with `:noz` when it looks compressed) and copied to the final path |
|
|
54
|
+
| `:proc` | `Proc` (arity 0 or 1) | called with no args (arity 0) or the final path (arity 1); the result is dispatched (below) |
|
|
55
|
+
| `:rake` | Rakefile directory/task | `run_rake` builds the file (below) |
|
|
56
|
+
| `:install` | software spec hash | `Resource.install` installs software into `share/software` (below) |
|
|
57
|
+
|
|
58
|
+
`:proc` result dispatch — the returned value drives how the file is written:
|
|
59
|
+
|
|
60
|
+
- `String`, `IO`, `StringIO` → `Open.sensible_write(final_path, data)`
|
|
61
|
+
- `Array` → elements joined with `"\n"` then written
|
|
62
|
+
- `TSV` / `TSV::Dumper` → the dumper stream is written (requires
|
|
63
|
+
`rbbt-util`/scout-gear; **a `nil` return trips the `when TSV` branch
|
|
64
|
+
first** and raises `NameError: uninitialized constant Resource::TSV`, so
|
|
65
|
+
procs that may return nil must guard themselves)
|
|
66
|
+
- `nil` → nothing is written (file left missing) — but see the caveat above
|
|
67
|
+
- anything else → `RuntimeError "Unkown object produced: ..."`
|
|
68
|
+
|
|
69
|
+
## Producing
|
|
70
|
+
|
|
71
|
+
`Path#produce` (in `lib/scout/resource/path.rb`) is the entry point:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
MyApp.data.config.produce # returns the logical path (a Path)
|
|
75
|
+
MyApp.data.config.find # => "$HOME/.myapp/data/config" (now existing)
|
|
76
|
+
Open.read(MyApp.data.config.find) # => "key=value\n"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- It never returns nil: on `ResourceNotFound` (nothing claims the path) it
|
|
80
|
+
stores `false` and returns it, and that decision is **latched** in
|
|
81
|
+
`@produced` — a later call does not retry unless you pass `force`.
|
|
82
|
+
- A raised `Exception` stored in `@produced` is re-raised on the next call.
|
|
83
|
+
- On a successful produce the target is written under a **produce lock**
|
|
84
|
+
(`TmpFile.tmp_for_file(final_path, dir: lock_dir)` + `Open.lock`).
|
|
85
|
+
- If nothing claims the plain path but a `.gz` or `.bgz` variant is claimed,
|
|
86
|
+
production falls through to those extensions. The fall-through is
|
|
87
|
+
asymmetric: producing `x.txt` may materialize `x.txt.gz`, but asking for
|
|
88
|
+
`x.gz` when only `x.txt` is claimed fails.
|
|
89
|
+
- `force: true` (or `produce(true)`) removes the existing file and reruns.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
MyApp.data.config.produce(true) # force re-production
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Extension-aware helpers
|
|
96
|
+
|
|
97
|
+
- `produce_and_find(extension = nil, *args)` — produce (falling back to the
|
|
98
|
+
given extension with `produce_with_extension`) and return the located
|
|
99
|
+
path; raises `RuntimeError "Not found"` if the result is still missing.
|
|
100
|
+
- `produce_with_extension(extension, *args)` — try the plain path first, then
|
|
101
|
+
the variant with `extension` appended; raise the *first* exception if both
|
|
102
|
+
fail.
|
|
103
|
+
- `find_with_extension(ext, *args, produce: true)` — `find` first; if the
|
|
104
|
+
plain result does not exist, look for `self.set_extension(ext)`.
|
|
105
|
+
`produce:` defaults to **true**, so it may trigger a produce.
|
|
106
|
+
- `exists?(produce: true)` — default **produces** before answering; pass
|
|
107
|
+
`false` for a pure existence check.
|
|
108
|
+
|
|
109
|
+
### Produce-aware `Path` I/O
|
|
110
|
+
|
|
111
|
+
`Path#read` and `Path#open` call `produce` first; `Path#list` is
|
|
112
|
+
`produce_and_find('list')` then `Open.list`. `Path#write` does **not**
|
|
113
|
+
produce — it writes straight to `self.find` (use it to place a manual file
|
|
114
|
+
under the resource tree):
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
MyApp.data.manual.write("manual\n")
|
|
118
|
+
# => writes to the found location, no claim consulted
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Rake claims
|
|
122
|
+
|
|
123
|
+
A rake claim points at a directory containing a `Rakefile`:
|
|
124
|
+
|
|
125
|
+
- `rake_dirs` lists candidate directories; `has_rake?(path)` is true when a
|
|
126
|
+
rake prefix matches, and `rake_for(path)` returns the longest matching
|
|
127
|
+
prefix (an empty-string prefix matches everything).
|
|
128
|
+
- Production runs the task through `ScoutRake` (forked execution). If rake
|
|
129
|
+
reports `Don't know how to build task`, the walk-up retry joins the task
|
|
130
|
+
name with the parent directory and retries; if that also fails, the error
|
|
131
|
+
is converted to `ResourceNotFound`.
|
|
132
|
+
- A `Path` whose pkgdir is the resource module is produced with
|
|
133
|
+
`pkgdir.produce(self)`; `Path#produce` on a String path returns `false`.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# Rakefile in ./ with: file "data/built.txt" do |t| Open.write(t.name, "x") end
|
|
137
|
+
MyApp.data.built.txt.produce
|
|
138
|
+
Open.read(MyApp.data.built.txt.find) # => "x"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Software installs
|
|
142
|
+
|
|
143
|
+
`:install` claims (and `Resource.install(name, software_dir, options)`)
|
|
144
|
+
install software binaries under `share/software/<name>` by sourcing
|
|
145
|
+
`share/software/install_helpers` and running the recipe from the spec hash
|
|
146
|
+
(`:git`, `:src`, `:url`, `:jar`, `:extra`, `:configure`, `:make`, ...).
|
|
147
|
+
`Resource.set_software_env(software_dir)` runs at load time on the default
|
|
148
|
+
`software` dir to expose installed binaries on `PATH`/`JAVA_CLASSPATH`-style
|
|
149
|
+
variables.
|
|
150
|
+
|
|
151
|
+
## `Resource.sync`
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
Resource.sync(path, map = nil, options = {})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A module-level method (there is **no** `Path#sync`) that walks the map
|
|
158
|
+
order copying/symlinking resource files found in other locations into the
|
|
159
|
+
map's directory, so that unlocated paths resolve locally. It is unrelated to
|
|
160
|
+
`Open.rsync`.
|
|
161
|
+
|
|
162
|
+
## Scout itself is a Resource
|
|
163
|
+
|
|
164
|
+
`Scout` extends `Resource` with `pkgdir 'scout'` and is
|
|
165
|
+
`Resource.default_resource`. `Scout.etc`, `Scout.tmp`, ... are ordinary
|
|
166
|
+
`Path` segment builders over the `Scout` package:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
Resource.default_resource # => Scout
|
|
170
|
+
Scout.etc.find # => "$HOME/.scout/etc"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Related
|
|
174
|
+
|
|
175
|
+
- [Path Resolution](../developer/PathResolution.md) for `find`/`follow`.
|
|
176
|
+
- [Persistence and Resources](../developer/PersistenceAndResources.md) for the
|
|
177
|
+
produce lock namespace and claim semantics from a developer angle.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Remote Data
|
|
2
|
+
|
|
3
|
+
Fetching remote files over HTTP(S)/FTP/SSH, the URL cache that backs
|
|
4
|
+
`Open.wget`, and the rsync helpers used to move directories between hosts.
|
|
5
|
+
Source: `lib/scout/open/remote.rb`, `lib/scout/open/sync.rb`,
|
|
6
|
+
`lib/scout/resource/sync.rb`.
|
|
7
|
+
|
|
8
|
+
Executed examples come from `tmp/rewrite_D/probe_07_remote.rb`, which
|
|
9
|
+
starts a throwaway local `python3 -m http.server` to serve real URLs;
|
|
10
|
+
transcripts live in `tmp/rewrite_D/BATCH_D_PROBE_TRANSCRIPTS.txt`.
|
|
11
|
+
|
|
12
|
+
For the local side of `Open` see [WorkingWithFiles.md](WorkingWithFiles.md);
|
|
13
|
+
for `Resource` itself see [ProducingResources.md](ProducingResources.md).
|
|
14
|
+
|
|
15
|
+
## Recognising remote paths
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
Open.remote?('http://a') # => true
|
|
19
|
+
Open.remote?('https://a') # => true
|
|
20
|
+
Open.remote?('ssh://a:x') # => true
|
|
21
|
+
Open.remote?('ftp://a') # => true
|
|
22
|
+
Open.remote?('/local') # => false
|
|
23
|
+
Open.ssh?('ssh://a:x') # => true
|
|
24
|
+
Open.ssh?('http://a') # => false
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Both are pure regexes (`/^(?:https?|ftp|ssh):\/\//` and `/^ssh:\/\//`). A
|
|
28
|
+
path without a scheme is never remote. Verified by
|
|
29
|
+
`tmp/rewrite_D/probe_07_remote.rb`.
|
|
30
|
+
|
|
31
|
+
## `Open.ssh` — reading over SSH
|
|
32
|
+
|
|
33
|
+
`Open.ssh('ssh://server:path')` parses server and path from the URI. When
|
|
34
|
+
the server is exactly `localhost` it bypasses SSH entirely and calls
|
|
35
|
+
`Open.open(file)` on the bare path — handy for tests. Otherwise it returns a
|
|
36
|
+
pipe from `CMD.cmd("ssh '<server>' cat '<file>'", :pipe => true, :autojoin
|
|
37
|
+
=> true)`, i.e. a `ConcurrentStream`, not a String.
|
|
38
|
+
|
|
39
|
+
## `Open.wget` and the URL cache
|
|
40
|
+
|
|
41
|
+
`Open.wget(url, options)` is the main entry point for HTTP/FTP. It has a
|
|
42
|
+
**permanent file cache** keyed by a digest of the request, and the default
|
|
43
|
+
behaviour is to serve from that cache without touching the network.
|
|
44
|
+
|
|
45
|
+
### Cache location and key
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
Open.remote_cache_dir # $HOME/.scout/var/cache/open-remote
|
|
49
|
+
Open.remote_cache_dir = dir # override (probe sets it to a tmpdir)
|
|
50
|
+
Open.cache_file(url, options) # <cache_dir>/<digest>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The digest (`Open.digest_url`) covers the URL, the `--post-data` value and
|
|
54
|
+
the **sorted lines** of `--post-file`. Different post-data therefore gets a
|
|
55
|
+
different cache entry (probe: three URLs/options => three distinct cache
|
|
56
|
+
paths).
|
|
57
|
+
|
|
58
|
+
### No TTL, no expiry
|
|
59
|
+
|
|
60
|
+
There is no timestamp check and no maximum age. Once a URL is cached, a
|
|
61
|
+
later `Open.wget` of the same URL/options returns the cached bytes even if
|
|
62
|
+
the server has changed. The probe demonstrates it: the file was edited on
|
|
63
|
+
the server three times (`HELLO` → `FIRST` → `SECOND` → `THIRD`) while the
|
|
64
|
+
second `Open.wget` still reported the old cached content.
|
|
65
|
+
|
|
66
|
+
### Forcing a refresh
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
Open.wget(url, :nocache => 'update') # re-fetch, write cache, read cache
|
|
70
|
+
Open.wget(url, :force => true) # skip the cache check entirely
|
|
71
|
+
Open.remove_from_cache(url, options) # delete the cache entry
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Exactly `'update'`** re-fetches and re-writes the cache, then *reads it
|
|
75
|
+
back* (probe: after `Open.wget(url, :nocache => 'update')` the returned file
|
|
76
|
+
is the cache file again, not the raw pipe). Any other truthy `nocache`
|
|
77
|
+
value takes the other branch and returns the raw wget pipe **without**
|
|
78
|
+
touching the cache. `:force` only bypasses the initial `in_cache?` lookup,
|
|
79
|
+
so a `:force` fetch without `nocache` still ends by writing and re-opening
|
|
80
|
+
the cache.
|
|
81
|
+
|
|
82
|
+
Other `wget` passthroughs: `--user-agent=` (defaults to `rbbt`),
|
|
83
|
+
`:post` → `--post-data=`, `:cookies => file` → `--save-cookies`,
|
|
84
|
+
`--load-cookies`, `--keep-session-cookies`, plus any literal wget flag.
|
|
85
|
+
`:nice` / `:nice_key` route through `Open.wait` (below). Network errors are
|
|
86
|
+
wrapped in `OpenURLError` with the message
|
|
87
|
+
`Error reading remote url: <url>. <cause>`.
|
|
88
|
+
|
|
89
|
+
## `Open.download` and `Open.scp`
|
|
90
|
+
|
|
91
|
+
`Open.download(url, file)` runs `wget '<url>' -O '<file>'` via
|
|
92
|
+
`CMD.cmd_log`; on failure it removes the partial output file and re-raises
|
|
93
|
+
the original error. Verified: downloading a live URL produced the current
|
|
94
|
+
server bytes (no cache involved).
|
|
95
|
+
|
|
96
|
+
`Open.scp(source_file, target_file, target:, source:)` first creates the
|
|
97
|
+
remote parent directory with `ssh <target> mkdir -p ...`, then runs
|
|
98
|
+
`scp -r`. Both keyword arguments are optional and only prefix the paths with
|
|
99
|
+
`host:` when they are not already prefixed.
|
|
100
|
+
|
|
101
|
+
## `Open.rsync` and `Open.sync`
|
|
102
|
+
|
|
103
|
+
`Open.rsync(source, target, options)` shells out to
|
|
104
|
+
`rsync -avztHP --copy-unsafe-links --omit-dir-times` with:
|
|
105
|
+
|
|
106
|
+
- `:excludes` — defaults to `%w(.save .crap .source tmp filecache
|
|
107
|
+
open-remote)`; a String is split on commas unless it already contains
|
|
108
|
+
`--exclude`.
|
|
109
|
+
- `:files` — a list of relative filenames written to a temp file and passed
|
|
110
|
+
as `--files-from=`.
|
|
111
|
+
- `:hard_link` — adds `--link-dest '<source>'` (local sources only).
|
|
112
|
+
- `:test` — adds `-nv` (dry run).
|
|
113
|
+
- `:delete` — appends `&& rm -Rf <source>` when no `:files`, or removes the
|
|
114
|
+
listed files and empties their directories afterwards.
|
|
115
|
+
- `:source` / `:target` — turn one side into `server:'path'` URIs, creating
|
|
116
|
+
the remote directory with `ssh <server> mkdir -p` when needed.
|
|
117
|
+
- `:print => true` — return the command string instead of running it.
|
|
118
|
+
- `:other` — a String or Array appended verbatim.
|
|
119
|
+
|
|
120
|
+
Trailing-slash semantics follow rsync: if either side is a directory (or ends
|
|
121
|
+
in `/`), both get a trailing `/`, which copies *contents* rather than the
|
|
122
|
+
directory itself. A local-to-local sync of the identical path logs a warning
|
|
123
|
+
and returns.
|
|
124
|
+
|
|
125
|
+
`Open.sync(...)` is a literal alias: `Open.sync` and `Open.rsync` are the
|
|
126
|
+
same implementation forwarded with Ruby's `...` (probe: same owner). Use
|
|
127
|
+
either name — they are interchangeable.
|
|
128
|
+
|
|
129
|
+
## `Resource.sync(path, map, options)`
|
|
130
|
+
|
|
131
|
+
`Resource.sync` copies a `Resource` path into a mapped location:
|
|
132
|
+
`resource.identify(path).find(map)` is the target, and each existing file or
|
|
133
|
+
globbed match is synced with `Open.sync(source, target, options)`. The
|
|
134
|
+
resource is resolved from `:resource`, from the path's `pkgdir` when it is
|
|
135
|
+
already a `Path` over a `Resource`, or from `Resource.default_resource`.
|
|
136
|
+
`map` defaults to `'user'`. All `Open.rsync` options pass through.
|
|
137
|
+
|
|
138
|
+
## `Open.wait` — client-side rate limiting
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
Open.wait(lag, key = nil)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
A tiny rate limiter backed by the module-level `LAST_TIME` hash. If the
|
|
145
|
+
last call *for that key* was less than `lag` seconds ago, it sleeps the
|
|
146
|
+
remainder. `Open.wget` uses it via `:nice` / `:nice_key`. Probe: two
|
|
147
|
+
`Open.wait(0.3, :k)` calls in a row took 0.3 s wall clock.
|
|
148
|
+
|
|
149
|
+
Note the process-local `LAST_TIME` map is shared mutable state with no
|
|
150
|
+
mutex; see [LockingAndConcurrency.md](../developer/LockingAndConcurrency.md).
|
|
151
|
+
|
|
152
|
+
## Where this is used
|
|
153
|
+
|
|
154
|
+
- [CachingResults.md](CachingResults.md) — the other caching layers
|
|
155
|
+
(`Persist`) and how they differ from this URL cache.
|
|
156
|
+
- [WorkingWithFiles.md](WorkingWithFiles.md) — `Open.open` dispatching to
|
|
157
|
+
`wget`/`ssh` when a path looks remote.
|