exec_service 0.0.0 → 0.1.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.
@@ -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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ExecService
4
+ ##
5
+ # Version of the exec_service gem
6
+ # @return [String]
7
+ #
8
+ VERSION = "0.1.0"
9
+ end