childprocess 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.document +6 -0
  3. data/.gitignore +28 -0
  4. data/.rspec +1 -0
  5. data/.travis.yml +36 -0
  6. data/CHANGELOG.md +44 -0
  7. data/Gemfile +15 -0
  8. data/LICENSE +20 -0
  9. data/README.md +192 -0
  10. data/Rakefile +61 -0
  11. data/appveyor.yml +43 -0
  12. data/childprocess.gemspec +30 -0
  13. data/lib/childprocess.rb +205 -0
  14. data/lib/childprocess/abstract_io.rb +36 -0
  15. data/lib/childprocess/abstract_process.rb +192 -0
  16. data/lib/childprocess/errors.rb +26 -0
  17. data/lib/childprocess/jruby.rb +56 -0
  18. data/lib/childprocess/jruby/io.rb +16 -0
  19. data/lib/childprocess/jruby/process.rb +159 -0
  20. data/lib/childprocess/jruby/pump.rb +53 -0
  21. data/lib/childprocess/tools/generator.rb +146 -0
  22. data/lib/childprocess/unix.rb +9 -0
  23. data/lib/childprocess/unix/fork_exec_process.rb +70 -0
  24. data/lib/childprocess/unix/io.rb +21 -0
  25. data/lib/childprocess/unix/lib.rb +186 -0
  26. data/lib/childprocess/unix/platform/i386-linux.rb +12 -0
  27. data/lib/childprocess/unix/platform/i386-solaris.rb +11 -0
  28. data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -0
  29. data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -0
  30. data/lib/childprocess/unix/posix_spawn_process.rb +134 -0
  31. data/lib/childprocess/unix/process.rb +89 -0
  32. data/lib/childprocess/version.rb +3 -0
  33. data/lib/childprocess/windows.rb +33 -0
  34. data/lib/childprocess/windows/handle.rb +91 -0
  35. data/lib/childprocess/windows/io.rb +25 -0
  36. data/lib/childprocess/windows/lib.rb +416 -0
  37. data/lib/childprocess/windows/process.rb +130 -0
  38. data/lib/childprocess/windows/process_builder.rb +175 -0
  39. data/lib/childprocess/windows/structs.rb +149 -0
  40. data/spec/abstract_io_spec.rb +12 -0
  41. data/spec/childprocess_spec.rb +391 -0
  42. data/spec/io_spec.rb +228 -0
  43. data/spec/jruby_spec.rb +24 -0
  44. data/spec/pid_behavior.rb +12 -0
  45. data/spec/platform_detection_spec.rb +86 -0
  46. data/spec/spec_helper.rb +261 -0
  47. data/spec/unix_spec.rb +57 -0
  48. data/spec/windows_spec.rb +23 -0
  49. metadata +179 -0
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "childprocess/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "childprocess"
7
+ s.version = ChildProcess::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jari Bakken", "Eric Kessler"]
10
+ s.email = ["morrow748@gmail.com"]
11
+ s.homepage = "http://github.com/enkessler/childprocess"
12
+ s.summary = %q{A simple and reliable solution for controlling external programs running in the background on any Ruby / OS combination.}
13
+ s.description = %q{This gem aims at being a simple and reliable solution for controlling external programs running in the background on any Ruby / OS combination.}
14
+
15
+ s.rubyforge_project = "childprocess"
16
+ s.license = 'MIT'
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- spec/*`.split("\n")
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_runtime_dependency "ffi", "~> 1.0", ">= 1.0.11"
23
+
24
+ s.add_development_dependency "rspec", "~> 3.0"
25
+ s.add_development_dependency "yard", "~> 0.0"
26
+ s.add_development_dependency 'rake', '< 12.0'
27
+ s.add_development_dependency 'coveralls', '< 1.0'
28
+ end
29
+
30
+
@@ -0,0 +1,205 @@
1
+ require 'childprocess/version'
2
+ require 'childprocess/errors'
3
+ require 'childprocess/abstract_process'
4
+ require 'childprocess/abstract_io'
5
+ require "fcntl"
6
+ require 'logger'
7
+
8
+ module ChildProcess
9
+
10
+ @posix_spawn = false
11
+
12
+ class << self
13
+ attr_writer :logger
14
+
15
+ def new(*args)
16
+ case os
17
+ when :macosx, :linux, :solaris, :bsd, :cygwin, :aix
18
+ if posix_spawn?
19
+ Unix::PosixSpawnProcess.new(args)
20
+ elsif jruby?
21
+ JRuby::Process.new(args)
22
+ else
23
+ Unix::ForkExecProcess.new(args)
24
+ end
25
+ when :windows
26
+ Windows::Process.new(args)
27
+ else
28
+ raise Error, "unsupported platform #{platform_name.inspect}"
29
+ end
30
+ end
31
+ alias_method :build, :new
32
+
33
+ def logger
34
+ return @logger if defined?(@logger) and @logger
35
+
36
+ @logger = Logger.new($stderr)
37
+ @logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
38
+
39
+ @logger
40
+ end
41
+
42
+ def platform
43
+ if RUBY_PLATFORM == "java"
44
+ :jruby
45
+ elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == "ironruby"
46
+ :ironruby
47
+ else
48
+ os
49
+ end
50
+ end
51
+
52
+ def platform_name
53
+ @platform_name ||= "#{arch}-#{os}"
54
+ end
55
+
56
+ def unix?
57
+ !windows?
58
+ end
59
+
60
+ def linux?
61
+ os == :linux
62
+ end
63
+
64
+ def jruby?
65
+ platform == :jruby
66
+ end
67
+
68
+ def windows?
69
+ os == :windows
70
+ end
71
+
72
+ def posix_spawn?
73
+ enabled = @posix_spawn || %w[1 true].include?(ENV['CHILDPROCESS_POSIX_SPAWN'])
74
+ return false unless enabled
75
+
76
+ require 'ffi'
77
+ begin
78
+ require "childprocess/unix/platform/#{ChildProcess.platform_name}"
79
+ rescue LoadError
80
+ raise ChildProcess::MissingPlatformError
81
+ end
82
+
83
+ require "childprocess/unix/lib"
84
+ require 'childprocess/unix/posix_spawn_process'
85
+
86
+ true
87
+ rescue ChildProcess::MissingPlatformError => ex
88
+ warn_once ex.message
89
+ false
90
+ end
91
+
92
+ #
93
+ # Set this to true to enable experimental use of posix_spawn.
94
+ #
95
+
96
+ def posix_spawn=(bool)
97
+ @posix_spawn = bool
98
+ end
99
+
100
+ def os
101
+ @os ||= (
102
+ require "rbconfig"
103
+ host_os = RbConfig::CONFIG['host_os'].downcase
104
+
105
+ case host_os
106
+ when /linux/
107
+ :linux
108
+ when /darwin|mac os/
109
+ :macosx
110
+ when /mswin|msys|mingw32/
111
+ :windows
112
+ when /cygwin/
113
+ :cygwin
114
+ when /solaris|sunos/
115
+ :solaris
116
+ when /bsd/
117
+ :bsd
118
+ when /aix/
119
+ :aix
120
+ else
121
+ raise Error, "unknown os: #{host_os.inspect}"
122
+ end
123
+ )
124
+ end
125
+
126
+ def arch
127
+ @arch ||= (
128
+ host_cpu = RbConfig::CONFIG['host_cpu'].downcase
129
+ case host_cpu
130
+ when /i[3456]86/
131
+ if workaround_older_macosx_misreported_cpu?
132
+ # Workaround case: older 64-bit Darwin Rubies misreported as i686
133
+ "x86_64"
134
+ else
135
+ "i386"
136
+ end
137
+ when /amd64|x86_64/
138
+ "x86_64"
139
+ when /ppc|powerpc/
140
+ "powerpc"
141
+ else
142
+ host_cpu
143
+ end
144
+ )
145
+ end
146
+
147
+ #
148
+ # By default, a child process will inherit open file descriptors from the
149
+ # parent process. This helper provides a cross-platform way of making sure
150
+ # that doesn't happen for the given file/io.
151
+ #
152
+
153
+ def close_on_exec(file)
154
+ if file.respond_to?(:close_on_exec=)
155
+ file.close_on_exec = true
156
+ elsif file.respond_to?(:fcntl) && defined?(Fcntl::FD_CLOEXEC)
157
+ file.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
158
+
159
+ if jruby? && posix_spawn?
160
+ # on JRuby, the fcntl call above apparently isn't enough when
161
+ # we're launching the process through posix_spawn.
162
+ fileno = JRuby.posix_fileno_for(file)
163
+ Unix::Lib.fcntl fileno, Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
164
+ end
165
+ elsif windows?
166
+ Windows::Lib.dont_inherit file
167
+ else
168
+ raise Error, "not sure how to set close-on-exec for #{file.inspect} on #{platform_name.inspect}"
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def warn_once(msg)
175
+ @warnings ||= {}
176
+
177
+ unless @warnings[msg]
178
+ @warnings[msg] = true
179
+ logger.warn msg
180
+ end
181
+ end
182
+
183
+ # Workaround: detect the situation that an older Darwin Ruby is actually
184
+ # 64-bit, but is misreporting cpu as i686, which would imply 32-bit.
185
+ #
186
+ # @return [Boolean] `true` if:
187
+ # (a) on Mac OS X
188
+ # (b) actually running in 64-bit mode
189
+ def workaround_older_macosx_misreported_cpu?
190
+ os == :macosx && is_64_bit?
191
+ end
192
+
193
+ # @return [Boolean] `true` if this Ruby represents `1` in 64 bits (8 bytes).
194
+ def is_64_bit?
195
+ 1.size == 8
196
+ end
197
+
198
+ end # class << self
199
+ end # ChildProcess
200
+
201
+ require 'jruby' if ChildProcess.jruby?
202
+
203
+ require 'childprocess/unix' if ChildProcess.unix?
204
+ require 'childprocess/windows' if ChildProcess.windows?
205
+ require 'childprocess/jruby' if ChildProcess.jruby?
@@ -0,0 +1,36 @@
1
+ module ChildProcess
2
+ class AbstractIO
3
+ attr_reader :stderr, :stdout, :stdin
4
+
5
+ def inherit!
6
+ @stdout = STDOUT
7
+ @stderr = STDERR
8
+ end
9
+
10
+ def stderr=(io)
11
+ check_type io
12
+ @stderr = io
13
+ end
14
+
15
+ def stdout=(io)
16
+ check_type io
17
+ @stdout = io
18
+ end
19
+
20
+ #
21
+ # @api private
22
+ #
23
+
24
+ def _stdin=(io)
25
+ check_type io
26
+ @stdin = io
27
+ end
28
+
29
+ private
30
+
31
+ def check_type(io)
32
+ raise SubclassResponsibility, "check_type"
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,192 @@
1
+ module ChildProcess
2
+ class AbstractProcess
3
+ POLL_INTERVAL = 0.1
4
+
5
+ attr_reader :exit_code
6
+
7
+ #
8
+ # Set this to true if you do not care about when or if the process quits.
9
+ #
10
+ attr_accessor :detach
11
+
12
+ #
13
+ # Set this to true if you want to write to the process' stdin (process.io.stdin)
14
+ #
15
+ attr_accessor :duplex
16
+
17
+ #
18
+ # Modify the child's environment variables
19
+ #
20
+ attr_reader :environment
21
+
22
+ #
23
+ # Set the child's current working directory.
24
+ #
25
+ attr_accessor :cwd
26
+
27
+ #
28
+ # Set this to true to make the child process the leader of a new process group
29
+ #
30
+ # This can be used to make sure that all grandchildren are killed
31
+ # when the child process dies.
32
+ #
33
+ attr_accessor :leader
34
+
35
+ #
36
+ # Create a new process with the given args.
37
+ #
38
+ # @api private
39
+ # @see ChildProcess.build
40
+ #
41
+
42
+ def initialize(args)
43
+ unless args.all? { |e| e.kind_of?(String) }
44
+ raise ArgumentError, "all arguments must be String: #{args.inspect}"
45
+ end
46
+
47
+ @args = args
48
+ @started = false
49
+ @exit_code = nil
50
+ @io = nil
51
+ @cwd = nil
52
+ @detach = false
53
+ @duplex = false
54
+ @leader = false
55
+ @environment = {}
56
+ end
57
+
58
+ #
59
+ # Returns a ChildProcess::AbstractIO subclass to configure the child's IO streams.
60
+ #
61
+
62
+ def io
63
+ raise SubclassResponsibility, "io"
64
+ end
65
+
66
+ #
67
+ # @return [Integer] the pid of the process after it has started
68
+ #
69
+
70
+ def pid
71
+ raise SubclassResponsibility, "pid"
72
+ end
73
+
74
+ #
75
+ # Launch the child process
76
+ #
77
+ # @return [AbstractProcess] self
78
+ #
79
+
80
+ def start
81
+ launch_process
82
+ @started = true
83
+
84
+ self
85
+ end
86
+
87
+ #
88
+ # Forcibly terminate the process, using increasingly harsher methods if possible.
89
+ #
90
+ # @param [Integer] timeout (3) Seconds to wait before trying the next method.
91
+ #
92
+
93
+ def stop(timeout = 3)
94
+ raise SubclassResponsibility, "stop"
95
+ end
96
+
97
+ #
98
+ # Block until the process has been terminated.
99
+ #
100
+ # @return [Integer] The exit status of the process
101
+ #
102
+
103
+ def wait
104
+ raise SubclassResponsibility, "wait"
105
+ end
106
+
107
+ #
108
+ # Did the process exit?
109
+ #
110
+ # @return [Boolean]
111
+ #
112
+
113
+ def exited?
114
+ raise SubclassResponsibility, "exited?"
115
+ end
116
+
117
+ #
118
+ # Has the process started?
119
+ #
120
+ # @return [Boolean]
121
+ #
122
+
123
+ def started?
124
+ @started
125
+ end
126
+
127
+ #
128
+ # Is this process running?
129
+ #
130
+ # @return [Boolean]
131
+ #
132
+
133
+ def alive?
134
+ started? && !exited?
135
+ end
136
+
137
+ #
138
+ # Returns true if the process has exited and the exit code was not 0.
139
+ #
140
+ # @return [Boolean]
141
+ #
142
+
143
+ def crashed?
144
+ exited? && @exit_code != 0
145
+ end
146
+
147
+ #
148
+ # Wait for the process to exit, raising a ChildProcess::TimeoutError if
149
+ # the timeout expires.
150
+ #
151
+
152
+ def poll_for_exit(timeout)
153
+ log "polling #{timeout} seconds for exit"
154
+
155
+ end_time = Time.now + timeout
156
+ until (ok = exited?) || Time.now > end_time
157
+ sleep POLL_INTERVAL
158
+ end
159
+
160
+ unless ok
161
+ raise TimeoutError, "process still alive after #{timeout} seconds"
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def launch_process
168
+ raise SubclassResponsibility, "launch_process"
169
+ end
170
+
171
+ def detach?
172
+ @detach
173
+ end
174
+
175
+ def duplex?
176
+ @duplex
177
+ end
178
+
179
+ def leader?
180
+ @leader
181
+ end
182
+
183
+ def log(*args)
184
+ ChildProcess.logger.debug "#{self.inspect} : #{args.inspect}"
185
+ end
186
+
187
+ def assert_started
188
+ raise Error, "process not started" unless started?
189
+ end
190
+
191
+ end # AbstractProcess
192
+ end # ChildProcess