wslc-wip 0.4.0 → 0.4.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1e6e4afaf5e33e69fa5dd6dc35f4fc32695e9916b0ceeac998ae58334fab3d4
4
- data.tar.gz: 56e438283cad62937ac37132bb49048a1159897de4d3058c122f14272bca9e68
3
+ metadata.gz: da7bb5fa632921c307adeeea727e792d38e688204393dfd3ee2c7759e40791b4
4
+ data.tar.gz: 2e2a277a4ab2dcf0ce540778b6263a142553430452905924f67158dc13ffed54
5
5
  SHA512:
6
- metadata.gz: 29bfbc9430132bf62ae58813e333f64df00d9228ad98386f3374e3a0de897e55605b977d7208bf7ce9622957e0a3acd1d598e25f25a594eed839fc7007deb37d
7
- data.tar.gz: 8588e1b97851a59936bb42daf6a8d09002e1c3fdba12dafba0a35bfe8a1c9e430742bee1b8f818a677b83cc793f6b96151be1d729f7465632c3c368d9b518cd8
6
+ metadata.gz: f71156a19e193d0402f2fa4dc501aca0f32b27d63807c20b7a1a8dcddf1932f091af8ec12a0310aac36084a6c28034d843100abbf4adc3ac9c081d4e9304c18c
7
+ data.tar.gz: 538f842fd08441c9ec1603fc147d7c5ce7a5c6f2f2ce22edd0673b12709da19a6c23d3e7f1231a73d69d675337f09a17bc312aa07792308df414f1130e11b2a5
data/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  [![License: MIT](https://img.shields.io/github/license/slidict/wip.svg)](LICENSE)
6
6
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg)](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,32 @@ 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. Set `WIP_DEBUG=1` to print the `Shellwords`-joined command before
131
- running it.
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
+ used, and the top CPU-consuming processes) every 5 seconds, so a hang is visible even before the
153
+ 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 | top: wslc.exe(8842) cpu 61.0%/mem 3.2%, ...): running: wslc.exe exec -it -w /app app bin/rails c
157
+ ```
132
158
 
133
159
  ## doctor
134
160
 
data/lib/wip/cli.rb CHANGED
@@ -9,6 +9,7 @@ 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'
12
13
  default_task :dispatch
13
14
 
14
15
  def self.exit_on_failure? = true
@@ -119,19 +120,31 @@ module Wip
119
120
  def builder = CommandBuilder.new(wslc: resolver.resolve(load_config.wslc_command), config: load_config)
120
121
 
121
122
  def execute(command, interactive: false, exit_on_failure: true)
122
- code = CommandRunner.new.run(command, interactive: interactive)
123
+ runner = CommandRunner.new(debug: debug?)
124
+ code = reporter.step("running: #{CommandDisplay.for_debug(command)}") do
125
+ runner.run(command, interactive: interactive)
126
+ end
123
127
  exit(code) if exit_on_failure && !code.zero?
124
128
  code
125
129
  end
126
130
 
127
131
  def resource_exists?(find_command)
128
- out = StringIO.new
129
- code = CommandRunner.new(stdout: out, stderr: StringIO.new).run(find_command)
130
- code.zero? && !JSON.parse(out.string).empty?
132
+ code, output = probe(find_command)
133
+ code.zero? && !JSON.parse(output).empty?
131
134
  rescue JSON::ParserError
132
135
  false
133
136
  end
134
137
 
138
+ def probe(command)
139
+ out = StringIO.new
140
+ runner = CommandRunner.new(stdout: out, stderr: StringIO.new, debug: debug?)
141
+ code = reporter.step("checking: #{CommandDisplay.for_debug(command)}") { runner.run(command) }
142
+ [code, out.string]
143
+ end
144
+
145
+ def debug? = options[:debug] || !ENV['WIP_DEBUG'].to_s.empty?
146
+ def reporter = @reporter ||= DebugReporter.new(enabled: debug?)
147
+
135
148
  def ensure_network
136
149
  network = load_config.network
137
150
  return unless network
@@ -142,11 +155,8 @@ module Wip
142
155
  end
143
156
 
144
157
  def network_exists?(network)
145
- out = StringIO.new
146
- code = CommandRunner.new(stdout: out, stderr: StringIO.new).run(builder.network_list)
147
- return false unless code.zero?
148
-
149
- JSON.parse(out.string).any? { |entry| entry['Name'] == network }
158
+ code, output = probe(builder.network_list)
159
+ code.zero? && JSON.parse(output).any? { |entry| entry['Name'] == network }
150
160
  rescue JSON::ParserError
151
161
  false
152
162
  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
@@ -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 "+ #{Shellwords.join(command)}" if ENV['WIP_DEBUG']
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,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ # Prints each step wip takes and how long it took, when debug mode is enabled.
5
+ class DebugReporter
6
+ def initialize(enabled:, out: $stderr)
7
+ @enabled = enabled
8
+ @out = out
9
+ end
10
+
11
+ def step(label)
12
+ return yield unless @enabled
13
+
14
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
15
+ @out.puts "wip: [debug] #{label}"
16
+ monitor = ResourceMonitor.new(out: @out)
17
+ monitor.start(label)
18
+ begin
19
+ yield
20
+ ensure
21
+ monitor.stop
22
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
23
+ @out.puts format('wip: [debug] done in %<elapsed>.2fs: %<label>s', elapsed: elapsed, label: label)
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/wip/doctor.rb CHANGED
@@ -15,9 +15,8 @@ module Wip
15
15
 
16
16
  def call
17
17
  results = []
18
- results << result(@environment.wsl2? ? :ok : :fail, 'Running on WSL2', 'Not running on WSL2')
19
- results << result(@environment.windows_interop? ? :ok : :fail, 'Windows executable interoperability is enabled',
20
- 'Windows executable interoperability is disabled')
18
+ results << result(@environment.wsl2? ? :ok : :fail, *wsl2_messages)
19
+ results << interop_result unless @environment.windows?
21
20
  results << Result.new(:ok, "Architecture: #{@environment.architecture}")
22
21
  config = load_config(results)
23
22
  command = resolve(config, results) if config
@@ -29,6 +28,17 @@ module Wip
29
28
 
30
29
  private
31
30
 
31
+ def wsl2_messages
32
+ return ['WSL2 is available', 'WSL2 is not available'] if @environment.windows?
33
+
34
+ ['Running on WSL2', 'Not running on WSL2']
35
+ end
36
+
37
+ def interop_result
38
+ result(@environment.windows_interop? ? :ok : :fail, 'Windows executable interoperability is enabled',
39
+ 'Windows executable interoperability is disabled')
40
+ end
41
+
32
42
  def load_config(results)
33
43
  config = @loader.load
34
44
  invalid = %w[container image].select { |key| config.defaults[key].to_s.empty? }
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'open3'
4
+
3
5
  module Wip
4
6
  # Detects WSL2, Windows interop, and architecture facts about the host.
5
7
  class Environment
@@ -8,7 +10,11 @@ module Wip
8
10
  @stdout = stdout
9
11
  end
10
12
 
13
+ def windows? = Gem.win_platform?
14
+
11
15
  def wsl2?
16
+ return wsl2_backend_available? if windows?
17
+
12
18
  File.read('/proc/version').match?(/microsoft.*WSL2/i)
13
19
  rescue Errno::ENOENT
14
20
  false
@@ -18,9 +24,20 @@ module Wip
18
24
  def interactive? = @stdin.tty? && @stdout.tty?
19
25
 
20
26
  def architecture
21
- machine = `uname -m`.strip
22
- { 'x86_64' => 'linux/amd64', 'aarch64' => 'linux/arm64', 'arm64' => 'linux/arm64' }.fetch(machine,
23
- "linux/#{machine}")
27
+ machine = RbConfig::CONFIG['host_cpu']
28
+ { 'x86_64' => 'linux/amd64', 'x64' => 'linux/amd64', 'aarch64' => 'linux/arm64', 'arm64' => 'linux/arm64' }
29
+ .fetch(machine, "linux/#{machine}")
30
+ end
31
+
32
+ private
33
+
34
+ # On native Windows there is no /proc/version to read, so ask Windows
35
+ # itself whether the WSL2 backend that WSLC depends on is installed.
36
+ def wsl2_backend_available?
37
+ _output, status = Open3.capture2e('wsl.exe', '--status')
38
+ status.success?
39
+ rescue Errno::ENOENT
40
+ false
24
41
  end
25
42
  end
26
43
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ # Periodically prints host CPU/memory/process info while a debug step is still
5
+ # running, so a hung or slow step is visible even before it produces output.
6
+ class ResourceMonitor
7
+ def initialize(interval: 5, out: $stderr)
8
+ @interval = interval
9
+ @out = out
10
+ end
11
+
12
+ def start(label)
13
+ @thread = Thread.new do
14
+ loop do
15
+ sleep @interval
16
+ @out.puts "wip: [debug] still running (#{load_average} | #{memory} | top: #{top_processes}): #{label}"
17
+ end
18
+ end
19
+ end
20
+
21
+ def stop
22
+ @thread&.kill
23
+ @thread&.join
24
+ end
25
+
26
+ private
27
+
28
+ def load_average
29
+ "load #{File.read('/proc/loadavg').split[0..2].join(' ')}"
30
+ rescue Errno::ENOENT, IOError
31
+ 'load n/a'
32
+ end
33
+
34
+ def memory
35
+ fields = File.read('/proc/meminfo').lines.to_h { |line| line.split(':', 2).then { |k, v| [k, v.to_i] } }
36
+ total_gb = fields.fetch('MemTotal', 0) / 1_048_576.0
37
+ used_gb = total_gb - (fields.fetch('MemAvailable', 0) / 1_048_576.0)
38
+ format('mem %<used>.1fG/%<total>.1fG', used: used_gb, total: total_gb)
39
+ rescue Errno::ENOENT, IOError
40
+ 'mem n/a'
41
+ end
42
+
43
+ def top_processes
44
+ `ps -eo pid,pcpu,pmem,comm --sort=-pcpu 2>/dev/null`.lines.drop(1).first(3).filter_map do |line|
45
+ pid, cpu, mem, comm = line.split(nil, 4)
46
+ next unless comm
47
+
48
+ "#{comm.strip}(#{pid}) cpu #{cpu}%/mem #{mem}%"
49
+ end.join(', ')
50
+ rescue StandardError
51
+ 'n/a'
52
+ end
53
+ end
54
+ end
data/lib/wip/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wip
4
- VERSION = '0.4.0'
4
+ VERSION = '0.4.2'
5
5
  end
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.0
4
+ version: 0.4.2
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://github.com/slidict/wip
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