exec_service 0.0.0 → 0.1.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/.yardopts +11 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE.md +21 -0
- data/README.md +101 -7
- data/lib/exec_service/controller.rb +359 -0
- data/lib/exec_service/executor.rb +962 -0
- data/lib/exec_service/opts.rb +102 -0
- data/lib/exec_service/result.rb +191 -0
- data/lib/exec_service/version.rb +9 -0
- data/lib/exec_service.rb +511 -6
- metadata +40 -11
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ExecService
|
|
4
|
+
##
|
|
5
|
+
# An internal helper class storing the configuration of a subprocess invocation
|
|
6
|
+
#
|
|
7
|
+
# @private
|
|
8
|
+
#
|
|
9
|
+
class Opts
|
|
10
|
+
##
|
|
11
|
+
# Option keys that belong to exec configuration
|
|
12
|
+
#
|
|
13
|
+
# @private
|
|
14
|
+
#
|
|
15
|
+
CONFIG_KEYS = [
|
|
16
|
+
:argv0,
|
|
17
|
+
:background,
|
|
18
|
+
:env,
|
|
19
|
+
:err,
|
|
20
|
+
:in,
|
|
21
|
+
:logger,
|
|
22
|
+
:log_cmd,
|
|
23
|
+
:log_level,
|
|
24
|
+
:name,
|
|
25
|
+
:out,
|
|
26
|
+
:result_callback,
|
|
27
|
+
:unbundle,
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# Option keys that belong to spawn configuration
|
|
32
|
+
#
|
|
33
|
+
# @private
|
|
34
|
+
#
|
|
35
|
+
SPAWN_KEYS = [
|
|
36
|
+
:chdir,
|
|
37
|
+
:close_others,
|
|
38
|
+
:new_pgroup,
|
|
39
|
+
:pgroup,
|
|
40
|
+
:umask,
|
|
41
|
+
:unsetenv_others,
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# @private
|
|
46
|
+
#
|
|
47
|
+
def initialize(parent = nil)
|
|
48
|
+
if parent
|
|
49
|
+
@config_opts = ::Hash.new { |_h, k| parent.config_opts[k] }
|
|
50
|
+
@spawn_opts = ::Hash.new { |_h, k| parent.spawn_opts[k] }
|
|
51
|
+
elsif block_given?
|
|
52
|
+
@config_opts = ::Hash.new { |_h, k| yield k }
|
|
53
|
+
@spawn_opts = ::Hash.new { |_h, k| yield k }
|
|
54
|
+
else
|
|
55
|
+
@config_opts = {}
|
|
56
|
+
@spawn_opts = {}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# @private
|
|
62
|
+
#
|
|
63
|
+
def add(config)
|
|
64
|
+
config.each do |k, v|
|
|
65
|
+
if CONFIG_KEYS.include?(k)
|
|
66
|
+
@config_opts[k] = v
|
|
67
|
+
elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
|
|
68
|
+
@spawn_opts[k] = v
|
|
69
|
+
else
|
|
70
|
+
raise ::ArgumentError, "Unknown key: #{k.inspect}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# @private
|
|
78
|
+
#
|
|
79
|
+
def delete(*keys)
|
|
80
|
+
keys.each do |k|
|
|
81
|
+
if CONFIG_KEYS.include?(k)
|
|
82
|
+
@config_opts.delete(k)
|
|
83
|
+
elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
|
|
84
|
+
@spawn_opts.delete(k)
|
|
85
|
+
else
|
|
86
|
+
raise ::ArgumentError, "Unknown key: #{k.inspect}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# @private
|
|
94
|
+
#
|
|
95
|
+
attr_reader :config_opts
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# @private
|
|
99
|
+
#
|
|
100
|
+
attr_reader :spawn_opts
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ExecService
|
|
4
|
+
##
|
|
5
|
+
# The result returned from a subcommand execution. This includes the
|
|
6
|
+
# identifying name of the execution (if any), the result status of the
|
|
7
|
+
# execution, and any captured stream output.
|
|
8
|
+
#
|
|
9
|
+
# Possible result statuses are:
|
|
10
|
+
#
|
|
11
|
+
# * The process failed to start. {Result#failed?} will return true, and
|
|
12
|
+
# {Result#exception} will return an exception describing the failure
|
|
13
|
+
# (often an errno).
|
|
14
|
+
# * The process executed and exited with a normal exit code. Either
|
|
15
|
+
# {Result#success?} or {Result#error?} will return true, and
|
|
16
|
+
# {Result.exit_code} will return the numeric exit code.
|
|
17
|
+
# * The process executed but was terminated by an uncaught signal.
|
|
18
|
+
# {Result#signaled?} will return true, and {Result#signal_code} will
|
|
19
|
+
# return the numeric signal code.
|
|
20
|
+
#
|
|
21
|
+
class Result
|
|
22
|
+
##
|
|
23
|
+
# The subcommand's name.
|
|
24
|
+
#
|
|
25
|
+
# @return [Object]
|
|
26
|
+
#
|
|
27
|
+
attr_reader :name
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# The captured output string.
|
|
31
|
+
#
|
|
32
|
+
# @return [String] The string captured from stdout.
|
|
33
|
+
# @return [nil] if the command was not configured to capture stdout.
|
|
34
|
+
#
|
|
35
|
+
attr_reader :captured_out
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# The captured error string.
|
|
39
|
+
#
|
|
40
|
+
# @return [String] The string captured from stderr.
|
|
41
|
+
# @return [nil] if the command was not configured to capture stderr.
|
|
42
|
+
#
|
|
43
|
+
attr_reader :captured_err
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# The Ruby process status object, providing various information about
|
|
47
|
+
# the ending state of the process.
|
|
48
|
+
#
|
|
49
|
+
# Exactly one of {#exception} and {#status} will be non-nil.
|
|
50
|
+
#
|
|
51
|
+
# @return [Process::Status] The status, if the process was successfully
|
|
52
|
+
# spawned and terminated.
|
|
53
|
+
# @return [nil] if the process could not be started.
|
|
54
|
+
#
|
|
55
|
+
attr_reader :status
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# The exception raised if a process couldn't be started.
|
|
59
|
+
#
|
|
60
|
+
# Exactly one of {#exception} and {#status} will be non-nil.
|
|
61
|
+
# Exactly one of {#exception}, {#exit_code}, or {#signal_code} will be
|
|
62
|
+
# non-nil.
|
|
63
|
+
#
|
|
64
|
+
# @return [Exception] The exception raised from process start.
|
|
65
|
+
# @return [nil] if the process started successfully.
|
|
66
|
+
#
|
|
67
|
+
attr_reader :exception
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# The numeric status code for a process that exited normally,
|
|
71
|
+
#
|
|
72
|
+
# Exactly one of {#exception}, {#exit_code}, or {#signal_code} will be
|
|
73
|
+
# non-nil.
|
|
74
|
+
#
|
|
75
|
+
# @return [Integer] the numeric status code, if the process started
|
|
76
|
+
# successfully and exited normally.
|
|
77
|
+
# @return [nil] if the process did not start successfully, or was
|
|
78
|
+
# terminated by an uncaught signal.
|
|
79
|
+
#
|
|
80
|
+
def exit_code
|
|
81
|
+
status&.exitstatus
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# The numeric signal code that caused process termination.
|
|
86
|
+
#
|
|
87
|
+
# Exactly one of {#exception}, {#exit_code}, or {#signal_code} will be
|
|
88
|
+
# non-nil.
|
|
89
|
+
#
|
|
90
|
+
# @return [Integer] The signal that caused the process to terminate.
|
|
91
|
+
# @return [nil] if the process did not start successfully, or executed
|
|
92
|
+
# and exited with a normal exit code.
|
|
93
|
+
#
|
|
94
|
+
def signal_code
|
|
95
|
+
status&.termsig
|
|
96
|
+
end
|
|
97
|
+
alias term_signal signal_code
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# Returns true if the subprocess failed to start, or false if the
|
|
101
|
+
# process was able to execute.
|
|
102
|
+
#
|
|
103
|
+
# @return [boolean]
|
|
104
|
+
#
|
|
105
|
+
def failed?
|
|
106
|
+
status.nil?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Returns true if the subprocess terminated due to an unhandled signal,
|
|
111
|
+
# or false if the process failed to start or exited normally.
|
|
112
|
+
#
|
|
113
|
+
# @return [boolean]
|
|
114
|
+
#
|
|
115
|
+
def signaled?
|
|
116
|
+
!signal_code.nil?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
##
|
|
120
|
+
# Returns true if the subprocess terminated with a zero status, or
|
|
121
|
+
# false if the process failed to start, terminated due to a signal, or
|
|
122
|
+
# returned a nonzero status.
|
|
123
|
+
#
|
|
124
|
+
# @return [boolean]
|
|
125
|
+
#
|
|
126
|
+
def success?
|
|
127
|
+
code = exit_code
|
|
128
|
+
!code.nil? && code.zero?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
##
|
|
132
|
+
# Returns true if the subprocess terminated with a nonzero status, or
|
|
133
|
+
# false if the process failed to start, terminated due to a signal, or
|
|
134
|
+
# returned a zero status.
|
|
135
|
+
#
|
|
136
|
+
# @return [boolean]
|
|
137
|
+
#
|
|
138
|
+
def error?
|
|
139
|
+
code = exit_code
|
|
140
|
+
!code.nil? && !code.zero?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
##
|
|
144
|
+
# Returns an "effective" exit code, which is always an integer if the
|
|
145
|
+
# process has terminated for any reason. In general, this code will be:
|
|
146
|
+
#
|
|
147
|
+
# * The same as {#exit_code} if the process terminated normally with an
|
|
148
|
+
# exit code,
|
|
149
|
+
# * The convention of `128+signalnum` if the process terminated due to
|
|
150
|
+
# a signal,
|
|
151
|
+
# * The convention of 126 if the process could not start due to lack of
|
|
152
|
+
# execution permissions,
|
|
153
|
+
# * The convention of 127 if the process could not start because the
|
|
154
|
+
# command was not recognized or could not be found, or
|
|
155
|
+
# * An undefined value between 1 and 255 for other failures.
|
|
156
|
+
#
|
|
157
|
+
# Note that the normal exit code and signal number cases are stable,
|
|
158
|
+
# but any other cases are subject to change on future releases.
|
|
159
|
+
#
|
|
160
|
+
# @return [Integer]
|
|
161
|
+
#
|
|
162
|
+
def effective_code
|
|
163
|
+
code = exit_code
|
|
164
|
+
return code unless code.nil?
|
|
165
|
+
code = signal_code
|
|
166
|
+
return code + 128 unless code.nil?
|
|
167
|
+
case exception
|
|
168
|
+
when ::Errno::ENOENT
|
|
169
|
+
127
|
|
170
|
+
else
|
|
171
|
+
# This is the intended result for ENOEXEC/EACCES.
|
|
172
|
+
# For now, any other error (e.g. EBADARCH on MacOS) will also map
|
|
173
|
+
# to this result. We can change this in the future since the
|
|
174
|
+
# documentation explicitly allows it.
|
|
175
|
+
126
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
##
|
|
180
|
+
# @private
|
|
181
|
+
#
|
|
182
|
+
def initialize(name, out, err, status, exception)
|
|
183
|
+
@name = name
|
|
184
|
+
@captured_out = out
|
|
185
|
+
@captured_err = err
|
|
186
|
+
@status = status
|
|
187
|
+
@exception = exception
|
|
188
|
+
freeze
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|