wslc-wip 0.4.1 → 0.4.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/README.md +44 -2
- data/lib/wip/cli.rb +20 -9
- data/lib/wip/command_display.rb +19 -0
- data/lib/wip/command_runner.rb +4 -3
- data/lib/wip/debug_reporter.rb +59 -0
- data/lib/wip/resource_monitor.rb +94 -0
- data/lib/wip/version.rb +1 -1
- data/lib/wip.rb +3 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be995c6781c440a7fd84dd3c3bbbddd6799154b59694edce5aa653d80c7d6e53
|
|
4
|
+
data.tar.gz: 7fb1638a125b2bce92212e8ff525a38e31e2d8aa4386e3e74e379420d4df9c91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 539721780baa7f366f215581629a761c85e396823e05ff08a3da97c2d0de7808d1470c959240867a5a0ab762f4441ea8b2cb22c92f0063712a18c6b0f80dc4fe
|
|
7
|
+
data.tar.gz: fc00ceceeeb01cd63ac931ff2ffeea1f64848f38a1222b60b8c5de7a501e914a3820d61abbd7d437ebd412e2046958223edab7b46d582b5cb6ef693badf5cc3e
|
data/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](wslc-wip.gemspec)
|
|
7
7
|
|
|
8
|
+
Homepage: https://wslc-wip.slidict.com/
|
|
9
|
+
|
|
8
10
|
`wip` is a Ruby-built OSS CLI wrapper that brings a [`dip`](https://github.com/bibendi/dip)-like
|
|
9
11
|
workflow to Microsoft WSLC. It collects a project's container, image, environment variables, and
|
|
10
12
|
commands into a single `wip.yml`, and forwards them to `wslc.exe` / `wslc` as safe argument arrays
|
|
@@ -127,8 +129,48 @@ dependencies down (the network itself is left in place). Each dependency entry a
|
|
|
127
129
|
| `wip NAME ARGS...` | Run `commands.NAME`, appending any extra arguments |
|
|
128
130
|
|
|
129
131
|
TTY allocation is decided by combining the command's config, the CLI option, and whether both
|
|
130
|
-
stdin and stdout are real TTYs.
|
|
131
|
-
|
|
132
|
+
stdin and stdout are real TTYs.
|
|
133
|
+
|
|
134
|
+
Pass `--debug` (or set `WIP_DEBUG=1`) to see where time is going: wip prints each step it takes —
|
|
135
|
+
checking for an existing network/container/dependency, and running the resolved `wslc`/`docker`
|
|
136
|
+
command — along with how long that step took, e.g.:
|
|
137
|
+
|
|
138
|
+
```console
|
|
139
|
+
$ wip rails c --debug
|
|
140
|
+
wip: [debug] running: wslc.exe exec -it -w /app app bin/rails c
|
|
141
|
+
+ wslc.exe exec -it -w /app app bin/rails c
|
|
142
|
+
...
|
|
143
|
+
wip: [debug] done in 4.32s: running: wslc.exe exec -it -w /app app bin/rails c
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For long-running interactive commands (like `rails c`), the "done" line only prints after you
|
|
147
|
+
exit, but the timestamp of the `+ ...` line tells you when wip finished its own setup and handed
|
|
148
|
+
off to `wslc`/`docker` — useful for telling wip-side overhead apart from time spent booting inside
|
|
149
|
+
the container.
|
|
150
|
+
|
|
151
|
+
While a step is still running, wip also prints a host resource snapshot (load average, memory,
|
|
152
|
+
disk I/O, and the top CPU-consuming processes) every 5 seconds, so a hang is visible even before
|
|
153
|
+
the command has produced any output of its own:
|
|
154
|
+
|
|
155
|
+
```console
|
|
156
|
+
wip: [debug] still running (load 3.42 2.10 1.05 | mem 6.1G/15.6G | io read 12000KB/s write 400KB/s | top: wslc.exe(8842) cpu 61.0%/mem 3.2%, ...): running: wslc.exe exec -it -w /app app bin/rails c
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The disk I/O figure is worth watching first if the host's CPU and memory look idle — a slow
|
|
160
|
+
`bundle`/`rails` boot is often WSL2's bind-mounted (`.:/app`-style) volumes doing a lot of small
|
|
161
|
+
reads, not the container starving for CPU.
|
|
162
|
+
|
|
163
|
+
For commands that hand the real terminal to the child (`-it`, e.g. `rails c`), these periodic
|
|
164
|
+
snapshots go to a log file instead of your terminal — wip prints the path once at the start —
|
|
165
|
+
since writing into a terminal the child controls in raw mode would garble both outputs. Commands
|
|
166
|
+
that don't need a TTY still get the snapshots printed live.
|
|
167
|
+
|
|
168
|
+
Override that choice with `--debug-log`:
|
|
169
|
+
|
|
170
|
+
- `--debug-log=-` forces snapshots inline even for `-it` commands (only useful if you know your
|
|
171
|
+
terminal/pager can tolerate the interleaving).
|
|
172
|
+
- `--debug-log=PATH` always writes snapshots to `PATH`, including for non-TTY commands, e.g. to
|
|
173
|
+
keep every run's snapshots in one place: `wip rails c --debug --debug-log=/tmp/wip-debug.log`.
|
|
132
174
|
|
|
133
175
|
## doctor
|
|
134
176
|
|
data/lib/wip/cli.rb
CHANGED
|
@@ -9,6 +9,8 @@ module Wip
|
|
|
9
9
|
# Thor-based command-line interface for wip.
|
|
10
10
|
class CLI < Thor
|
|
11
11
|
class_option :config, type: :string, desc: 'Path to wip.yml'
|
|
12
|
+
class_option :debug, type: :boolean, default: false, desc: 'Print progress and timing for each step'
|
|
13
|
+
class_option :debug_log, type: :string, desc: 'Where --debug snapshots go: a file path, or "-" for inline'
|
|
12
14
|
default_task :dispatch
|
|
13
15
|
|
|
14
16
|
def self.exit_on_failure? = true
|
|
@@ -119,19 +121,31 @@ module Wip
|
|
|
119
121
|
def builder = CommandBuilder.new(wslc: resolver.resolve(load_config.wslc_command), config: load_config)
|
|
120
122
|
|
|
121
123
|
def execute(command, interactive: false, exit_on_failure: true)
|
|
122
|
-
|
|
124
|
+
runner = CommandRunner.new(debug: debug?)
|
|
125
|
+
code = reporter.step("running: #{CommandDisplay.for_debug(command)}", live: !interactive) do
|
|
126
|
+
runner.run(command, interactive: interactive)
|
|
127
|
+
end
|
|
123
128
|
exit(code) if exit_on_failure && !code.zero?
|
|
124
129
|
code
|
|
125
130
|
end
|
|
126
131
|
|
|
127
132
|
def resource_exists?(find_command)
|
|
128
|
-
|
|
129
|
-
code
|
|
130
|
-
code.zero? && !JSON.parse(out.string).empty?
|
|
133
|
+
code, output = probe(find_command)
|
|
134
|
+
code.zero? && !JSON.parse(output).empty?
|
|
131
135
|
rescue JSON::ParserError
|
|
132
136
|
false
|
|
133
137
|
end
|
|
134
138
|
|
|
139
|
+
def probe(command)
|
|
140
|
+
out = StringIO.new
|
|
141
|
+
runner = CommandRunner.new(stdout: out, stderr: StringIO.new, debug: debug?)
|
|
142
|
+
code = reporter.step("checking: #{CommandDisplay.for_debug(command)}") { runner.run(command) }
|
|
143
|
+
[code, out.string]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def debug? = options[:debug] || !ENV['WIP_DEBUG'].to_s.empty?
|
|
147
|
+
def reporter = @reporter ||= DebugReporter.new(enabled: debug?, log: options[:debug_log])
|
|
148
|
+
|
|
135
149
|
def ensure_network
|
|
136
150
|
network = load_config.network
|
|
137
151
|
return unless network
|
|
@@ -142,11 +156,8 @@ module Wip
|
|
|
142
156
|
end
|
|
143
157
|
|
|
144
158
|
def network_exists?(network)
|
|
145
|
-
|
|
146
|
-
code
|
|
147
|
-
return false unless code.zero?
|
|
148
|
-
|
|
149
|
-
JSON.parse(out.string).any? { |entry| entry['Name'] == network }
|
|
159
|
+
code, output = probe(builder.network_list)
|
|
160
|
+
code.zero? && JSON.parse(output).any? { |entry| entry['Name'] == network }
|
|
150
161
|
rescue JSON::ParserError
|
|
151
162
|
false
|
|
152
163
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module Wip
|
|
6
|
+
# Renders a command array for debug output, masking `-e KEY=value` env values
|
|
7
|
+
# so secrets from wip.yml never reach logs.
|
|
8
|
+
module CommandDisplay
|
|
9
|
+
def self.for_debug(command)
|
|
10
|
+
masked = command.each_cons(2).with_index.each_with_object(command.dup) do |((flag, pair), index), result|
|
|
11
|
+
next unless flag == '-e'
|
|
12
|
+
|
|
13
|
+
key, = pair.split('=', 2)
|
|
14
|
+
result[index + 1] = "#{key}=***"
|
|
15
|
+
end
|
|
16
|
+
Shellwords.join(masked)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/wip/command_runner.rb
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
|
-
require 'shellwords'
|
|
5
4
|
|
|
6
5
|
module Wip
|
|
7
6
|
# Executes a built command, pumping its I/O and returning the exit status.
|
|
8
7
|
class CommandRunner
|
|
9
|
-
def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr, interpreter: ErrorInterpreter.new
|
|
8
|
+
def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr, interpreter: ErrorInterpreter.new,
|
|
9
|
+
debug: !ENV['WIP_DEBUG'].to_s.empty?)
|
|
10
10
|
@stdin = stdin
|
|
11
11
|
@stdout = stdout
|
|
12
12
|
@stderr = stderr
|
|
13
13
|
@interpreter = interpreter
|
|
14
|
+
@debug = debug
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def run(command, env: {}, interactive: false)
|
|
17
|
-
@stderr.puts "+ #{
|
|
18
|
+
@stderr.puts "+ #{CommandDisplay.for_debug(command)}" if @debug
|
|
18
19
|
return run_attached(command, env) if interactive
|
|
19
20
|
|
|
20
21
|
captured = +''
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
|
|
5
|
+
module Wip
|
|
6
|
+
# Prints each step wip takes and how long it took, when debug mode is enabled.
|
|
7
|
+
class DebugReporter
|
|
8
|
+
# `log:` overrides where resource snapshots go, regardless of `live`:
|
|
9
|
+
# nil - automatic (see `step`'s `live:`)
|
|
10
|
+
# '-' - always print inline, even for interactive steps
|
|
11
|
+
# path - always write to this file, even for non-interactive steps
|
|
12
|
+
def initialize(enabled:, out: $stderr, log: nil)
|
|
13
|
+
@enabled = enabled
|
|
14
|
+
@out = out
|
|
15
|
+
@log = log
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# `live: false` is for steps that hand the real TTY to the child process
|
|
19
|
+
# (e.g. `wslc exec -it`). The child owns raw-mode cursor control there, so
|
|
20
|
+
# writing periodic snapshots straight into that same terminal races with
|
|
21
|
+
# it and garbles the output; those snapshots go to a log file instead,
|
|
22
|
+
# unless overridden by `log:` in the constructor.
|
|
23
|
+
def step(label, live: true)
|
|
24
|
+
return yield unless @enabled
|
|
25
|
+
|
|
26
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
27
|
+
@out.puts "wip: [debug] #{label}"
|
|
28
|
+
file = log_file(live)
|
|
29
|
+
monitor = ResourceMonitor.new(out: file || @out)
|
|
30
|
+
monitor.start(label)
|
|
31
|
+
begin
|
|
32
|
+
yield
|
|
33
|
+
ensure
|
|
34
|
+
monitor.stop
|
|
35
|
+
file&.close
|
|
36
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
37
|
+
@out.puts format('wip: [debug] done in %<elapsed>.2fs: %<label>s', elapsed: elapsed, label: label)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def log_file(live)
|
|
44
|
+
return nil if @log == '-'
|
|
45
|
+
return open_log(@log, "streaming resource snapshots to #{@log}") if @log
|
|
46
|
+
return nil if live
|
|
47
|
+
|
|
48
|
+
path = File.join(Dir.tmpdir, "wip-debug-#{Process.pid}-#{Time.now.to_i}.log")
|
|
49
|
+
open_log(path, "command owns the terminal; streaming resource snapshots to #{path}")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def open_log(path, notice)
|
|
53
|
+
@out.puts "wip: [debug] #{notice}"
|
|
54
|
+
file = File.open(path, 'a') # rubocop:disable Style/FileOpen -- closed by `step`'s ensure block
|
|
55
|
+
file.sync = true
|
|
56
|
+
file
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wip
|
|
4
|
+
# Periodically prints host CPU/memory/disk-IO/process info while a debug step
|
|
5
|
+
# is still running, so a hung or slow step is visible even before it produces
|
|
6
|
+
# output of its own.
|
|
7
|
+
class ResourceMonitor
|
|
8
|
+
def initialize(interval: 5, out: $stderr)
|
|
9
|
+
@interval = interval
|
|
10
|
+
@out = out
|
|
11
|
+
@last_disk = disk_sectors
|
|
12
|
+
@last_sampled_at = monotonic
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def start(label)
|
|
16
|
+
@thread = Thread.new do
|
|
17
|
+
loop do
|
|
18
|
+
sleep @interval
|
|
19
|
+
@out.puts "wip: [debug] still running (#{snapshot}): #{label}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop
|
|
25
|
+
@thread&.kill
|
|
26
|
+
@thread&.join
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
32
|
+
|
|
33
|
+
def snapshot
|
|
34
|
+
[load_average, memory, disk_io, top_processes].join(' | ')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_average
|
|
38
|
+
"load #{File.read('/proc/loadavg').split[0..2].join(' ')}"
|
|
39
|
+
rescue Errno::ENOENT, IOError
|
|
40
|
+
'load n/a'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def memory
|
|
44
|
+
fields = File.read('/proc/meminfo').lines.to_h { |line| line.split(':', 2).then { |k, v| [k, v.to_i] } }
|
|
45
|
+
total_gb = fields.fetch('MemTotal', 0) / 1_048_576.0
|
|
46
|
+
used_gb = total_gb - (fields.fetch('MemAvailable', 0) / 1_048_576.0)
|
|
47
|
+
format('mem %<used>.1fG/%<total>.1fG', used: used_gb, total: total_gb)
|
|
48
|
+
rescue Errno::ENOENT, IOError
|
|
49
|
+
'mem n/a'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# WSL2's bind-mounted (9p) volumes are often the real bottleneck behind a
|
|
53
|
+
# slow `bundle`/`rails` boot, and that shows up here rather than in CPU.
|
|
54
|
+
def disk_io
|
|
55
|
+
now = monotonic
|
|
56
|
+
elapsed = [now - @last_sampled_at, 0.001].max
|
|
57
|
+
current = disk_sectors
|
|
58
|
+
read_kbps = (current[:read] - @last_disk[:read]) * 0.5 / elapsed
|
|
59
|
+
write_kbps = (current[:write] - @last_disk[:write]) * 0.5 / elapsed
|
|
60
|
+
@last_disk = current
|
|
61
|
+
@last_sampled_at = now
|
|
62
|
+
format('io read %<read>.0fKB/s write %<write>.0fKB/s', read: read_kbps, write: write_kbps)
|
|
63
|
+
rescue Errno::ENOENT, IOError
|
|
64
|
+
'io n/a'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def disk_sectors
|
|
68
|
+
totals = { read: 0, write: 0 }
|
|
69
|
+
File.readlines('/proc/diskstats').each do |line|
|
|
70
|
+
fields = line.split
|
|
71
|
+
next if fields[2].to_s.match?(/\A(loop|ram)/)
|
|
72
|
+
|
|
73
|
+
totals[:read] += fields[5].to_i
|
|
74
|
+
totals[:write] += fields[9].to_i
|
|
75
|
+
end
|
|
76
|
+
totals
|
|
77
|
+
rescue Errno::ENOENT, IOError
|
|
78
|
+
{ read: 0, write: 0 }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def top_processes
|
|
82
|
+
lines = `ps -eo pid,pcpu,pmem,comm --sort=-pcpu 2>/dev/null`.lines.drop(1).first(3)
|
|
83
|
+
entries = lines.filter_map do |line|
|
|
84
|
+
pid, cpu, mem, comm = line.split(nil, 4)
|
|
85
|
+
next unless comm
|
|
86
|
+
|
|
87
|
+
"#{comm.strip}(#{pid}) cpu #{cpu}%/mem #{mem}%"
|
|
88
|
+
end
|
|
89
|
+
"top: #{entries.join(', ')}"
|
|
90
|
+
rescue StandardError
|
|
91
|
+
'top: n/a'
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/wip/version.rb
CHANGED
data/lib/wip.rb
CHANGED
|
@@ -8,6 +8,9 @@ require_relative 'wip/environment'
|
|
|
8
8
|
require_relative 'wip/command_resolver'
|
|
9
9
|
require_relative 'wip/error_interpreter'
|
|
10
10
|
require_relative 'wip/command_builder'
|
|
11
|
+
require_relative 'wip/command_display'
|
|
11
12
|
require_relative 'wip/command_runner'
|
|
13
|
+
require_relative 'wip/resource_monitor'
|
|
14
|
+
require_relative 'wip/debug_reporter'
|
|
12
15
|
require_relative 'wip/doctor'
|
|
13
16
|
require_relative 'wip/cli'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wslc-wip
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wip contributors
|
|
@@ -35,20 +35,24 @@ files:
|
|
|
35
35
|
- lib/wip.rb
|
|
36
36
|
- lib/wip/cli.rb
|
|
37
37
|
- lib/wip/command_builder.rb
|
|
38
|
+
- lib/wip/command_display.rb
|
|
38
39
|
- lib/wip/command_resolver.rb
|
|
39
40
|
- lib/wip/command_runner.rb
|
|
40
41
|
- lib/wip/config.rb
|
|
41
42
|
- lib/wip/config_loader.rb
|
|
43
|
+
- lib/wip/debug_reporter.rb
|
|
42
44
|
- lib/wip/doctor.rb
|
|
43
45
|
- lib/wip/environment.rb
|
|
44
46
|
- lib/wip/error_interpreter.rb
|
|
45
47
|
- lib/wip/errors.rb
|
|
48
|
+
- lib/wip/resource_monitor.rb
|
|
46
49
|
- lib/wip/version.rb
|
|
47
|
-
homepage: https://
|
|
50
|
+
homepage: https://wslc-wip.slidict.com/
|
|
48
51
|
licenses:
|
|
49
52
|
- MIT
|
|
50
53
|
metadata:
|
|
51
54
|
rubygems_mfa_required: 'true'
|
|
55
|
+
homepage_uri: https://wslc-wip.slidict.com/
|
|
52
56
|
source_code_uri: https://github.com/slidict/wip
|
|
53
57
|
changelog_uri: https://github.com/slidict/wip/releases
|
|
54
58
|
bug_tracker_uri: https://github.com/slidict/wip/issues
|