├── .editorconfig ├── .github └── workflows │ ├── development.yml │ └── documentation.yml ├── .gitignore ├── .rspec ├── README.md ├── examples └── terminal.rb ├── gems.rb ├── lib └── process │ ├── group.rb │ └── group │ └── version.rb ├── process-group.gemspec ├── release.cert └── spec ├── process └── group │ ├── foreground_spec.rb │ ├── fork_spec.rb │ ├── interrupt_spec.rb │ ├── io_spec.rb │ ├── load_spec.rb │ ├── process_spec.rb │ ├── spawn_spec.rb │ └── wait_spec.rb └── spec_helper.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: Development 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: ${{matrix.ruby}} on ${{matrix.os}} 8 | runs-on: ${{matrix.os}}-latest 9 | continue-on-error: ${{matrix.experimental}} 10 | 11 | strategy: 12 | matrix: 13 | os: 14 | - ubuntu 15 | - macos 16 | 17 | ruby: 18 | - "2.6" 19 | - "2.7" 20 | - "3.0" 21 | 22 | experimental: [false] 23 | env: [""] 24 | 25 | include: 26 | - os: ubuntu 27 | ruby: truffleruby 28 | experimental: true 29 | - os: ubuntu 30 | ruby: jruby 31 | experimental: true 32 | - os: ubuntu 33 | ruby: head 34 | experimental: true 35 | 36 | steps: 37 | - uses: actions/checkout@v2 38 | - uses: ruby/setup-ruby@v1 39 | with: 40 | ruby-version: ${{matrix.ruby}} 41 | bundler-cache: true 42 | 43 | - name: Run tests 44 | timeout-minutes: 5 45 | run: ${{matrix.env}} bundle exec rspec 46 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | BUNDLE_WITH: maintenance 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: 3.1 21 | bundler-cache: true 22 | 23 | - name: Installing packages 24 | run: sudo apt-get install wget 25 | 26 | - name: Prepare GitHub Pages 27 | run: bundle exec bake github:pages:prepare --directory docs 28 | 29 | - name: Generate documentation 30 | timeout-minutes: 5 31 | run: bundle exec bake utopia:project:static --force no 32 | 33 | - name: Deploy GitHub Pages 34 | run: bundle exec bake github:pages:commit --directory docs 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /gems.locked 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --backtrace 3 | --warnings 4 | --require spec_helper 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Process::Group 2 | 3 | `Process::Group` allows for multiple fibers to run system processes concurrently with minimal overhead. 4 | 5 | [![Development Status](https://github.com/ioquatix/process-group/workflows/Development/badge.svg)](https://github.com/ioquatix/process-group/actions?workflow=Development) 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | gem 'process-group' 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install process-group 20 | 21 | ## Usage 22 | 23 | It is fairly straight forward to run multiple commands in parallel: 24 | 25 | ``` ruby 26 | Process::Group.wait do |group| 27 | group.run("ls", "-lah") {|status| puts status.inspect} 28 | group.run("echo", "Hello World") {|status| puts status.inspect} 29 | end 30 | ``` 31 | 32 | You can also run Ruby code in parallel with executed commands: 33 | 34 | ``` ruby 35 | # Create a new process group: 36 | Process::Group.wait do |group| 37 | # Run the command (non-blocking): 38 | group.run("sleep 1") do |exit_status| 39 | # Running in a separate fiber, will execute this code once the process completes: 40 | puts "Command finished with status: #{exit_status}" 41 | end 42 | 43 | # Do something else here: 44 | sleep(1) 45 | 46 | # Wait for all processes in group to finish. 47 | end 48 | ``` 49 | 50 | The `group.wait` call is an explicit synchronization point, and if it completes successfully, all processes/fibers have finished successfully. If an error is raised in a fiber, it will be passed back out through `group.wait` and this is the only failure condition. Even if this occurs, all children processes are guaranteed to be cleaned up. 51 | 52 | ### Explicit Fibers 53 | 54 | Items within a single fiber will execute sequentially. Processes (e.g. via `Group#spawn`) will run concurrently in multiple fibers. 55 | 56 | ``` ruby 57 | Process::Group.wait do |group| 58 | # Explicity manage concurrency in this fiber: 59 | Fiber.new do 60 | # These processes will be run sequentially: 61 | group.spawn("sleep 1") 62 | group.spawn("sleep 1") 63 | end.resume 64 | 65 | # Implicitly run this task concurrently as the above fiber: 66 | group.run("sleep 2") 67 | end 68 | ``` 69 | 70 | `Group#spawn` is theoretically identical to `Process#spawn` except the processes are run concurrently if possible. 71 | 72 | ### Explicit Wait 73 | 74 | The recommended approach to use process group is to call `Process::Group.wait` with a block which invokes tasks. This block is wrapped in appropriate `rescue Interrupt` and `ensure` blocks which guarantee that the process group is cleaned up: 75 | 76 | ``` ruby 77 | Process::Group.wait do |group| 78 | group.run("sleep 10") 79 | end 80 | ``` 81 | 82 | It is also possible to invoke this machinery and reuse the process group simply by instantiating the group and calling wait explicitly: 83 | 84 | ``` ruby 85 | group = Process::Group.new 86 | 87 | group.wait do 88 | group.run("sleep 10") 89 | end 90 | ``` 91 | 92 | It is also possible to queue tasks for execution outside the wait block. But by design, it's only possible to execute tasks within the wait block. Tasks added outside a wait block will be queued up for execution when `#wait` is invoked: 93 | 94 | ``` ruby 95 | group = Process::Group.new 96 | 97 | group.run("sleep 10") 98 | 99 | # Run command here: 100 | group.wait 101 | ``` 102 | 103 | ### Specify Options 104 | 105 | You can specify options to `Group#run` and `Group#spawn` just like `Process::spawn`: 106 | 107 | ``` ruby 108 | Process::Group.wait do |group| 109 | env = {'FOO' => 'BAR'} 110 | 111 | # Arguments are essentially the same as Process::spawn. 112 | group.run(env, "sleep 1", chdir: "/tmp") 113 | end 114 | ``` 115 | 116 | ### Process Limit 117 | 118 | The process group can be used as a way to spawn multiple processes, but sometimes you'd like to limit the number of parallel processes to something relating to the number of processors in the system. By default, there is no limit on the number of processes running concurrently. 119 | 120 | ``` ruby 121 | # limit based on the number of processors: 122 | require 'etc' 123 | group = Process::Group.new(limit: Etc.nprocessors) 124 | 125 | # hardcoded - set to n (8 < n < 32) and let the OS scheduler worry about it: 126 | group = Process::Group.new(limit: 32) 127 | 128 | # unlimited - default: 129 | group = Process::Group.new 130 | ``` 131 | 132 | ### Kill Group 133 | 134 | It is possible to send a signal (kill) to the entire process group: 135 | 136 | ``` ruby 137 | group.kill(:TERM) 138 | ``` 139 | 140 | If there are no running processes, this is a no-op (rather than an error). [Proper handling of SIGINT/SIGQUIT](http://www.cons.org/cracauer/sigint.html) explains how to use signals correctly. 141 | 142 | #### Handling Interrupts 143 | 144 | `Process::Group` transparently handles `Interrupt` when raised within a `Fiber`. If `Interrupt` is raised, all children processes will be sent `kill(:INT)` and we will wait for all children to complete, but without resuming the controlling fibers. If `Interrupt` is raised during this process, children will be sent `kill(:TERM)`. After calling `Interrupt`, the fibers will not be resumed. 145 | 146 | ### Process Timeout 147 | 148 | You can run a process group with a time limit by using a separate child process: 149 | 150 | ``` ruby 151 | group = Process::Group.new 152 | 153 | class Timeout < StandardError 154 | end 155 | 156 | Fiber.new do 157 | # Wait for 2 seconds, let other processes run: 158 | group.fork { sleep 2 } 159 | 160 | # If no other processes are running, we are done: 161 | Fiber.yield unless group.running? 162 | 163 | # Send SIGINT to currently running processes: 164 | group.kill(:INT) 165 | 166 | # Wait for 2 seconds, let other processes run: 167 | group.fork { sleep 2 } 168 | 169 | # If no other processes are running, we are done: 170 | Fiber.yield unless group.running? 171 | 172 | # Send SIGTERM to currently running processes: 173 | group.kill(:TERM) 174 | 175 | # Raise an Timeout exception which is based back out: 176 | raise Timeout 177 | end.resume 178 | 179 | # Run some other long task: 180 | group.run("sleep 10") 181 | 182 | # Wait for fiber to complete: 183 | begin 184 | group.wait 185 | rescue Timeout 186 | puts "Process group was terminated forcefully." 187 | end 188 | ``` 189 | 190 | ## Contributing 191 | 192 | 1. Fork it 193 | 2. Create your feature branch (`git checkout -b my-new-feature`) 194 | 3. Commit your changes (`git commit -am 'Add some feature'`) 195 | 4. Push to the branch (`git push origin my-new-feature`) 196 | 5. Create new Pull Request 197 | 198 | ## License 199 | 200 | Released under the MIT license. 201 | 202 | Copyright, 2014, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). 203 | 204 | Permission is hereby granted, free of charge, to any person obtaining a copy 205 | of this software and associated documentation files (the "Software"), to deal 206 | in the Software without restriction, including without limitation the rights 207 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 208 | copies of the Software, and to permit persons to whom the Software is 209 | furnished to do so, subject to the following conditions: 210 | 211 | The above copyright notice and this permission notice shall be included in 212 | all copies or substantial portions of the Software. 213 | 214 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 215 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 216 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 217 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 218 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 219 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 220 | THE SOFTWARE. 221 | -------------------------------------------------------------------------------- /examples/terminal.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Copyright, 2014, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative '../lib/process/group' 24 | 25 | group = Process::Group.new 26 | 27 | 5.times do 28 | Fiber.new do 29 | result = group.fork do 30 | begin 31 | sleep 1 while true 32 | rescue Interrupt 33 | puts "Interrupted in child #{Process.pid}" 34 | end 35 | end 36 | end.resume 37 | end 38 | 39 | begin 40 | group.wait 41 | rescue Interrupt 42 | puts "Interrupted in parent #{Process.pid}" 43 | end 44 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :maintenance, optional: true do 6 | gem "bake-modernize" 7 | gem "bake-gem" 8 | 9 | gem "bake-github-pages" 10 | gem "utopia-project" 11 | end 12 | -------------------------------------------------------------------------------- /lib/process/group.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'fiber' 22 | require 'process/terminal' 23 | 24 | module Process 25 | # A group of tasks which can be run asynchrnously using fibers. Someone must call Group#wait to ensure that all fibers eventually resume. 26 | class Group 27 | def self.wait(**options, &block) 28 | group = Group.new(**options) 29 | 30 | group.wait(&block) 31 | end 32 | 33 | class Command 34 | def initialize(foreground: false, **options) 35 | @options = options 36 | @foreground = foreground 37 | 38 | @fiber = Fiber.current 39 | @pid = nil 40 | end 41 | 42 | attr :options 43 | 44 | attr :pid 45 | 46 | def foreground? 47 | @foreground 48 | end 49 | 50 | def resume(*arguments) 51 | @fiber.resume(*arguments) 52 | end 53 | 54 | def kill(signal = :INT) 55 | Process.kill(signal, @pid) 56 | end 57 | end 58 | 59 | # Executes a command using Process.spawn with the given arguments and options. 60 | class Spawn < Command 61 | def initialize(arguments, **options) 62 | @arguments = arguments 63 | 64 | super(**options) 65 | end 66 | 67 | attr :arguments 68 | 69 | def call(**options) 70 | options = @options.merge(options) 71 | 72 | @pid = Process.spawn(*@arguments, **options) 73 | end 74 | end 75 | 76 | # Runs a given block using a forked process. 77 | class Fork < Command 78 | def initialize(block, **options) 79 | @block = block 80 | 81 | super(**options) 82 | end 83 | 84 | def call(**options) 85 | options = @options.merge(options) 86 | 87 | @pid = Process.fork(&@block) 88 | 89 | if options[:pgroup] == true 90 | # Establishes the child process as a process group leader: 91 | Process.setpgid(@pid, 0) 92 | elsif pgroup = options[:pgroup] 93 | # Set this process as part of the existing process group: 94 | Process.setpgid(@pid, pgroup) 95 | end 96 | 97 | return @pid 98 | end 99 | 100 | def resume(*arguments) 101 | @fiber.resume(*arguments) 102 | end 103 | end 104 | 105 | # Create a new process group. Can specify `limit:` which limits the maximum number of concurrent processes. 106 | def initialize(limit: nil, terminal: Terminal::Device.new?) 107 | raise ArgumentError.new("Limit must be nil (unlimited) or > 0") unless limit == nil or limit > 0 108 | 109 | @pid = Process.pid 110 | 111 | @terminal = terminal 112 | 113 | @queue = [] 114 | @limit = limit 115 | 116 | @running = {} 117 | @fiber = nil 118 | 119 | @pgid = nil 120 | 121 | # Whether we can actively schedule tasks or not: 122 | @waiting = false 123 | end 124 | 125 | # A table of currently running processes. 126 | attr :running 127 | 128 | # The maximum number of processes to run concurrently, or zero 129 | attr_accessor :limit 130 | 131 | # The id of the process group, only valid if processes are currently running. 132 | def id 133 | raise RuntimeError.new("No processes in group, no group id available.") if @running.size == 0 134 | 135 | -@pgid 136 | end 137 | 138 | def queued? 139 | @queue.size > 0 140 | end 141 | 142 | # Are there processes currently running? 143 | def running? 144 | @running.size > 0 145 | end 146 | 147 | # Run a process in a new fiber, arguments have same meaning as Process#spawn. 148 | def run(*arguments, **options) 149 | Fiber.new do 150 | exit_status = self.spawn(*arguments, **options) 151 | 152 | yield exit_status if block_given? 153 | end.resume 154 | end 155 | 156 | def async 157 | Fiber.new do 158 | yield self 159 | end.resume 160 | end 161 | 162 | # Run a specific command as a child process. 163 | def spawn(*arguments, **options) 164 | append! Spawn.new(arguments, **options) 165 | end 166 | 167 | # Fork a block as a child process. 168 | def fork(**options, &block) 169 | append! Fork.new(block, **options) 170 | end 171 | 172 | # Whether or not #spawn, #fork or #run can be scheduled immediately. 173 | def available? 174 | if @limit 175 | @running.size < @limit 176 | else 177 | true 178 | end 179 | end 180 | 181 | # Whether or not calling #spawn, #fork or #run would block the caller fiber (i.e. call Fiber.yield). 182 | def blocking? 183 | not available? 184 | end 185 | 186 | # Wait for all running and queued processes to finish. If you provide a block, it will be invoked before waiting, but within canonical signal handling machinery. 187 | def wait 188 | raise ArgumentError.new("Cannot call Process::Group#wait from child process!") unless @pid == Process.pid 189 | 190 | waiting do 191 | yield(self) if block_given? 192 | 193 | while running? 194 | process, status = wait_one 195 | 196 | schedule! 197 | 198 | process.resume(status) 199 | end 200 | end 201 | 202 | # No processes, process group is no longer valid: 203 | @pgid = nil 204 | 205 | return self 206 | rescue Interrupt 207 | # If the user interrupts the wait, interrupt the process group and wait for them to finish: 208 | self.kill(:INT) 209 | 210 | # If user presses Ctrl-C again (or something else goes wrong), we will come out and kill(:TERM) in the ensure below: 211 | wait_all 212 | 213 | raise 214 | ensure 215 | # You'd only get here with running processes if some unexpected error was thrown in user code: 216 | begin 217 | self.kill(:TERM) 218 | rescue Errno::EPERM 219 | # Sometimes, `kill` code can give EPERM, if any signal couldn't be delivered to a child. This might occur if an exception is thrown in the user code (e.g. within the fiber), and there are other zombie processes which haven't been reaped yet. These should be dealt with below, so it shouldn't be an issue to ignore this condition. 220 | end 221 | 222 | # Clean up zombie processes - if user presses Ctrl-C or for some reason something else blows up, exception would propagate back to caller: 223 | wait_all 224 | end 225 | 226 | # Send a signal to all currently running processes. No-op unless #running? 227 | def kill(signal = :INT) 228 | if running? 229 | Process.kill(signal, id) 230 | end 231 | end 232 | 233 | def to_s 234 | "#<#{self.class} running=#{@running.size} queued=#{@queue.size} limit=#{@limit} pgid=#{@pgid}>" 235 | end 236 | 237 | private 238 | 239 | # The waiting loop, schedule any outstanding tasks: 240 | def waiting 241 | @waiting = true 242 | 243 | # Schedule any queued tasks: 244 | schedule! 245 | 246 | yield 247 | ensure 248 | @waiting = false 249 | end 250 | 251 | def waiting? 252 | @waiting 253 | end 254 | 255 | # Append a process to the queue and schedule it for execution if possible. 256 | def append!(process) 257 | @queue << process 258 | 259 | schedule! if waiting? 260 | 261 | Fiber.yield 262 | end 263 | 264 | # Run any processes while space is available in the group. 265 | def schedule! 266 | while available? and @queue.size > 0 267 | process = @queue.shift 268 | 269 | if @running.size == 0 270 | pid = process.call(:pgroup => true) 271 | 272 | # The process group id is the pid of the first process: 273 | @pgid = pid 274 | else 275 | pid = process.call(:pgroup => @pgid) 276 | end 277 | 278 | if @terminal and process.foreground? 279 | @terminal.foreground = pid 280 | end 281 | 282 | @running[pid] = process 283 | end 284 | end 285 | 286 | # Wait for all children to exit but without resuming any controlling fibers. 287 | def wait_all 288 | wait_one while running? 289 | 290 | # Clear any queued tasks: 291 | @queue.clear 292 | end 293 | 294 | # Wait for one process, should only be called when a child process has finished, otherwise would block. 295 | def wait_one(flags = 0) 296 | raise RuntimeError.new("Process group has no running children!") unless running? 297 | 298 | # Wait for processes in this group: 299 | pid, status = Process.wait2(-@pgid, flags) 300 | 301 | return if flags & Process::WNOHANG and pid == nil 302 | 303 | process = @running.delete(pid) 304 | 305 | # This should never happen unless something very odd has happened: 306 | raise RuntimeError.new("Process id=#{pid} is not part of group!") unless process 307 | 308 | return process, status 309 | end 310 | end 311 | end 312 | -------------------------------------------------------------------------------- /lib/process/group/version.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2014, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module Process 22 | class Group 23 | VERSION = "1.2.4" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /process-group.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/process/group/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "process-group" 7 | spec.version = Process::Group::VERSION 8 | 9 | spec.summary = "Run and manage multiple processes in separate fibers with predictable behaviour." 10 | spec.authors = ["Samuel Williams", "Dustin Zeisler", "Olle Jonsson"] 11 | spec.license = "MIT" 12 | 13 | spec.cert_chain = ['release.cert'] 14 | spec.signing_key = File.expand_path('~/.gem/release.pem') 15 | 16 | spec.homepage = "https://github.com/ioquatix/process-group" 17 | 18 | spec.metadata = { 19 | "funding_uri" => "https://github.com/sponsors/ioquatix/", 20 | } 21 | 22 | spec.files = Dir.glob('{lib}/**/*', File::FNM_DOTMATCH, base: __dir__) 23 | 24 | spec.required_ruby_version = ">= 2.0" 25 | 26 | spec.add_dependency "process-terminal", "~> 0.2.0" 27 | 28 | spec.add_development_dependency "bundler" 29 | spec.add_development_dependency "covered" 30 | spec.add_development_dependency "rspec", "~> 3.9.0" 31 | end 32 | -------------------------------------------------------------------------------- /release.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEhDCCAuygAwIBAgIBATANBgkqhkiG9w0BAQsFADA3MTUwMwYDVQQDDCxzYW11 3 | ZWwud2lsbGlhbXMvREM9b3Jpb250cmFuc2Zlci9EQz1jby9EQz1uejAeFw0yMTA4 4 | MTYwNjMzNDRaFw0yMjA4MTYwNjMzNDRaMDcxNTAzBgNVBAMMLHNhbXVlbC53aWxs 5 | aWFtcy9EQz1vcmlvbnRyYW5zZmVyL0RDPWNvL0RDPW56MIIBojANBgkqhkiG9w0B 6 | AQEFAAOCAY8AMIIBigKCAYEAyXLSS/cw+fXJ5e7hi+U/TeChPWeYdwJojDsFY1xr 7 | xvtqbTTL8gbLHz5LW3QD2nfwCv3qTlw0qI3Ie7a9VMJMbSvgVEGEfQirqIgJXWMj 8 | eNMDgKsMJtC7u/43abRKx7TCURW3iWyR19NRngsJJmaR51yGGGm2Kfsr+JtKKLtL 9 | L188Wm3f13KAx7QJU8qyuBnj1/gWem076hzdA7xi1DbrZrch9GCRz62xymJlrJHn 10 | 9iZEZ7AxrS7vokhMlzSr/XMUihx/8aFKtk+tMLClqxZSmBWIErWdicCGTULXCBNb 11 | E/mljo4zEVKhlTWpJklMIhr55ZRrSarKFuW7en0+tpJrfsYiAmXMJNi4XAYJH7uL 12 | rgJuJwSaa/dMz+VmUoo7VKtSfCoOI+6v5/z0sK3oT6sG6ZwyI47DBq2XqNC6tnAj 13 | w+XmCywiTQrFzMMAvcA7rPI4F0nU1rZId51rOvvfxaONp+wgTi4P8owZLw0/j0m4 14 | 8C20DYi6EYx4AHDXiLpElWh3AgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8E 15 | BAMCBLAwHQYDVR0OBBYEFB6ZaeWKxQjGTI+pmz7cKRmMIywwMC4GA1UdEQQnMCWB 16 | I3NhbXVlbC53aWxsaWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWB 17 | I3NhbXVlbC53aWxsaWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEB 18 | CwUAA4IBgQBVoM+pu3dpdUhZM1w051iw5GfiqclAr1Psypf16Tiod/ho//4oAu6T 19 | 9fj3DPX/acWV9P/FScvqo4Qgv6g4VWO5ZU7z2JmPoTXZtYMunRAmQPFL/gSUc6aK 20 | vszMHIyhtyzRc6DnfW2AiVOjMBjaYv8xXZc9bduniRVPrLR4J7ozmGLh4o4uJp7w 21 | x9KCFaR8Lvn/r0oJWJOqb/DMAYI83YeN2Dlt3jpwrsmsONrtC5S3gOUle5afSGos 22 | bYt5ocnEpKSomR9ZtnCGljds/aeO1Xgpn2r9HHcjwnH346iNrnHmMlC7BtHUFPDg 23 | Ts92S47PTOXzwPBDsrFiq3VLbRjHSwf8rpqybQBH9MfzxGGxTaETQYOd6b4e4Ag6 24 | y92abGna0bmIEb4+Tx9rQ10Uijh1POzvr/VTH4bbIPy9FbKrRsIQ24qDbNJRtOpE 25 | RAOsIl+HOBTb252nx1kIRN5hqQx272AJCbCjKx8egcUQKffFVVCI0nye09v5CK+a 26 | HiLJ8VOFx6w= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /spec/process/group/foreground_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2015, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/group' 22 | 23 | RSpec.describe Process::Group do 24 | it "can run a process in the foreground" do 25 | output, input = IO.pipe 26 | 27 | subject.wait do 28 | subject.run("irb", foreground: true, in: output) 29 | 30 | input.puts("exit") 31 | input.close 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/process/group/fork_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/group' 22 | 23 | RSpec.describe Process::Group do 24 | it "should fork and write to pipe" do 25 | input, output = IO.pipe 26 | 27 | Fiber.new do 28 | result = subject.fork do 29 | output.puts "Hello World" 30 | 31 | exit(1) 32 | end 33 | 34 | # We need to close output so that input.read will encounter end of stream. 35 | output.close 36 | 37 | expect(result.exitstatus).to be == 1 38 | end.resume 39 | 40 | subject.wait 41 | 42 | expect(input.read).to be == "Hello World\n" 43 | end 44 | 45 | it "should not throw interrupt from fork" do 46 | Fiber.new do 47 | result = subject.fork do 48 | # Don't print out a backtrace when Ruby invariably exits due to the execption below: 49 | $stderr.reopen('/dev/null', 'w') 50 | 51 | raise Interrupt 52 | end 53 | 54 | expect(result.exitstatus).not_to be == 0 55 | end.resume 56 | 57 | # Shouldn't raise any errors: 58 | subject.wait 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/process/group/interrupt_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/group' 22 | 23 | RSpec.describe Process::Group do 24 | it "should raise interrupt exception" do 25 | checkpoint = "" 26 | 27 | Fiber.new do 28 | checkpoint += 'X' 29 | 30 | result = subject.fork { sleep 0.1 } 31 | 32 | expect(result).to be == 0 33 | 34 | checkpoint += 'Y' 35 | 36 | # Simulate the user pressing Ctrl-C after a short time: 37 | raise Interrupt 38 | end.resume 39 | 40 | Fiber.new do 41 | checkpoint += 'A' 42 | 43 | # This never returns: 44 | subject.fork do 45 | # We do this to exit immediately.. otherwise Ruby will print a backtrace and that's a bit confusing. 46 | trap(:INT) { exit!(0) } 47 | sleep(0.2) 48 | end 49 | 50 | checkpoint += 'B' 51 | end.resume 52 | 53 | expect(subject).to receive(:kill).with(:INT).once.and_call_original 54 | expect(subject).to receive(:kill).with(:TERM).once.and_call_original 55 | 56 | expect do 57 | subject.wait 58 | end.to raise_error(Interrupt) 59 | 60 | expect(checkpoint).to be == 'XAY' 61 | end 62 | 63 | it "should raise an exception" do 64 | checkpoint = "" 65 | 66 | Fiber.new do 67 | checkpoint += 'X' 68 | 69 | result = subject.fork { sleep 0.1 } 70 | expect(result).to be == 0 71 | 72 | checkpoint += 'Y' 73 | 74 | # Raises a RuntimeError 75 | fail "Error" 76 | end.resume 77 | 78 | Fiber.new do 79 | checkpoint += 'A' 80 | 81 | # This never returns: 82 | subject.fork { sleep 0.2 } 83 | 84 | checkpoint += 'B' 85 | end.resume 86 | 87 | expect do 88 | expect(subject).to receive(:kill).with(:TERM).once 89 | 90 | subject.wait 91 | end.to raise_error(RuntimeError) 92 | 93 | expect(checkpoint).to be == 'XAY' 94 | end 95 | 96 | it "should pass back out exceptions" do 97 | checkpoint = "" 98 | 99 | Fiber.new do 100 | # Wait for 2 seconds, let other processes run: 101 | subject.fork { sleep 2 } 102 | checkpoint += 'A' 103 | #puts "Finished waiting #1..." 104 | 105 | # If no other processes are running, we are done: 106 | Fiber.yield unless subject.running? 107 | checkpoint += 'B' 108 | #puts "Sending SIGINT..." 109 | 110 | # Send SIGINT to currently running processes: 111 | subject.kill(:INT) 112 | 113 | # Wait for 2 seconds, let other processes run: 114 | subject.fork { sleep 2 } 115 | checkpoint += 'C' 116 | #puts "Finished waiting #2..." 117 | 118 | # If no other processes are running, we are done: 119 | Fiber.yield unless subject.running? 120 | checkpoint += 'D' 121 | #puts "Sending SIGTERM..." 122 | 123 | # Send SIGTERM to currently running processes: 124 | subject.kill(:TERM) 125 | 126 | # Raise an Timeout exception which is pased back out: 127 | raise StandardError.new("Should never get here!") 128 | end.resume 129 | 130 | # Run some other long task: 131 | subject.run("sleep 10") 132 | 133 | start_time = Time.now 134 | 135 | # Wait for fiber to complete: 136 | expect do 137 | subject.wait 138 | checkpoint += 'E' 139 | end.not_to raise_error 140 | 141 | end_time = Time.now 142 | 143 | expect(checkpoint).to be == 'ABCE' 144 | expect(end_time - start_time).to be_within(0.2).of 4.0 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /spec/process/group/io_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/group' 22 | 23 | RSpec.describe Process::Group do 24 | it "should read line on separate thread" do 25 | input, output = IO.pipe 26 | 27 | Fiber.new do 28 | result = subject.fork do 29 | 3.times do 30 | output.puts "Hello World" 31 | sleep 0.1 32 | end 33 | 34 | exit(0) 35 | end 36 | 37 | output.close 38 | 39 | expect(result).to be == 0 40 | end.resume 41 | 42 | lines = nil 43 | io_thread = Thread.new do 44 | lines = input.read 45 | end 46 | 47 | subject.wait 48 | 49 | io_thread.join 50 | expect(lines).to be == ("Hello World\n" * 3) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/process/group/load_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2015, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/group' 22 | 23 | RSpec.describe Process::Group.new(limit: 5) do 24 | it "should only run a limited number of processes" do 25 | expect(subject.available?).to be_truthy 26 | expect(subject.blocking?).to be_falsey 27 | 28 | 5.times do 29 | Fiber.new do 30 | result = subject.fork do 31 | exit(0) 32 | end 33 | 34 | expect(result.exitstatus).to be == 0 35 | end.resume 36 | end 37 | 38 | subject.wait do 39 | expect(subject.blocking?).to be_truthy 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/process/group/process_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2015, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | RSpec.describe Process do 22 | it "default fork exit status should be 0" do 23 | pid = fork do 24 | end 25 | 26 | Process.waitpid(pid) 27 | 28 | expect($?.exitstatus).to be == 0 29 | end 30 | 31 | it "should fork and return exit status correctly" do 32 | pid = fork do 33 | exit(1) 34 | end 35 | 36 | Process.waitpid(pid) 37 | 38 | expect($?.exitstatus).to be == 1 39 | end 40 | 41 | # This is currently broken on Rubinius. 42 | it "should be okay to use fork within a fiber" do 43 | pid = nil 44 | 45 | Fiber.new do 46 | pid = fork do 47 | exit(2) 48 | end 49 | end.resume 50 | 51 | Process.waitpid(pid) 52 | 53 | expect($?.exitstatus).to be == 2 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/process/group/spawn_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/group' 22 | 23 | RSpec.describe Process::Group do 24 | it "should execute fibers concurrently" do 25 | start_time = Time.now 26 | 27 | Fiber.new do 28 | result = subject.fork { sleep 1.0 } 29 | 30 | expect(result).to be == 0 31 | end.resume 32 | 33 | Fiber.new do 34 | result = subject.fork { sleep 2.0 } 35 | 36 | expect(result).to be == 0 37 | end.resume 38 | 39 | subject.wait 40 | 41 | end_time = Time.now 42 | 43 | # Check that the execution time was roughly 2 seconds: 44 | expect(end_time - start_time).to be_within(0.2).of(2.0) 45 | end 46 | 47 | it "should kill commands" do 48 | start_time = Time.now 49 | 50 | subject.run("sleep 1") do |exit_status| 51 | expect(exit_status).to_not be 0 52 | end 53 | 54 | subject.run("sleep 2") do |exit_status| 55 | expect(exit_status).to_not be 0 56 | end 57 | 58 | subject.wait do 59 | subject.kill(:KILL) 60 | end 61 | 62 | end_time = Time.now 63 | 64 | # Check that processes killed almost immediately: 65 | expect(end_time - start_time).to be < 0.2 66 | end 67 | 68 | it "should pass environment to child process" do 69 | env = {'FOO' => 'BAR'} 70 | 71 | # Make a pipe to receive output from child process: 72 | input, output = IO.pipe 73 | 74 | subject.run(env, "echo $FOO", out: output) do |exit_status| 75 | output.close 76 | end 77 | 78 | subject.wait 79 | 80 | expect(input.read).to be == "BAR\n" 81 | end 82 | 83 | it "should yield exit status" do 84 | start_time = Time.now 85 | 86 | subject.run("sleep 1") 87 | 88 | subject.run("sleep 1") do |exit_status| 89 | expect(exit_status).to be == 0 90 | end 91 | 92 | subject.wait 93 | 94 | end_time = Time.now 95 | 96 | # Check that the execution time was roughly 1 second: 97 | expect(end_time - start_time).to be_within(0.2).of(1.0) 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/process/group/wait_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'process/group' 22 | 23 | RSpec.describe Process::Group do 24 | it "should invoke child task normally" do 25 | child_exit_status = nil 26 | 27 | subject.wait do 28 | subject.run("exit 0") do |exit_status| 29 | child_exit_status = exit_status 30 | end 31 | end 32 | 33 | expect(child_exit_status).to be == 0 34 | end 35 | 36 | it "should kill child task if process is interrupted" do 37 | child_exit_status = nil 38 | 39 | expect do 40 | subject.wait do 41 | subject.run("sleep 10") do |exit_status| 42 | child_exit_status = exit_status 43 | end 44 | 45 | # Simulate the parent (controlling) process receiving an interrupt. 46 | raise Interrupt 47 | end 48 | end.to raise_error(Interrupt) 49 | 50 | expect(child_exit_status).to_not be == 0 51 | end 52 | 53 | it "should propagate Interrupt" do 54 | expect(Process::Group).to receive(:new).once.and_call_original 55 | 56 | expect do 57 | Process::Group.wait do |group| 58 | raise Interrupt 59 | end 60 | end.to raise_error(Interrupt) 61 | end 62 | 63 | it "should clear queue after wait" do 64 | subject.limit = 1 65 | 66 | subject.run("sleep 10") 67 | subject.run("sleep 10") 68 | 69 | expect(subject.running?).to be_falsey 70 | expect(subject.queued?).to be_truthy 71 | 72 | expect do 73 | subject.wait do 74 | raise Interrupt 75 | end 76 | end.to raise_error(Interrupt) 77 | 78 | expect(subject.running?).to be_falsey 79 | expect(subject.queued?).to be_falsey 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2019, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require "bundler/setup" 22 | require 'covered/rspec' 23 | 24 | RSpec.configure do |config| 25 | # Enable flags like --only-failures and --next-failure 26 | config.example_status_persistence_file_path = ".rspec_status" 27 | 28 | # Disable RSpec exposing methods globally on `Module` and `main` 29 | config.disable_monkey_patching! 30 | 31 | config.expect_with :rspec do |c| 32 | c.syntax = :expect 33 | end 34 | end 35 | --------------------------------------------------------------------------------