Skip to content

Commit 56ebcd9

Browse files
authored
multi: Updates to the exec utility (#460)
feat(core): The exec utility can now remove any existing bundle for a subprocess fix(core): Made the controller thread-safe fix(core): The result callback is now called in background mode if the result is never explicitly obtained from the controller
1 parent a2bceff commit 56ebcd9

File tree

5 files changed

+336
-142
lines changed

5 files changed

+336
-142
lines changed

toys-core/lib/toys/standard_mixins/exec.rb

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,36 +35,6 @@ module StandardMixins
3535
# but you can also retrieve the service object itself by calling
3636
# {Toys::Context#get} with the key {Toys::StandardMixins::Exec::KEY}.
3737
#
38-
# ### Controlling processes
39-
#
40-
# A process can be started in the *foreground* or the *background*. If you
41-
# start a foreground process, it will "take over" your standard input and
42-
# output streams by default, and it will keep control until it completes.
43-
# If you start a background process, its streams will be redirected to null
44-
# by default, and control will be returned to you immediately.
45-
#
46-
# While a process is running, you can control it using a
47-
# {Toys::Utils::Exec::Controller} object. Use a controller to interact with
48-
# the process's input and output streams, send it signals, or wait for it
49-
# to complete.
50-
#
51-
# When running a process in the foreground, the controller will be yielded
52-
# to an optional block. For example, the following code starts a process in
53-
# the foreground and passes its output stream to a controller.
54-
#
55-
# exec(["git", "init"], out: :controller) do |controller|
56-
# loop do
57-
# line = controller.out.gets
58-
# break if line.nil?
59-
# puts "Got line: #{line}"
60-
# end
61-
# end
62-
#
63-
# When running a process in the background, the controller is returned from
64-
# the method that starts the process:
65-
#
66-
# controller = exec(["git", "init"], background: true)
67-
#
6838
# ### Stream handling
6939
#
7040
# By default, subprocess streams are connected to the corresponding streams
@@ -84,7 +54,7 @@ module StandardMixins
8454
#
8555
# * **Inherit parent stream:** You can inherit the corresponding stream
8656
# in the parent process by passing `:inherit` as the option value. This
87-
# is the default if the subprocess is *not* run in the background.
57+
# is the default if the subprocess is run in the foreground.
8858
#
8959
# * **Redirect to null:** You can redirect to a null stream by passing
9060
# `:null` as the option value. This connects to a stream that is not
@@ -136,7 +106,7 @@ module StandardMixins
136106
# the setting `:controller`. You can then manipulate the stream via the
137107
# controller. If you pass a block to {Toys::StandardMixins::Exec#exec},
138108
# it yields the {Toys::Utils::Exec::Controller}, giving you access to
139-
# streams.
109+
# streams. See the section below on controlling processes.
140110
#
141111
# * **Make copies of an output stream:** You can "tee," or duplicate the
142112
# `:out` or `:err` stream and redirect those copies to various
@@ -156,6 +126,68 @@ module StandardMixins
156126
# the tee. Larger buffers may allow higher throughput. The default
157127
# is 65536.
158128
#
129+
# ### Controlling processes
130+
#
131+
# A process can be started in the *foreground* or the *background*. If you
132+
# start a foreground process, it will inherit your standard input and
133+
# output streams by default, and it will keep control until it completes.
134+
# If you start a background process, its streams will be redirected to null
135+
# by default, and control will be returned to you immediately.
136+
#
137+
# While a process is running, you can control it using a
138+
# {Toys::Utils::Exec::Controller} object. Use a controller to interact with
139+
# the process's input and output streams, send it signals, or wait for it
140+
# to complete.
141+
#
142+
# When running a process in the foreground, the controller will be yielded
143+
# to an optional block. For example, the following code starts a process in
144+
# the foreground and passes its output stream to a controller.
145+
#
146+
# exec(["git", "init"], out: :controller) do |controller|
147+
# loop do
148+
# line = controller.out.gets
149+
# break if line.nil?
150+
# puts "Got line: #{line}"
151+
# end
152+
# end
153+
#
154+
# At the end of the block, if the controller is handling the process's
155+
# input stream, that stream will automatically be closed. The following
156+
# example programmatically sends data to the `wc` unix program, and
157+
# captures its output. Because the controller is handling the input stream,
158+
# it automatically closes the stream at the end of the block, which causes
159+
# `wc` to end.
160+
#
161+
# result = exec(["wc"], in: :controller, out: :capture) do |controller|
162+
# controller.in.puts "Hello, world!"
163+
# end
164+
# puts "Results: #{result.captured_out}"
165+
#
166+
# Otherwise, depending on the process's behavior, it may continue to run
167+
# after the end of the block. Control will not be returned to the caller
168+
# until the process actually terminates. Conversely, it is also possible
169+
# the process could terminate by itself while the block is still executing.
170+
# You can call controller methods to obtain the process's actual current
171+
# state.
172+
#
173+
# When running a process in the background, the controller is returned
174+
# immediately from the method that starts the process. In the following
175+
# example, git init is kicked off in the background and the output is
176+
# thrown away to /dev/null.
177+
#
178+
# controller = exec(["git", "init"], background: true)
179+
#
180+
# In this mode, use the returned controller to query the process's state
181+
# and interact with it. Streams directed to the controller are not
182+
# automatically closed, so you will need to do so yourself. Following is an
183+
# example of running `wc` in the background:
184+
#
185+
# controller = exec(["wc"], background: true,
186+
# in: :controller, out: :controller)
187+
# controller.in.puts "Hello, world!"
188+
# controller.in.close # Do this explicitly to cause wc to finish
189+
# puts "Results: #{controller.out.read}" # Read the entire stream
190+
#
159191
# ### Result handling
160192
#
161193
# A subprocess result is represented by a {Toys::Utils::Exec::Result}
@@ -193,6 +225,12 @@ module StandardMixins
193225
# puts "exit code: #{result.exit_code}"
194226
# end
195227
#
228+
# In foreground mode, the callback is executed in the calling thread, after
229+
# the process terminates (and after any controller block has completed) but
230+
# before control is returned to the caller. In background mode, the
231+
# callback is executed asynchronously in a separate thread after the
232+
# process terminates.
233+
#
196234
# Finally, you can force your tool to exit if a subprocess fails, similar
197235
# to setting the `set -e` option in bash, by setting the
198236
# `:exit_on_nonzero_status` option. This is often set as a default
@@ -216,6 +254,10 @@ module StandardMixins
216254
#
217255
# * `:background` (Boolean) Runs the process in the background if `true`.
218256
#
257+
# * `:unbundle` (Boolean) Disables any existing bundle when running the
258+
# subprocess. Has no effect if Bundler isn't active at the call point.
259+
# Cannot be used when executing in a fork, e.g. via {#exec_proc}.
260+
#
219261
# Options related to handling results
220262
#
221263
# * `:result_callback` (Proc,Symbol) A procedure that is called, and
@@ -837,12 +879,7 @@ def self._setup_clean_process(cmd)
837879
context = self
838880
opts = Exec._setup_exec_opts(opts, context)
839881
context[KEY] = Utils::Exec.new(**opts) do |k|
840-
case k
841-
when :logger
842-
context[Context::Key::LOGGER]
843-
when :cli
844-
context[Context::Key::CLI]
845-
end
882+
k == :logger ? context[Context::Key::LOGGER] : nil
846883
end
847884
end
848885
end

0 commit comments

Comments
 (0)