childprocess 0.0.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.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +43 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/childprocess.rb +33 -0
- data/lib/childprocess/ironruby.rb +6 -0
- data/lib/childprocess/ironruby/process.rb +7 -0
- data/lib/childprocess/jruby.rb +6 -0
- data/lib/childprocess/jruby/process.rb +47 -0
- data/lib/childprocess/unix.rb +6 -0
- data/lib/childprocess/unix/process.rb +77 -0
- data/lib/childprocess/windows.rb +19 -0
- data/lib/childprocess/windows/api.rb +48 -0
- data/lib/childprocess/windows/constants.rb +27 -0
- data/lib/childprocess/windows/functions.rb +139 -0
- data/lib/childprocess/windows/handle.rb +87 -0
- data/lib/childprocess/windows/process.rb +42 -0
- data/lib/childprocess/windows/structs.rb +59 -0
- data/spec/childprocess_spec.rb +35 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +48 -0
- metadata +125 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Jari Bakken
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
= childprocess
|
2
|
+
|
3
|
+
This gem aims at being a simple and reliable solution for controlling
|
4
|
+
external programs running in the background on any Ruby / OS combination.
|
5
|
+
|
6
|
+
The code originated in the selenium-webdriver gem, but should prove useful as
|
7
|
+
a standalone library.
|
8
|
+
|
9
|
+
== Usage
|
10
|
+
|
11
|
+
process = ChildProcess.build("ruby", "-e", "sleep").start
|
12
|
+
|
13
|
+
process.started? #=> true
|
14
|
+
process.alive? #=> true
|
15
|
+
process.exited? #=> false
|
16
|
+
|
17
|
+
process.stop
|
18
|
+
|
19
|
+
The object returned from ChildProcess.build will implement ChildProcess::AbstractProcess.
|
20
|
+
|
21
|
+
== Implementation
|
22
|
+
|
23
|
+
How the process is launched and killed depends on the platform:
|
24
|
+
|
25
|
+
* Unix : fork + exec
|
26
|
+
* Windows : CreateProcess and friends
|
27
|
+
* JRuby : java.lang.{Process,ProcessBuilder}
|
28
|
+
* IronRuby : System.Diagnostics.Process
|
29
|
+
|
30
|
+
|
31
|
+
== Note on Patches/Pull Requests
|
32
|
+
|
33
|
+
* Fork the project.
|
34
|
+
* Make your feature addition or bug fix.
|
35
|
+
* Add tests for it. This is important so I don't break it in a
|
36
|
+
future version unintentionally.
|
37
|
+
* Commit, do not mess with rakefile, version, or history.
|
38
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
39
|
+
* Send me a pull request. Bonus points for topic branches.
|
40
|
+
|
41
|
+
== Copyright
|
42
|
+
|
43
|
+
Copyright (c) 2010 Jari Bakken. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "childprocess"
|
8
|
+
gem.summary = %Q{Cross-platform ruby library for managing child processes.}
|
9
|
+
gem.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.}
|
10
|
+
gem.email = "jari.bakken@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/jarib/childprocess"
|
12
|
+
gem.authors = ["Jari Bakken"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
gem.add_development_dependency "yard", ">= 0"
|
15
|
+
|
16
|
+
gem.add_dependency "ffi", "~> 0.6.3"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'spec/rake/spectask'
|
25
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
end
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
33
|
+
spec.rcov = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :spec => :check_dependencies
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
begin
|
41
|
+
require 'yard'
|
42
|
+
YARD::Rake::YardocTask.new
|
43
|
+
rescue LoadError
|
44
|
+
task :yardoc do
|
45
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
46
|
+
end
|
47
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/childprocess.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'childprocess/abstract_process'
|
2
|
+
require 'childprocess/errors'
|
3
|
+
|
4
|
+
module ChildProcess
|
5
|
+
autoload :Unix, 'childprocess/unix'
|
6
|
+
autoload :Windows, 'childprocess/windows'
|
7
|
+
autoload :JRuby, 'childprocess/jruby'
|
8
|
+
autoload :IronRuby, 'childprocess/ironruby'
|
9
|
+
|
10
|
+
def self.build(*args)
|
11
|
+
case platform
|
12
|
+
when :jruby
|
13
|
+
JRuby::Process.new(args)
|
14
|
+
when :ironruby
|
15
|
+
IronRuby::Process.new(args)
|
16
|
+
when :windows
|
17
|
+
Windows::Process.new(args)
|
18
|
+
else
|
19
|
+
Unix::Process.new(args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.platform
|
24
|
+
if RUBY_PLATFORM == "java"
|
25
|
+
:jruby
|
26
|
+
elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == "ironruby"
|
27
|
+
:ironruby
|
28
|
+
elsif RUBY_PLATFORM =~ /mswin|msys|mingw32/
|
29
|
+
:windows
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "java"
|
2
|
+
|
3
|
+
module ChildProcess
|
4
|
+
module JRuby
|
5
|
+
class Process < AbstractProcess
|
6
|
+
|
7
|
+
def exited?
|
8
|
+
return true if @exit_code
|
9
|
+
|
10
|
+
assert_started
|
11
|
+
@exit_code = @process.exitValue
|
12
|
+
rescue java.lang.IllegalThreadStateException
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop
|
17
|
+
assert_started
|
18
|
+
|
19
|
+
@process.destroy
|
20
|
+
@process.waitFor
|
21
|
+
|
22
|
+
@exit_code = @process.exitValue
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def launch_process
|
28
|
+
pb = java.lang.ProcessBuilder.new(@args)
|
29
|
+
|
30
|
+
# not sure why this is necessary
|
31
|
+
env = pb.environment
|
32
|
+
ENV.each { |k,v| env.put(k, v) }
|
33
|
+
|
34
|
+
@process = pb.start
|
35
|
+
|
36
|
+
# Firefox 3.6 on Snow Leopard has a lot output on stderr, which makes
|
37
|
+
# the launch act funny if we don't do something to the streams
|
38
|
+
# Closing the streams solves the problem for now, but on other platforms
|
39
|
+
# we might need to actually read them.
|
40
|
+
|
41
|
+
@process.getErrorStream.close
|
42
|
+
@process.getInputStream.close
|
43
|
+
end
|
44
|
+
|
45
|
+
end # Process
|
46
|
+
end # JRuby
|
47
|
+
end # ChildProcess
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module ChildProcess
|
2
|
+
module Unix
|
3
|
+
class Process < AbstractProcess
|
4
|
+
|
5
|
+
def stop(timeout = 3)
|
6
|
+
assert_started
|
7
|
+
send_term
|
8
|
+
|
9
|
+
begin
|
10
|
+
return poll_for_exit(timeout)
|
11
|
+
rescue TimeoutError
|
12
|
+
# try next
|
13
|
+
end
|
14
|
+
|
15
|
+
send_kill
|
16
|
+
wait
|
17
|
+
rescue Errno::ECHILD
|
18
|
+
# that'll do
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Did the process exit?
|
24
|
+
#
|
25
|
+
# @return [Boolean]
|
26
|
+
#
|
27
|
+
|
28
|
+
def exited?
|
29
|
+
return true if @exit_code
|
30
|
+
|
31
|
+
|
32
|
+
assert_started
|
33
|
+
pid, status = ::Process.waitpid2(@pid, ::Process::WNOHANG)
|
34
|
+
|
35
|
+
log(pid, status)
|
36
|
+
|
37
|
+
if pid
|
38
|
+
@exit_code = status.exitstatus || status.termsig
|
39
|
+
end
|
40
|
+
|
41
|
+
!!pid
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def wait
|
47
|
+
@exit_code = ::Process.waitpid @pid
|
48
|
+
end
|
49
|
+
|
50
|
+
def send_term
|
51
|
+
send_signal 'TERM'
|
52
|
+
end
|
53
|
+
|
54
|
+
def send_kill
|
55
|
+
send_signal 'KILL'
|
56
|
+
end
|
57
|
+
|
58
|
+
def send_signal(sig)
|
59
|
+
assert_started
|
60
|
+
|
61
|
+
log "sending #{sig}"
|
62
|
+
::Process.kill sig, @pid
|
63
|
+
end
|
64
|
+
|
65
|
+
def launch_process
|
66
|
+
@pid = fork {
|
67
|
+
unless $DEBUG
|
68
|
+
[STDOUT, STDERR].each { |io| io.reopen("/dev/null") }
|
69
|
+
end
|
70
|
+
|
71
|
+
exec(*@args)
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
end # Process
|
76
|
+
end # Unix
|
77
|
+
end # ChildProcess
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "ffi"
|
2
|
+
|
3
|
+
module ChildProcess
|
4
|
+
module Windows
|
5
|
+
module Lib
|
6
|
+
extend FFI::Library
|
7
|
+
|
8
|
+
ffi_lib "kernel32"
|
9
|
+
ffi_convention :stdcall
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require "childprocess/windows/constants"
|
15
|
+
require "childprocess/windows/structs"
|
16
|
+
require "childprocess/windows/functions"
|
17
|
+
require "childprocess/windows/handle"
|
18
|
+
require "childprocess/windows/api"
|
19
|
+
require "childprocess/windows/process"
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ChildProcess
|
2
|
+
module Windows
|
3
|
+
class << self
|
4
|
+
def kill(signal, *pids)
|
5
|
+
case signal
|
6
|
+
when 'SIGINT', 'INT', :SIGINT, :INT
|
7
|
+
signal = WIN_SIGINT
|
8
|
+
when 'SIGBRK', 'BRK', :SIGBREAK, :BRK
|
9
|
+
signal = WIN_SIGBREAK
|
10
|
+
when 'SIGKILL', 'KILL', :SIGKILL, :KILL
|
11
|
+
signal = WIN_SIGKILL
|
12
|
+
when 0..9
|
13
|
+
# Do nothing
|
14
|
+
else
|
15
|
+
raise Error, "invalid signal #{signal.inspect}"
|
16
|
+
end
|
17
|
+
|
18
|
+
pids.map { |pid| pid if send_signal(signal, pid) }.compact
|
19
|
+
end
|
20
|
+
|
21
|
+
def waitpid(pid, flags = 0)
|
22
|
+
wait_for_pid(pid, no_hang?(flags))
|
23
|
+
end
|
24
|
+
|
25
|
+
def waitpid2(pid, flags = 0)
|
26
|
+
code = wait_for_pid(pid, no_hang?(flags))
|
27
|
+
|
28
|
+
[pid, code] if code
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def no_hang?(flags)
|
34
|
+
(flags & Process::WNOHANG) == Process::WNOHANG
|
35
|
+
end
|
36
|
+
|
37
|
+
def wait_for_pid(pid, no_hang)
|
38
|
+
code = Handle.open(pid) { |handle|
|
39
|
+
handle.wait unless no_hang
|
40
|
+
handle.exit_code
|
41
|
+
}
|
42
|
+
|
43
|
+
code if code != PROCESS_STILL_ACTIVE
|
44
|
+
end
|
45
|
+
|
46
|
+
end # class << self
|
47
|
+
end # Windows
|
48
|
+
end # ChildProcess
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ChildProcess::Windows
|
2
|
+
|
3
|
+
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
|
4
|
+
FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000
|
5
|
+
|
6
|
+
PROCESS_ALL_ACCESS = 0x1F0FFF
|
7
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
8
|
+
PROCESS_VM_READ = 0x0010
|
9
|
+
PROCESS_STILL_ACTIVE = 259
|
10
|
+
|
11
|
+
INFINITE = 0xFFFFFFFF
|
12
|
+
|
13
|
+
WIN_SIGINT = 2
|
14
|
+
WIN_SIGBREAK = 3
|
15
|
+
WIN_SIGKILL = 9
|
16
|
+
|
17
|
+
CTRL_C_EVENT = 0
|
18
|
+
CTRL_BREAK_EVENT = 1
|
19
|
+
|
20
|
+
|
21
|
+
module Lib
|
22
|
+
enum :wait_status, [ :wait_object_0, 0,
|
23
|
+
:wait_timeout, 0x102,
|
24
|
+
:wait_abandoned, 0x80,
|
25
|
+
:wait_failed, 0xFFFFFFFF ]
|
26
|
+
end # Lib
|
27
|
+
end # ChildProcess::Windows
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module ChildProcess
|
2
|
+
module Windows
|
3
|
+
module Lib
|
4
|
+
|
5
|
+
def self.last_error_message
|
6
|
+
errnum = get_last_error
|
7
|
+
buf = FFI::MemoryPointer.new :char, 512
|
8
|
+
|
9
|
+
size = format_message(
|
10
|
+
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY,
|
11
|
+
nil, errnum, 0, buf, buf.size, nil
|
12
|
+
)
|
13
|
+
|
14
|
+
buf.read_string(size).strip
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create_proc(cmd, opts = {})
|
18
|
+
cmd_ptr = FFI::MemoryPointer.from_string cmd
|
19
|
+
|
20
|
+
si = StartupInfo.new
|
21
|
+
pi = ProcessInfo.new
|
22
|
+
|
23
|
+
ok = create_process(nil, cmd_ptr, nil, nil, !!opts[:inherit], 0, nil, nil, si, pi)
|
24
|
+
ok or raise Error, last_error_message
|
25
|
+
|
26
|
+
close_handle pi[:hProcess]
|
27
|
+
close_handle pi[:hThread]
|
28
|
+
|
29
|
+
pi[:dwProcessId]
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# BOOL WINAPI CreateProcess(
|
34
|
+
# __in_opt LPCTSTR lpApplicationName,
|
35
|
+
# __inout_opt LPTSTR lpCommandLine,
|
36
|
+
# __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
|
37
|
+
# __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
|
38
|
+
# __in BOOL bInheritHandles,
|
39
|
+
# __in DWORD dwCreationFlags,
|
40
|
+
# __in_opt LPVOID lpEnvironment,
|
41
|
+
# __in_opt LPCTSTR lpCurrentDirectory,
|
42
|
+
# __in LPSTARTUPINFO lpStartupInfo,
|
43
|
+
# __out LPPROCESS_INFORMATION lpProcessInformation
|
44
|
+
# );
|
45
|
+
#
|
46
|
+
|
47
|
+
attach_function :create_process, :CreateProcessA, [
|
48
|
+
:pointer,
|
49
|
+
:pointer,
|
50
|
+
:pointer,
|
51
|
+
:pointer,
|
52
|
+
:bool,
|
53
|
+
:ulong,
|
54
|
+
:pointer,
|
55
|
+
:pointer,
|
56
|
+
:pointer,
|
57
|
+
:pointer],
|
58
|
+
:bool
|
59
|
+
|
60
|
+
#
|
61
|
+
# DWORD WINAPI GetLastError(void);
|
62
|
+
#
|
63
|
+
|
64
|
+
attach_function :get_last_error, :GetLastError, [], :ulong
|
65
|
+
|
66
|
+
#
|
67
|
+
# DWORD WINAPI FormatMessage(
|
68
|
+
# __in DWORD dwFlags,
|
69
|
+
# __in_opt LPCVOID lpSource,
|
70
|
+
# __in DWORD dwMessageId,
|
71
|
+
# __in DWORD dwLanguageId,
|
72
|
+
# __out LPTSTR lpBuffer,
|
73
|
+
# __in DWORD nSize,
|
74
|
+
# __in_opt va_list *Arguments
|
75
|
+
# );
|
76
|
+
#
|
77
|
+
|
78
|
+
attach_function :format_message, :FormatMessageA, [
|
79
|
+
:ulong,
|
80
|
+
:pointer,
|
81
|
+
:ulong,
|
82
|
+
:ulong,
|
83
|
+
:pointer,
|
84
|
+
:ulong,
|
85
|
+
:pointer],
|
86
|
+
:ulong
|
87
|
+
|
88
|
+
|
89
|
+
attach_function :close_handle, :CloseHandle, [:pointer], :bool
|
90
|
+
|
91
|
+
#
|
92
|
+
# HANDLE WINAPI OpenProcess(
|
93
|
+
# __in DWORD dwDesiredAccess,
|
94
|
+
# __in BOOL bInheritHandle,
|
95
|
+
# __in DWORD dwProcessId
|
96
|
+
# );
|
97
|
+
#
|
98
|
+
|
99
|
+
attach_function :open_process, :OpenProcess, [:ulong, :bool, :ulong], :pointer
|
100
|
+
|
101
|
+
#
|
102
|
+
# DWORD WINAPI WaitForSingleObject(
|
103
|
+
# __in HANDLE hHandle,
|
104
|
+
# __in DWORD dwMilliseconds
|
105
|
+
# );
|
106
|
+
#
|
107
|
+
|
108
|
+
attach_function :wait_for_single_object, :WaitForSingleObject, [:pointer, :ulong], :wait_status
|
109
|
+
|
110
|
+
#
|
111
|
+
# BOOL WINAPI GetExitCodeProcess(
|
112
|
+
# __in HANDLE hProcess,
|
113
|
+
# __out LPDWORD lpExitCode
|
114
|
+
# );
|
115
|
+
#
|
116
|
+
|
117
|
+
attach_function :get_exit_code, :GetExitCodeProcess, [:pointer, :pointer], :bool
|
118
|
+
|
119
|
+
#
|
120
|
+
# BOOL WINAPI GenerateConsoleCtrlEvent(
|
121
|
+
# __in DWORD dwCtrlEvent,
|
122
|
+
# __in DWORD dwProcessGroupId
|
123
|
+
# );
|
124
|
+
#
|
125
|
+
|
126
|
+
attach_function :generate_console_ctrl_event, :GenerateConsoleCtrlEvent, [:ulong, :ulong], :bool
|
127
|
+
|
128
|
+
#
|
129
|
+
# BOOL WINAPI TerminateProcess(
|
130
|
+
# __in HANDLE hProcess,
|
131
|
+
# __in UINT uExitCode
|
132
|
+
# );
|
133
|
+
#
|
134
|
+
|
135
|
+
attach_function :terminate_process, :TerminateProcess, [:pointer, :uint], :bool
|
136
|
+
|
137
|
+
end # Lib
|
138
|
+
end # Windows
|
139
|
+
end # ChildProcess
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module ChildProcess::Windows
|
2
|
+
class Handle
|
3
|
+
|
4
|
+
class << self
|
5
|
+
private :new
|
6
|
+
|
7
|
+
def open(pid, access = PROCESS_ALL_ACCESS)
|
8
|
+
handle = Lib.open_process(access, false, pid)
|
9
|
+
|
10
|
+
if handle.null?
|
11
|
+
raise Error, Lib.last_error_message
|
12
|
+
end
|
13
|
+
|
14
|
+
h = new(handle, pid)
|
15
|
+
return h unless block_given?
|
16
|
+
|
17
|
+
begin
|
18
|
+
yield h
|
19
|
+
ensure
|
20
|
+
h.close
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(handle, pid)
|
26
|
+
unless handle.kind_of?(FFI::Pointer)
|
27
|
+
raise TypeError, "invalid handle: #{handle.inspect}"
|
28
|
+
end
|
29
|
+
|
30
|
+
if handle.null?
|
31
|
+
raise ArgumentError, "handle is null: #{handle.inspect}"
|
32
|
+
end
|
33
|
+
|
34
|
+
@pid = pid
|
35
|
+
@handle = handle
|
36
|
+
@closed = false
|
37
|
+
end
|
38
|
+
|
39
|
+
def exit_code
|
40
|
+
code_pointer = FFI::MemoryPointer.new :ulong
|
41
|
+
ok = Lib.get_exit_code(@handle, code_pointer)
|
42
|
+
|
43
|
+
if ok
|
44
|
+
code_pointer.get_ulong(0)
|
45
|
+
else
|
46
|
+
close
|
47
|
+
raise Error, Lib.last_error_message
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def send(signal)
|
52
|
+
case signal
|
53
|
+
when 0
|
54
|
+
exit_code == PROCESS_STILL_ALIVE
|
55
|
+
when WIN_SIGINT
|
56
|
+
Lib.generate_console_ctrl_event(CTRL_C_EVENT, @pid)
|
57
|
+
when WIN_SIGBREAK
|
58
|
+
Lib.generate_console_ctrl_event(CTRL_BREAK_EVENT, @pid)
|
59
|
+
when WIN_SIGKILL
|
60
|
+
ok = Lib.terminate_process(@handle, @pid)
|
61
|
+
ok or raise Error, Lib.last_error_message
|
62
|
+
else
|
63
|
+
thread_id = FFI::MemoryPointer.new(:ulong)
|
64
|
+
module_handle = Lib.get_module_handle("kernel32")
|
65
|
+
proc_address = Lib.get_proc_address(module_handle, "ExitProcess")
|
66
|
+
|
67
|
+
thread = Lib.create_remote_thread(@handle, 0, 0, proc_address, 0, 0, thread_id)
|
68
|
+
thread or raise Error, Lib.last_error_message
|
69
|
+
|
70
|
+
Lib.wait_for_single_object(thread, 5)
|
71
|
+
true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def close
|
76
|
+
return if @closed
|
77
|
+
|
78
|
+
Lib.close_handle(@handle)
|
79
|
+
@closed = true
|
80
|
+
end
|
81
|
+
|
82
|
+
def wait(milliseconds = nil)
|
83
|
+
Lib.wait_for_single_object(@handle, milliseconds || INFINITE)
|
84
|
+
end
|
85
|
+
|
86
|
+
end # Handle
|
87
|
+
end # WindowsProcess
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ChildProcess
|
2
|
+
module Windows
|
3
|
+
class Process < AbstractProcess
|
4
|
+
|
5
|
+
def stop(timeout = 3)
|
6
|
+
assert_started
|
7
|
+
|
8
|
+
# just kill right away on windows.
|
9
|
+
log "sending KILL"
|
10
|
+
@handle.send(WIN_SIGKILL)
|
11
|
+
|
12
|
+
poll_for_exit(timeout)
|
13
|
+
ensure
|
14
|
+
@handle.close
|
15
|
+
end
|
16
|
+
|
17
|
+
def exited?
|
18
|
+
return true if @exit_code
|
19
|
+
|
20
|
+
assert_started
|
21
|
+
code = @handle.exit_code
|
22
|
+
exited = code != PROCESS_STILL_ACTIVE
|
23
|
+
|
24
|
+
if exited
|
25
|
+
@exit_code = code
|
26
|
+
end
|
27
|
+
|
28
|
+
exited
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def launch_process
|
34
|
+
@pid = Lib.create_proc(@args.join(' '), :inherit => false)
|
35
|
+
@handle = Handle.open(@pid)
|
36
|
+
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
end # Process
|
41
|
+
end # Windows
|
42
|
+
end # ChildProcess
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ChildProcess::Windows
|
2
|
+
# typedef struct _STARTUPINFO {
|
3
|
+
# DWORD cb;
|
4
|
+
# LPTSTR lpReserved;
|
5
|
+
# LPTSTR lpDesktop;
|
6
|
+
# LPTSTR lpTitle;
|
7
|
+
# DWORD dwX;
|
8
|
+
# DWORD dwY;
|
9
|
+
# DWORD dwXSize;
|
10
|
+
# DWORD dwYSize;
|
11
|
+
# DWORD dwXCountChars;
|
12
|
+
# DWORD dwYCountChars;
|
13
|
+
# DWORD dwFillAttribute;
|
14
|
+
# DWORD dwFlags;
|
15
|
+
# WORD wShowWindow;
|
16
|
+
# WORD cbReserved2;
|
17
|
+
# LPBYTE lpReserved2;
|
18
|
+
# HANDLE hStdInput;
|
19
|
+
# HANDLE hStdOutput;
|
20
|
+
# HANDLE hStdError;
|
21
|
+
# } STARTUPINFO, *LPSTARTUPINFO;
|
22
|
+
|
23
|
+
class StartupInfo < FFI::Struct
|
24
|
+
layout :cb, :ulong,
|
25
|
+
:lpReserved, :pointer,
|
26
|
+
:lpDesktop, :pointer,
|
27
|
+
:lpTitle, :pointer,
|
28
|
+
:dwX, :ulong,
|
29
|
+
:dwY, :ulong,
|
30
|
+
:dwXSize, :ulong,
|
31
|
+
:dwYSize, :ulong,
|
32
|
+
:dwXCountChars, :ulong,
|
33
|
+
:dwYCountChars, :ulong,
|
34
|
+
:dwFillAttribute, :ulong,
|
35
|
+
:wShowWindow, :ushort,
|
36
|
+
:cbReserved2, :ushort,
|
37
|
+
:lpReserved2, :pointer,
|
38
|
+
:hStdInput, :pointer, # void ptr
|
39
|
+
:hStdOutput, :pointer, # void ptr
|
40
|
+
:hStdError, :pointer # void ptr
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# typedef struct _PROCESS_INFORMATION {
|
45
|
+
# HANDLE hProcess;
|
46
|
+
# HANDLE hThread;
|
47
|
+
# DWORD dwProcessId;
|
48
|
+
# DWORD dwThreadId;
|
49
|
+
# } PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
|
50
|
+
#
|
51
|
+
|
52
|
+
class ProcessInfo < FFI::Struct
|
53
|
+
layout :hProcess, :pointer, # void ptr
|
54
|
+
:hThread, :pointer, # void ptr
|
55
|
+
:dwProcessId, :ulong,
|
56
|
+
:dwThreadId, :ulong
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe ChildProcess do
|
4
|
+
|
5
|
+
EXIT_TIMEOUT = ChildProcess.platform == :jruby ? 2 : 1
|
6
|
+
|
7
|
+
it "returns self when started" do
|
8
|
+
process = sleeping_ruby
|
9
|
+
process.start.should == process
|
10
|
+
process.should be_started
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should know if the process crashed" do
|
14
|
+
process = exit_with(1).start
|
15
|
+
process.poll_for_exit(EXIT_TIMEOUT)
|
16
|
+
|
17
|
+
process.should be_exited
|
18
|
+
process.should be_crashed
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should know if the process didn't crash" do
|
22
|
+
process = exit_with(0).start
|
23
|
+
process.poll_for_exit(EXIT_TIMEOUT)
|
24
|
+
|
25
|
+
process.should be_exited
|
26
|
+
process.should_not be_crashed
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should escalate if TERM is ignored" do
|
30
|
+
process = ignored('TERM').start
|
31
|
+
process.stop
|
32
|
+
process.should be_exited
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'childprocess'
|
4
|
+
require 'spec'
|
5
|
+
require 'spec/autorun'
|
6
|
+
require 'tempfile'
|
7
|
+
|
8
|
+
module ChildProcessSpecHelper
|
9
|
+
|
10
|
+
def ruby_process(*args)
|
11
|
+
@process = ChildProcess.build("ruby" , *args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def sleeping_ruby
|
15
|
+
ruby_process("-e", "sleep")
|
16
|
+
end
|
17
|
+
|
18
|
+
def ignored(signal)
|
19
|
+
code = <<-RUBY
|
20
|
+
trap(#{signal.inspect}, "IGNORE")
|
21
|
+
sleep
|
22
|
+
RUBY
|
23
|
+
|
24
|
+
ruby_process tmp_script(code)
|
25
|
+
end
|
26
|
+
|
27
|
+
def exit_with(exit_code)
|
28
|
+
ruby_process(tmp_script("exit(#{exit_code})"))
|
29
|
+
end
|
30
|
+
|
31
|
+
def tmp_script(code)
|
32
|
+
tf = Tempfile.new("childprocess-temp")
|
33
|
+
tf << code
|
34
|
+
tf.close
|
35
|
+
|
36
|
+
puts code if $DEBUG
|
37
|
+
|
38
|
+
tf.path
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
Spec::Runner.configure do |config|
|
44
|
+
config.include(ChildProcessSpecHelper)
|
45
|
+
config.after(:each) {
|
46
|
+
@process && @process.alive? && @process.stop
|
47
|
+
}
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: childprocess
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Jari Bakken
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-10-08 00:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 2
|
30
|
+
- 9
|
31
|
+
version: 1.2.9
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: yard
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :development
|
45
|
+
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: ffi
|
48
|
+
prerelease: false
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 0
|
55
|
+
- 6
|
56
|
+
- 3
|
57
|
+
version: 0.6.3
|
58
|
+
type: :runtime
|
59
|
+
version_requirements: *id003
|
60
|
+
description: This gem aims at being a simple and reliable solution for controlling external programs running in the background on any Ruby / OS combination.
|
61
|
+
email: jari.bakken@gmail.com
|
62
|
+
executables: []
|
63
|
+
|
64
|
+
extensions: []
|
65
|
+
|
66
|
+
extra_rdoc_files:
|
67
|
+
- LICENSE
|
68
|
+
- README.rdoc
|
69
|
+
files:
|
70
|
+
- .document
|
71
|
+
- .gitignore
|
72
|
+
- LICENSE
|
73
|
+
- README.rdoc
|
74
|
+
- Rakefile
|
75
|
+
- VERSION
|
76
|
+
- lib/childprocess.rb
|
77
|
+
- lib/childprocess/ironruby.rb
|
78
|
+
- lib/childprocess/ironruby/process.rb
|
79
|
+
- lib/childprocess/jruby.rb
|
80
|
+
- lib/childprocess/jruby/process.rb
|
81
|
+
- lib/childprocess/unix.rb
|
82
|
+
- lib/childprocess/unix/process.rb
|
83
|
+
- lib/childprocess/windows.rb
|
84
|
+
- lib/childprocess/windows/api.rb
|
85
|
+
- lib/childprocess/windows/constants.rb
|
86
|
+
- lib/childprocess/windows/functions.rb
|
87
|
+
- lib/childprocess/windows/handle.rb
|
88
|
+
- lib/childprocess/windows/process.rb
|
89
|
+
- lib/childprocess/windows/structs.rb
|
90
|
+
- spec/childprocess_spec.rb
|
91
|
+
- spec/spec.opts
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
has_rdoc: true
|
94
|
+
homepage: http://github.com/jarib/childprocess
|
95
|
+
licenses: []
|
96
|
+
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options:
|
99
|
+
- --charset=UTF-8
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
version: "0"
|
116
|
+
requirements: []
|
117
|
+
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 1.3.6
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: Cross-platform ruby library for managing child processes.
|
123
|
+
test_files:
|
124
|
+
- spec/childprocess_spec.rb
|
125
|
+
- spec/spec_helper.rb
|