wslc-wip 0.4.2 → 0.5.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/README.md +36 -4
- data/lib/wip/build_context.rb +48 -0
- data/lib/wip/cli.rb +19 -4
- data/lib/wip/command_builder.rb +6 -2
- data/lib/wip/debug_reporter.rb +35 -3
- data/lib/wip/docker_ignore.rb +57 -0
- data/lib/wip/dotenv_loader.rb +39 -0
- data/lib/wip/resource_monitor.rb +46 -6
- data/lib/wip/version.rb +1 -1
- data/lib/wip.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0845dfb67f17323b8a99e6f2f92ef06c17bc03fd1316680cf5f43f2640d73da3'
|
|
4
|
+
data.tar.gz: 6eea9bb520aed0fff5b401b71a20dccdade6b50d9a71403037e59e4e5b83ab2e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b522d206ba3070244f51bda9d4e144dfe95df63ab01bb92b35d0fe0e84a29035c997d7b061c2ec23286c8bd9f586c69e276b7304abd1c8ad81fe808293599b5
|
|
7
|
+
data.tar.gz: d7fc237820edfcc734f55775e027133c5f9877c24b16f5b1ca6aa98304b35fdea5825a179ff7f64b4d22fe2634d52d11761777284b68a86e76faa56432d33230
|
data/README.md
CHANGED
|
@@ -103,6 +103,22 @@ commands:
|
|
|
103
103
|
credential, or auth. Keep real secrets out of the config file and in your runtime environment
|
|
104
104
|
instead.
|
|
105
105
|
|
|
106
|
+
### .env
|
|
107
|
+
|
|
108
|
+
Like `docker compose`, `wip` automatically loads a `.env` file next to `wip.yml` (one `KEY=VALUE`
|
|
109
|
+
per line; `#` comments, blank lines, `export` prefixes, and quoted values are all supported) and
|
|
110
|
+
passes its keys through as container environment variables on `build`, `up`, `run`, `exec`, and
|
|
111
|
+
custom commands. `.env` only fills in keys that aren't already set by `defaults.env` or a
|
|
112
|
+
command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH` to load a
|
|
113
|
+
different file instead.
|
|
114
|
+
|
|
115
|
+
### .dockerignore
|
|
116
|
+
|
|
117
|
+
`wip build` reads `.dockerignore` from the build context and stages a filtered copy of the
|
|
118
|
+
context (skipping anything it matches) before handing it to `wslc build`, since `wslc` sends the
|
|
119
|
+
context as-is otherwise. If there's no `.dockerignore`, the original context directory is used
|
|
120
|
+
directly with no copying.
|
|
121
|
+
|
|
106
122
|
### Dependency containers
|
|
107
123
|
|
|
108
124
|
If your app needs sidecar services (a database, Redis, ...), declare them under `dependencies`
|
|
@@ -148,14 +164,30 @@ exit, but the timestamp of the `+ ...` line tells you when wip finished its own
|
|
|
148
164
|
off to `wslc`/`docker` — useful for telling wip-side overhead apart from time spent booting inside
|
|
149
165
|
the container.
|
|
150
166
|
|
|
151
|
-
While a step is still running, wip also prints a host resource snapshot (load average, memory
|
|
152
|
-
|
|
153
|
-
command has produced any output of its own:
|
|
167
|
+
While a step is still running, wip also prints a host resource snapshot (load average, memory,
|
|
168
|
+
disk I/O, and the top CPU-consuming processes) every 5 seconds, so a hang is visible even before
|
|
169
|
+
the command has produced any output of its own:
|
|
154
170
|
|
|
155
171
|
```console
|
|
156
|
-
wip: [debug] still running (load 3.42 2.10 1.05 | mem 6.1G/15.6G | top: wslc.exe(8842) cpu 61.0%/mem 3.2%, ...): running: wslc.exe exec -it -w /app app bin/rails c
|
|
172
|
+
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
173
|
```
|
|
158
174
|
|
|
175
|
+
The disk I/O figure is worth watching first if the host's CPU and memory look idle — a slow
|
|
176
|
+
`bundle`/`rails` boot is often WSL2's bind-mounted (`.:/app`-style) volumes doing a lot of small
|
|
177
|
+
reads, not the container starving for CPU.
|
|
178
|
+
|
|
179
|
+
For commands that hand the real terminal to the child (`-it`, e.g. `rails c`), these periodic
|
|
180
|
+
snapshots go to a log file instead of your terminal — wip prints the path once at the start —
|
|
181
|
+
since writing into a terminal the child controls in raw mode would garble both outputs. Commands
|
|
182
|
+
that don't need a TTY still get the snapshots printed live.
|
|
183
|
+
|
|
184
|
+
Override that choice with `--debug-log`:
|
|
185
|
+
|
|
186
|
+
- `--debug-log=-` forces snapshots inline even for `-it` commands (only useful if you know your
|
|
187
|
+
terminal/pager can tolerate the interleaving).
|
|
188
|
+
- `--debug-log=PATH` always writes snapshots to `PATH`, including for non-TTY commands, e.g. to
|
|
189
|
+
keep every run's snapshots in one place: `wip rails c --debug --debug-log=/tmp/wip-debug.log`.
|
|
190
|
+
|
|
159
191
|
## doctor
|
|
160
192
|
|
|
161
193
|
Each check prints as `[OK]`, `[WARN]`, or `[FAIL]`. Warnings alone exit 0; a WSL2, interop, WSLC,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
module Wip
|
|
8
|
+
# Stages a build context into a scratch directory with anything matched by
|
|
9
|
+
# .dockerignore left out, since wslc build (unlike `docker build`) sends the
|
|
10
|
+
# context as-is instead of filtering it itself.
|
|
11
|
+
class BuildContext
|
|
12
|
+
def initialize(context, ignore: nil)
|
|
13
|
+
@root = Pathname(context).expand_path
|
|
14
|
+
@ignore = ignore || DockerIgnore.load(@root.join('.dockerignore'))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stage
|
|
18
|
+
return yield @root.to_s if @ignore.empty?
|
|
19
|
+
|
|
20
|
+
Dir.mktmpdir('wip-build-context-') do |dir|
|
|
21
|
+
copy_included_files(Pathname(dir))
|
|
22
|
+
yield dir
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def copy_included_files(destination)
|
|
29
|
+
each_included_file do |relative_path|
|
|
30
|
+
target = destination.join(relative_path)
|
|
31
|
+
FileUtils.mkdir_p(target.dirname)
|
|
32
|
+
FileUtils.cp(@root.join(relative_path), target)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def each_included_file
|
|
37
|
+
Dir.glob('**/*', File::FNM_DOTMATCH, base: @root.to_s).each do |entry|
|
|
38
|
+
next if %w[. ..].include?(entry)
|
|
39
|
+
|
|
40
|
+
path = @root.join(entry)
|
|
41
|
+
next unless path.file?
|
|
42
|
+
next if @ignore.ignored?(entry)
|
|
43
|
+
|
|
44
|
+
yield entry
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/wip/cli.rb
CHANGED
|
@@ -9,7 +9,9 @@ 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 :env_file, type: :string, desc: 'Path to a dotenv file (default: .env next to wip.yml)'
|
|
12
13
|
class_option :debug, type: :boolean, default: false, desc: 'Print progress and timing for each step'
|
|
14
|
+
class_option :debug_log, type: :string, desc: 'Where --debug snapshots go: a file path, or "-" for inline'
|
|
13
15
|
default_task :dispatch
|
|
14
16
|
|
|
15
17
|
def self.exit_on_failure? = true
|
|
@@ -51,7 +53,11 @@ module Wip
|
|
|
51
53
|
desc 'build [OPTIONS]', 'Build the configured image'
|
|
52
54
|
def build(*extra)
|
|
53
55
|
extra.shift if extra.first == '--'
|
|
54
|
-
|
|
56
|
+
settings = load_config.command('build') || {}
|
|
57
|
+
context = settings['context'] || load_config.defaults['context'] || '.'
|
|
58
|
+
BuildContext.new(context).stage do |staged_context|
|
|
59
|
+
execute(builder.build(settings: settings.merge('context' => staged_context), extra: extra))
|
|
60
|
+
end
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
desc 'up', 'Start the configured container and its dependencies, creating them if necessary'
|
|
@@ -117,11 +123,20 @@ module Wip
|
|
|
117
123
|
def loader = ConfigLoader.new(path: options[:config])
|
|
118
124
|
def load_config = (@load_config ||= loader.load)
|
|
119
125
|
def resolver = CommandResolver.new
|
|
120
|
-
|
|
126
|
+
|
|
127
|
+
def dotenv_path
|
|
128
|
+
options[:env_file] ? Pathname(options[:env_file]).expand_path : Pathname(load_config.path).dirname.join('.env')
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def dotenv = @dotenv ||= DotenvLoader.new(dotenv_path).load
|
|
132
|
+
|
|
133
|
+
def builder
|
|
134
|
+
CommandBuilder.new(wslc: resolver.resolve(load_config.wslc_command), config: load_config, dotenv: dotenv)
|
|
135
|
+
end
|
|
121
136
|
|
|
122
137
|
def execute(command, interactive: false, exit_on_failure: true)
|
|
123
138
|
runner = CommandRunner.new(debug: debug?)
|
|
124
|
-
code = reporter.step("running: #{CommandDisplay.for_debug(command)}") do
|
|
139
|
+
code = reporter.step("running: #{CommandDisplay.for_debug(command)}", live: !interactive) do
|
|
125
140
|
runner.run(command, interactive: interactive)
|
|
126
141
|
end
|
|
127
142
|
exit(code) if exit_on_failure && !code.zero?
|
|
@@ -143,7 +158,7 @@ module Wip
|
|
|
143
158
|
end
|
|
144
159
|
|
|
145
160
|
def debug? = options[:debug] || !ENV['WIP_DEBUG'].to_s.empty?
|
|
146
|
-
def reporter = @reporter ||= DebugReporter.new(enabled: debug
|
|
161
|
+
def reporter = @reporter ||= DebugReporter.new(enabled: debug?, log: options[:debug_log])
|
|
147
162
|
|
|
148
163
|
def ensure_network
|
|
149
164
|
network = load_config.network
|
data/lib/wip/command_builder.rb
CHANGED
|
@@ -5,10 +5,11 @@ require 'shellwords'
|
|
|
5
5
|
module Wip
|
|
6
6
|
# Builds the argument arrays for wslc build/exec/run/custom invocations.
|
|
7
7
|
class CommandBuilder
|
|
8
|
-
def initialize(wslc:, config:, environment: Environment.new)
|
|
8
|
+
def initialize(wslc:, config:, environment: Environment.new, dotenv: {})
|
|
9
9
|
@wslc = wslc
|
|
10
10
|
@config = config
|
|
11
11
|
@environment = environment
|
|
12
|
+
@dotenv = dotenv
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def exec(arguments, settings: {}, interactive: true)
|
|
@@ -115,7 +116,7 @@ module Wip
|
|
|
115
116
|
def options(values, include_container: false, include_publish: true)
|
|
116
117
|
result = []
|
|
117
118
|
result.push('-w', values['workdir']) unless values['workdir'].to_s.empty?
|
|
118
|
-
values
|
|
119
|
+
merged_env(values).each { |key, value| result.push('-e', "#{key}=#{value}") }
|
|
119
120
|
if include_publish
|
|
120
121
|
Array(values['ports']).each { |port| result.push('-p', port.to_s) }
|
|
121
122
|
Array(values['volumes']).each { |volume| result.push('-v', volume.to_s) }
|
|
@@ -124,6 +125,9 @@ module Wip
|
|
|
124
125
|
result
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
# .env supplies defaults; env set in wip.yml (defaults or per-command) wins on conflict.
|
|
129
|
+
def merged_env(values) = @dotenv.merge(values.fetch('env', {}))
|
|
130
|
+
|
|
127
131
|
def required(values, key)
|
|
128
132
|
value = values[key]
|
|
129
133
|
raise ConfigError, "Configured #{key} must not be empty" if value.to_s.empty?
|
data/lib/wip/debug_reporter.rb
CHANGED
|
@@ -1,27 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
|
|
3
5
|
module Wip
|
|
4
6
|
# Prints each step wip takes and how long it took, when debug mode is enabled.
|
|
5
7
|
class DebugReporter
|
|
6
|
-
|
|
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)
|
|
7
13
|
@enabled = enabled
|
|
8
14
|
@out = out
|
|
15
|
+
@log = log
|
|
9
16
|
end
|
|
10
17
|
|
|
11
|
-
|
|
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)
|
|
12
24
|
return yield unless @enabled
|
|
13
25
|
|
|
14
26
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
15
27
|
@out.puts "wip: [debug] #{label}"
|
|
16
|
-
|
|
28
|
+
file = log_file(live)
|
|
29
|
+
monitor = ResourceMonitor.new(out: file || @out)
|
|
17
30
|
monitor.start(label)
|
|
18
31
|
begin
|
|
19
32
|
yield
|
|
20
33
|
ensure
|
|
21
34
|
monitor.stop
|
|
35
|
+
file&.close
|
|
22
36
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
23
37
|
@out.puts format('wip: [debug] done in %<elapsed>.2fs: %<label>s', elapsed: elapsed, label: label)
|
|
24
38
|
end
|
|
25
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
|
|
26
58
|
end
|
|
27
59
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Wip
|
|
6
|
+
# Parses a .dockerignore file and decides whether a build-context-relative
|
|
7
|
+
# path should be excluded, following the same pattern rules as the Docker
|
|
8
|
+
# CLI (gitignore-like globs, later rules override earlier ones, `!` negates).
|
|
9
|
+
class DockerIgnore
|
|
10
|
+
Rule = Struct.new(:pattern, :negate)
|
|
11
|
+
|
|
12
|
+
FNMATCH_FLAGS = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
|
|
13
|
+
|
|
14
|
+
def self.load(path)
|
|
15
|
+
path = Pathname(path)
|
|
16
|
+
return new([]) unless path.file?
|
|
17
|
+
|
|
18
|
+
new(path.readlines)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(lines)
|
|
22
|
+
@rules = lines.filter_map { |line| parse(line) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def empty? = @rules.empty?
|
|
26
|
+
|
|
27
|
+
def ignored?(relative_path)
|
|
28
|
+
@rules.reduce(false) do |ignored, rule|
|
|
29
|
+
matches?(rule.pattern, relative_path) ? !rule.negate : ignored
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse(line)
|
|
36
|
+
line = line.strip
|
|
37
|
+
return nil if line.empty? || line.start_with?('#')
|
|
38
|
+
|
|
39
|
+
negate = line.start_with?('!')
|
|
40
|
+
pattern = negate ? line[1..] : line
|
|
41
|
+
pattern = pattern.delete_suffix('/')
|
|
42
|
+
anchored = pattern.start_with?('/')
|
|
43
|
+
pattern = pattern.delete_prefix('/')
|
|
44
|
+
pattern = "**/#{pattern}" if !anchored && !pattern.include?('/')
|
|
45
|
+
Rule.new(pattern, negate)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A match on a directory component also excludes everything under it,
|
|
49
|
+
# so every prefix of the path (not just the full path) is tested.
|
|
50
|
+
def matches?(pattern, relative_path)
|
|
51
|
+
components = relative_path.split('/')
|
|
52
|
+
(1..components.size).any? do |i|
|
|
53
|
+
File.fnmatch(pattern, components[0...i].join('/'), FNMATCH_FLAGS)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Wip
|
|
6
|
+
# Parses a .env file the way `docker compose` does, so values don't have to
|
|
7
|
+
# be duplicated into wip.yml just to reach the container as -e flags.
|
|
8
|
+
class DotenvLoader
|
|
9
|
+
LINE_PATTERN = /\A(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)\z/
|
|
10
|
+
|
|
11
|
+
def initialize(path)
|
|
12
|
+
@path = Pathname(path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load
|
|
16
|
+
return {} unless @path.file?
|
|
17
|
+
|
|
18
|
+
@path.readlines.each_with_object({}) do |line, env|
|
|
19
|
+
line = line.strip
|
|
20
|
+
next if line.empty? || line.start_with?('#')
|
|
21
|
+
|
|
22
|
+
match = LINE_PATTERN.match(line)
|
|
23
|
+
next unless match
|
|
24
|
+
|
|
25
|
+
env[match[1]] = unquote(match[2])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def unquote(value)
|
|
32
|
+
value = value.strip
|
|
33
|
+
return value[1..-2] if value.start_with?('"') && value.end_with?('"') && value.length >= 2
|
|
34
|
+
return value[1..-2] if value.start_with?("'") && value.end_with?("'") && value.length >= 2
|
|
35
|
+
|
|
36
|
+
value.split(/\s+#/, 2).first.to_s.strip
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/wip/resource_monitor.rb
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Wip
|
|
4
|
-
# Periodically prints host CPU/memory/process info while a debug step
|
|
5
|
-
# running, so a hung or slow step is visible even before it produces
|
|
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.
|
|
6
7
|
class ResourceMonitor
|
|
7
8
|
def initialize(interval: 5, out: $stderr)
|
|
8
9
|
@interval = interval
|
|
9
10
|
@out = out
|
|
11
|
+
@last_disk = disk_sectors
|
|
12
|
+
@last_sampled_at = monotonic
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def start(label)
|
|
13
16
|
@thread = Thread.new do
|
|
14
17
|
loop do
|
|
15
18
|
sleep @interval
|
|
16
|
-
@out.puts "wip: [debug] still running (#{
|
|
19
|
+
@out.puts "wip: [debug] still running (#{snapshot}): #{label}"
|
|
17
20
|
end
|
|
18
21
|
end
|
|
19
22
|
end
|
|
@@ -25,6 +28,12 @@ module Wip
|
|
|
25
28
|
|
|
26
29
|
private
|
|
27
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
|
+
|
|
28
37
|
def load_average
|
|
29
38
|
"load #{File.read('/proc/loadavg').split[0..2].join(' ')}"
|
|
30
39
|
rescue Errno::ENOENT, IOError
|
|
@@ -40,15 +49,46 @@ module Wip
|
|
|
40
49
|
'mem n/a'
|
|
41
50
|
end
|
|
42
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
|
+
|
|
43
81
|
def top_processes
|
|
44
|
-
`ps -eo pid,pcpu,pmem,comm --sort=-pcpu 2>/dev/null`.lines.drop(1).first(3)
|
|
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|
|
|
45
84
|
pid, cpu, mem, comm = line.split(nil, 4)
|
|
46
85
|
next unless comm
|
|
47
86
|
|
|
48
87
|
"#{comm.strip}(#{pid}) cpu #{cpu}%/mem #{mem}%"
|
|
49
|
-
end
|
|
88
|
+
end
|
|
89
|
+
"top: #{entries.join(', ')}"
|
|
50
90
|
rescue StandardError
|
|
51
|
-
'n/a'
|
|
91
|
+
'top: n/a'
|
|
52
92
|
end
|
|
53
93
|
end
|
|
54
94
|
end
|
data/lib/wip/version.rb
CHANGED
data/lib/wip.rb
CHANGED
|
@@ -5,6 +5,9 @@ require_relative 'wip/errors'
|
|
|
5
5
|
require_relative 'wip/config'
|
|
6
6
|
require_relative 'wip/config_loader'
|
|
7
7
|
require_relative 'wip/environment'
|
|
8
|
+
require_relative 'wip/dotenv_loader'
|
|
9
|
+
require_relative 'wip/docker_ignore'
|
|
10
|
+
require_relative 'wip/build_context'
|
|
8
11
|
require_relative 'wip/command_resolver'
|
|
9
12
|
require_relative 'wip/error_interpreter'
|
|
10
13
|
require_relative 'wip/command_builder'
|
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
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wip contributors
|
|
@@ -33,6 +33,7 @@ files:
|
|
|
33
33
|
- README.md
|
|
34
34
|
- exe/wip
|
|
35
35
|
- lib/wip.rb
|
|
36
|
+
- lib/wip/build_context.rb
|
|
36
37
|
- lib/wip/cli.rb
|
|
37
38
|
- lib/wip/command_builder.rb
|
|
38
39
|
- lib/wip/command_display.rb
|
|
@@ -41,7 +42,9 @@ files:
|
|
|
41
42
|
- lib/wip/config.rb
|
|
42
43
|
- lib/wip/config_loader.rb
|
|
43
44
|
- lib/wip/debug_reporter.rb
|
|
45
|
+
- lib/wip/docker_ignore.rb
|
|
44
46
|
- lib/wip/doctor.rb
|
|
47
|
+
- lib/wip/dotenv_loader.rb
|
|
45
48
|
- lib/wip/environment.rb
|
|
46
49
|
- lib/wip/error_interpreter.rb
|
|
47
50
|
- lib/wip/errors.rb
|