command_runner_ng 0.0.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/command_runner.rb +127 -0
  3. metadata +44 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 130d48c47f75bc4bf9cda29964b2ee29f34c1c57
4
+ data.tar.gz: f63abf2a6127e4ccbe0d669ef75baaeaa93726ab
5
+ SHA512:
6
+ metadata.gz: 8f31d164defc091230024b5b1d6ef0f1a37650d48c3fdab131e8220ea4cfc904aad24c33469ed5d27ffce0744876ef966f0f8e424e355dd03ed3e45a87fd91a9
7
+ data.tar.gz: 3b7d20c02db29c8ded423ba1471fd7c5b0e2d6775d50541ab5a882366cb485e80c7770d395a0ef36344519b110627a519572d71b782eff1ecab457de60d97d88
@@ -0,0 +1,127 @@
1
+ module CommandRunner
2
+
3
+ MAX_TIME = Time.new(2**63 -1)
4
+
5
+ # Like IO.popen(), but block until the child completes.
6
+ # Takes an optional timeout parameter. If timeout is a
7
+ # number the child will be killed after that many seconds
8
+ # if it haven't completed. Alternatively it can be a Hash
9
+ # of timeouts to actions. Each action can be a string or integer
10
+ # specifying the signal to send, or a Proc to execute. The Proc
11
+ # will be called with the child PID as argument.
12
+ # These examples are equivalent:
13
+ # run('sleep 10', timeout: 5) # With a subshell
14
+ # run(['sleep', '10'], timeout: 5) # No subshell in this one and the rest
15
+ # run(['sleep', '10'], timeout: {5 => 'KILL'})
16
+ # run(['sleep', '10'], timeout: {5 => Proc.new { |pid| Process.kill('KILL', pid)}})
17
+ # run(['sleep', '10'], timeout: {
18
+ # 5 => 'KILL',
19
+ # 2 => Proc.new {|pid| puts "PID #{pid} getting SIGKILL in 3s"}
20
+ # })
21
+ #
22
+ # Returns a Hash with :out and :status. :out is a string with stdout
23
+ # and stderr merged, and :status is a Process::Status.
24
+ #
25
+ # As a special case - if an action Proc raises an exception, the child
26
+ # will be killed with SIGKILL, cleaned up, and the exception rethrown
27
+ # to the caller of run.
28
+ #
29
+ def self.run(*args, timeout: nil)
30
+ # These could be tweakable through vararg opts
31
+ tick = 0.1
32
+ bufsize = 4096
33
+
34
+ now = Time.now
35
+
36
+ # Build deadline_sequence. A list of deadlines and corresponding actions to take
37
+ if timeout
38
+ if timeout.is_a? Numeric
39
+ deadline_sequence = [{deadline: now + timeout, action: 'KILL'}]
40
+ elsif timeout.is_a? Hash
41
+ deadline_sequence = timeout.collect do |t, action|
42
+ unless action.is_a? Integer or action.is_a? String or action.is_a? Proc
43
+ raise "Unsupported action type '#{action.class}'. Must be Integer, String, or Proc"
44
+ end
45
+ unless t.is_a? Numeric
46
+ raise "Unsupported timeout value '#{t}'. Must be a Numeric"
47
+ end
48
+ {deadline: now + t, action: action}
49
+ end.sort! { |a, b| a[:deadline] <=> b[:deadline]}
50
+ else
51
+ raise "Unsupported type for timeout paramter: #{timeout.class}"
52
+ end
53
+ else
54
+ deadline_sequence = [{deadline: MAX_TIME, action: 0}]
55
+ end
56
+
57
+ # Spawn child, merging stderr into stdout
58
+ io = IO.popen(*args, :err=>[:child, :out])
59
+ data = ""
60
+
61
+ # Wait until stdout closes
62
+ while Time.now < deadline_sequence.first[:deadline] do
63
+ IO.select([io], nil, nil, tick)
64
+ begin
65
+ data << io.read_nonblock(bufsize)
66
+ rescue IO::WaitReadable
67
+ # Ignore: tick time reached without io
68
+ rescue EOFError
69
+ # Child closed stdout (probably dead, but not necessarily)
70
+ break
71
+ end
72
+ end
73
+
74
+ # Run through all deadlines until command completes.
75
+ # We could merge this block into the selecting block above,
76
+ # but splitting like this saves us a Process.wait syscall per iteration.
77
+ deadline_sequence.each do |point|
78
+ while Time.now < point[:deadline]
79
+ if Process.wait(io.pid, Process::WNOHANG)
80
+ result = {out: data, status: $?}
81
+ io.close
82
+ return result
83
+ else
84
+ IO.select([io], nil, nil, tick)
85
+ begin
86
+ data << io.read_nonblock(bufsize)
87
+ rescue IO::WaitReadable
88
+ # Ignore: tick time reached without io
89
+ rescue EOFError
90
+ # Child closed stdout (probably dead, but not necessarily)
91
+ end
92
+ end
93
+ end
94
+
95
+ # Deadline for this point reached. Fire the action.
96
+ action = point[:action]
97
+ if action.is_a? String or action.is_a? Integer
98
+ Process.kill(action, io.pid)
99
+ elsif action.is_a? Proc
100
+ begin
101
+ action.call(io.pid)
102
+ rescue => e
103
+ # If the action block throws and error, clean up and rethrow
104
+ begin
105
+ Process.kill('KILL', io.pid)
106
+ rescue
107
+ # process already dead
108
+ end
109
+ Process.wait(io.pid)
110
+ io.close
111
+ raise e
112
+ end
113
+ else
114
+ # Given the assertions when building the deadline_sequence this should never be reached
115
+ raise "Internal error in CommandRunnerNG. Child may be left unattended!"
116
+ end
117
+ end
118
+
119
+ # Either we didn't have a deadline, or none of the deadlines killed of the child.
120
+ Process.wait(io.pid)
121
+ result = {out: data, status: $?}
122
+ io.close
123
+
124
+ result
125
+ end
126
+
127
+ end
metadata ADDED
@@ -0,0 +1,44 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: command_runner_ng
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Mikkel Kamstrup Erlandsen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Helper APIs for advanced interactions with subprocesses and shell commands
14
+ email: mikkel.kamstrup@xamarin.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/command_runner.rb
20
+ homepage: http://github.com/kamstrup/command_runner_ng
21
+ licenses:
22
+ - MIT
23
+ metadata: {}
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ requirements: []
39
+ rubyforge_project:
40
+ rubygems_version: 2.0.2
41
+ signing_key:
42
+ specification_version: 4
43
+ summary: Command Runner NG
44
+ test_files: []