procreate 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007-2010 Charles Lowe
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
+
@@ -0,0 +1,4 @@
1
+ == 1.0.0 / 2011-11-04
2
+
3
+ - First public release
4
+
data/README ADDED
@@ -0,0 +1,16 @@
1
+ = Introduction
2
+
3
+ The procreate library provides a simple but powerful api for process creation,
4
+ with a thin, cross-platform wrapper around Open4.background.
5
+
6
+ Ara Howard's Open4 library does the heavy lifting on POSIX systems, and a
7
+ compatible Open4 implementation is included for windows use.
8
+
9
+ The primary documentation is for the Process.create method.
10
+
11
+ = TODO
12
+
13
+ * Some useful tests.
14
+
15
+ * Perhaps use separate platform gems so linux can depend on open4 gem, and
16
+ windows on windows api.
@@ -0,0 +1,74 @@
1
+ if RUBY_PLATFORM =~ /mswin|mingw/
2
+ require 'procreate/win32/open4'
3
+ else
4
+ require 'procreate/posix/open4'
5
+ end
6
+
7
+ module Process
8
+ # Return the pid of the process.
9
+ def pid
10
+ raise NotImplementedError
11
+ end
12
+
13
+ # Return the process exit status. Will block until the process finishes
14
+ # executing.
15
+ def exitstatus
16
+ raise NotImplementedError
17
+ end
18
+
19
+ #
20
+ # Process.create is a thin, cross-platform wrapper around Open4.background.
21
+ #
22
+ # The given +cmdline+ is run in a background process and a thread object is
23
+ # returned as a handle. The thread is extended with the Process module, to
24
+ # facilitate testing for a process object and provide an extension point.
25
+ # Methods are provided to retrieve the #pid or #exitstatus.
26
+ #
27
+ # It takes arguments for :stdin, :stdout, and :stderr redirection, and also a
28
+ # :shell parameter which determines whether or not the command line should be
29
+ # run by a shell.
30
+ #
31
+ # The default if no streams are provided is that output is thrown away. You
32
+ # need to explicitly pass eg :stdout => STDOUT if you want to keep STDOUT from
33
+ # process.
34
+ #
35
+ # Currently the shell is avoided where possible to ensure the pid is that of
36
+ # the actual process, so it can be signaled (ie killed) reliably. Otherwise,
37
+ # the pid is that of the shell, and any signalling to that pid works as per
38
+ # the shell's documentation.
39
+ #
40
+ def Process.create cmdline, params={}
41
+ params = params.dup
42
+ shell = params.delete(:shell) # default is false, so nil is okay
43
+ if RUBY_PLATFORM !~ /mswin|mingw/
44
+ if Array === cmdline
45
+ raise ArgumentError, 'unable to use shell with array command' if shell
46
+ elsif shell
47
+ # nothing to do. exec will use /bin/sh anyway
48
+ else
49
+ # avoid use of shell, so that #pid on return value is actual process pid
50
+ # not shell pid
51
+ require 'shellwords'
52
+ cmdline = Shellwords.shellwords cmdline.to_s
53
+ cmdline = [cmdline, cmdline].transpose if cmdline.length == 1
54
+ end
55
+ # just due to different behaviour for Open4 unix
56
+ params.update :status => true
57
+ else
58
+ if Array === cmdline
59
+ # the windows version doesn't support array form of cmdline (though could
60
+ # perhaps fake using some shell quoting thing).
61
+ raise ArgumentError, 'array arguments not supported on windows'
62
+ elsif shell
63
+ # it won't use the shell by default with CreateProcess.
64
+ # so force it to do so here:
65
+ # FIXME, this should really be ENV['COMSPEC'] or whatever.
66
+ cmdline = "cmd /c #{cmdline}"
67
+ else
68
+ # nothing to do
69
+ end
70
+ end
71
+ Open4.background(cmdline, params).extend(Process)
72
+ end
73
+ end
74
+
@@ -0,0 +1,102 @@
1
+ require 'open4'
2
+
3
+ module PosixOpen4 = Open4
4
+ #
5
+ # This is a rewritten version of Open4.background. The reason for this is
6
+ # two-fold:
7
+ #
8
+ # * The existing function returns a thread that is extended to have a #pid
9
+ # method. This in turn waits on a queue, which will never have a pid pushed
10
+ # to it if there are any problems spawning. This version instead doesn't
11
+ # create the separate background thread until after the popen4 has
12
+ # succeeded, meaning any exceptions related to spawn are thrown immediately
13
+ # and there is no need for a pid queue.
14
+ # * The existing spawn function calls flatten! on its argv parameter. This is
15
+ # because it was trying to support either splatted or not-splatted array,
16
+ # but that falls over for the 1-arg array form of Kernel#exec (eg, to launch
17
+ # a command that has spaces in its name, without using the shell.)
18
+ #
19
+ def background argv, opts={}
20
+ cmd = [argv].flatten.join(' ')
21
+
22
+ getopt = getopts opts
23
+
24
+ ignore_exit_failure = getopt[ 'ignore_exit_failure', getopt['quiet', false] ]
25
+ ignore_exec_failure = getopt[ 'ignore_exec_failure', !getopt['raise', true] ]
26
+ exitstatus = getopt[ %w( exitstatus exit_status status ) ]
27
+ stdin = getopt[ %w( stdin in i 0 ) << 0 ]
28
+ stdout = getopt[ %w( stdout out o 1 ) << 1 ]
29
+ stderr = getopt[ %w( stderr err e 2 ) << 2 ]
30
+ timeout = getopt[ %w( timeout spawn_timeout ) ]
31
+ stdin_timeout = getopt[ %w( stdin_timeout ) ]
32
+ stdout_timeout = getopt[ %w( stdout_timeout io_timeout ) ]
33
+ stderr_timeout = getopt[ %w( stderr_timeout ) ]
34
+ status = getopt[ %w( status ) ]
35
+ cwd = getopt[ %w( cwd dir ) ]
36
+
37
+ exitstatus =
38
+ case exitstatus
39
+ when TrueClass, FalseClass
40
+ ignore_exit_failure = true if exitstatus
41
+ [0]
42
+ else
43
+ [*(exitstatus || 0)].map{|i| Integer i}
44
+ end
45
+
46
+ stdin ||= '' if stdin_timeout
47
+ stdout ||= '' if stdout_timeout
48
+ stderr ||= '' if stderr_timeout
49
+
50
+ started = false
51
+ begin
52
+ chdir(cwd) do
53
+ pid, i, o, e = popen4(*argv)
54
+ started = true
55
+
56
+ thread = Thread.new do
57
+ begin
58
+ # the semantics of this timeout have been changed slightly, in
59
+ # that it doesn't include the popen4 call itself anymore. i think
60
+ # the intention however was for it to apply primarily to the
61
+ # Process.waitpid
62
+ Timeout::timeout timeout do
63
+ te = ThreadEnsemble.new pid
64
+ te.add_thread(i, stdin) do |i, stdin|
65
+ relay stdin, i, stdin_timeout
66
+ i.close rescue nil
67
+ end
68
+ te.add_thread(o, stdout) do |o, stdout|
69
+ relay o, stdout, stdout_timeout
70
+ end
71
+ te.add_thread(e, stderr) do |o, stderr|
72
+ relay e, stderr, stderr_timeout
73
+ end
74
+ te.run
75
+
76
+ status = Process.waitpid2(pid).last
77
+ unless ignore_exit_failure or (status.nil? and ignore_exec_failure) or exitstatus.include?(status.exitstatus)
78
+ raise SpawnError.new(cmd, status)
79
+ end
80
+ status
81
+ end
82
+ ensure
83
+ [i, o, e].each { |fd| fd.close unless fd.closed? }
84
+ end
85
+ end
86
+
87
+ sc = class << thread; self; end
88
+ sc.module_eval do
89
+ define_method(:pid) { pid }
90
+ define_method(:spawn_status) { value }
91
+ define_method(:exitstatus) { spawn_status.exitstatus }
92
+ end
93
+
94
+ thread
95
+ end
96
+ rescue
97
+ raise unless not started and ignore_exec_failure
98
+ end
99
+ end
100
+ module_function :background
101
+ end
102
+
@@ -0,0 +1,289 @@
1
+ require 'windows/api'
2
+
3
+ module Open4 # :nodoc:
4
+ end
5
+
6
+ #
7
+ # An implementation of Open4 for windows, based largely on a post by Simon
8
+ # Kröger to comp.lang.ruby - "Problem with popen on windows"
9
+ # (http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/a40bac71df1f4a4c).
10
+ #
11
+ # Provides an api that loosely matches Ara Howard's Open4 (at least for
12
+ # Open4.background so far).
13
+ #
14
+ module Win32Open4 = Open4
15
+ # Import necessary raw kernel32 functions
16
+ module Kernel32 # :nodoc:
17
+ API = Windows::API
18
+ API.auto_namespace = 'Win32Open4::Kernel32'
19
+ API.auto_constant = true
20
+ API.auto_method = true
21
+ API.auto_unicode = true
22
+
23
+ ERROR_SUCCESS = 0x00
24
+ FORMAT_MESSAGE_FROM_SYSTEM = 0x1000
25
+ FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x2000
26
+
27
+ WAIT_OBJECT_0 = 0
28
+ WAIT_TIMEOUT = 0x102
29
+ WAIT_ABANDONED = 128
30
+ WAIT_ABANDONED_0 = WAIT_ABANDONED
31
+ WAIT_FAILED = 0xFFFFFFFF
32
+
33
+ NORMAL_PRIORITY_CLASS = 0x00000020
34
+ STARTUP_INFO_SIZE = 68
35
+ PROCESS_INFO_SIZE = 16
36
+ SECURITY_ATTRIBUTES_SIZE = 12
37
+
38
+ HANDLE_FLAG_INHERIT = 1
39
+ HANDLE_FLAG_PROTECT_FROM_CLOSE = 2
40
+
41
+ STARTF_USESHOWWINDOW = 0x00000001
42
+ STARTF_USESTDHANDLES = 0x00000100
43
+
44
+ API.new('GetLastError', '', 'L', 'kernel32')
45
+ API.new('CloseHandle', 'L', 'I', 'kernel32')
46
+ API.new('WaitForSingleObject', 'LL', 'L')
47
+ API.new('GetExitCodeProcess', 'LP', 'B')
48
+
49
+ params = [
50
+ 'L', # IN DWORD dwFlags,
51
+ 'P', # IN LPCVOID lpSource,
52
+ 'L', # IN DWORD dwMessageId,
53
+ 'L', # IN DWORD dwLanguageId,
54
+ 'P', # OUT LPSTR lpBuffer,
55
+ 'L', # IN DWORD nSize,
56
+ 'P', # IN va_list *Arguments
57
+ ]
58
+ API.new('FormatMessage', params.join, 'L', 'kernel32')
59
+
60
+ params = [
61
+ 'P', # pointer to read handle
62
+ 'P', # pointer to write handle
63
+ 'P', # pointer to security attributes
64
+ 'L' # pipe size
65
+ ]
66
+ API.new('CreatePipe', params.join, 'I', 'kernel32')
67
+
68
+ params = [
69
+ 'L', # handle to an object
70
+ 'L', # specifies flags to change
71
+ 'L' # specifies new values for flags
72
+ ]
73
+ API.new('SetHandleInformation', params.join, 'I', 'kernel32')
74
+
75
+ params = [
76
+ 'L', # IN LPCSTR lpApplicationName
77
+ 'P', # IN LPSTR lpCommandLine
78
+ 'L', # IN LPSECURITY_ATTRIBUTES lpProcessAttributes
79
+ 'L', # IN LPSECURITY_ATTRIBUTES lpThreadAttributes
80
+ 'L', # IN BOOL bInheritHandles
81
+ 'L', # IN DWORD dwCreationFlags
82
+ 'L', # IN LPVOID lpEnvironment
83
+ 'L', # IN LPCSTR lpCurrentDirectory
84
+ 'P', # IN LPSTARTUPINFOA lpStartupInfo
85
+ 'P' # OUT LPPROCESS_INFORMATION lpProcessInformation
86
+ ]
87
+ API.new('CreateProcess', params.join, 'I', 'kernel32')
88
+
89
+ params = [
90
+ 'L', # handle to file to write to
91
+ 'P', # pointer to data to write to file
92
+ 'L', # number of bytes to write
93
+ 'P', # pointer to number of bytes written
94
+ 'L' # pointer to structure for overlapped I/O
95
+ ]
96
+ API.new('WriteFile', params.join, 'I', 'kernel32')
97
+
98
+ params = [
99
+ 'L', # handle of file to read
100
+ 'P', # pointer to buffer that receives data
101
+ 'L', # number of bytes to read
102
+ 'P', # pointer to number of bytes read
103
+ 'L' # pointer to structure for data
104
+ ]
105
+ API.new('ReadFile', params.join, 'I', 'kernel32')
106
+
107
+ params = [
108
+ 'L', # handle to pipe to copy from
109
+ 'L', # pointer to data buffer
110
+ 'L', # size, in bytes, of data buffer
111
+ 'L', # pointer to number of bytes read
112
+ 'P', # pointer to total number of bytes available
113
+ 'L' # pointer to unread bytes in this message
114
+ ]
115
+ API.new('PeekNamedPipe', params.join, 'I', 'kernel32')
116
+ end
117
+
118
+ # Now add wrapper functions
119
+ module Kernel32 # :nodoc:
120
+ extend self
121
+
122
+ class Kernel32Error < StandardError # :nodoc:
123
+ end
124
+
125
+ module_function
126
+
127
+ def raise_last_error!
128
+ errorCode = GetLastError()
129
+ if errorCode != ERROR_SUCCESS
130
+ msg = ' ' * 255
131
+ msgLength = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY, '', errorCode, 0, msg, 255, '')
132
+ msg.delete! 0.chr
133
+ msg.strip!
134
+ raise Kernel32Error, msg
135
+ else
136
+ raise Kernel32Error, 'GetLastError returned ERROR_SUCCESS'
137
+ end
138
+ end
139
+
140
+ # returns read and write handle
141
+ def create_pipe
142
+ read_handle, write_handle = Array.new(2) { [0].pack('I') }
143
+ sec_attrs = [SECURITY_ATTRIBUTES_SIZE, 0, 1].pack('III')
144
+ raise_last_error! if CreatePipe(read_handle, write_handle, sec_attrs, 0).zero?
145
+ [read_handle.unpack('I')[0], write_handle.unpack('I')[0]]
146
+ end
147
+
148
+ def set_handle_information(handle, flags, value)
149
+ raise_last_error! if SetHandleInformation(handle, flags, value).zero?
150
+ end
151
+
152
+ def close_handle(handle)
153
+ raise_last_error! if CloseHandle(handle).zero?
154
+ end
155
+
156
+ def create_process(command, stdin, stdout, stderr)
157
+ startupInfo = [
158
+ STARTUP_INFO_SIZE,
159
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
160
+ STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW,
161
+ 0, 0, 0,
162
+ stdin, stdout, stderr
163
+ ].pack('IIIIIIIIIIIISSIIII')
164
+ processInfo = [0, 0, 0, 0].pack('IIII')
165
+ command = command + 0.chr
166
+ raise_last_error! if CreateProcessA(0, command, 0, 0, 1, 0, 0, 0, startupInfo, processInfo).zero?
167
+ # hProcess, hThread, dwProcessId, dwThreadId
168
+ processInfo.unpack('LLLL')
169
+ end
170
+
171
+ def write_file(hFile, buffer)
172
+ written = [0].pack('I')
173
+ raise_last_error! if WriteFile(hFile, buffer, buffer.size, written, 0).zero?
174
+ written.unpack('I')[0]
175
+ end
176
+
177
+ def read_file(hFile, size=1024)
178
+ number = [0].pack('I')
179
+ buffer = ' ' * size
180
+ # FIXME? we're masking errors here and just returning an empty string...
181
+ return '' if ReadFile(hFile, buffer, size, number, 0).zero?
182
+ buffer[0...number.unpack('I')[0]]
183
+ end
184
+
185
+ def peek_named_pipe(hFile)
186
+ available = [0].pack('I')
187
+ # FIXME? as above
188
+ return -1 if PeekNamedPipe(hFile, 0, 0, 0, available, 0).zero?
189
+ available.unpack('I')[0]
190
+ end
191
+ end
192
+
193
+ # Responsible for launching a process, forwarding its stdout & stderr, and
194
+ # catching its error code.
195
+ # TODO: stdin is not currently handled in the ProcessHandler#relay method,
196
+ # though it should be fairly straightforward to add.
197
+ class ProcessHandler # :nodoc:
198
+ include Win32Open4::Kernel32
199
+
200
+ def initialize cmdline, params={}
201
+ stdin, @stdout, @stderr = params.values_at :stdin, :stdout, :stderr
202
+ if stdin
203
+ raise NotImplementError, 'no stdin forwarding yet'
204
+ end
205
+
206
+ # create 3 pipes
207
+ pw, pr, pe = Array.new(3) { create_pipe }
208
+
209
+ set_handle_information pw.last, HANDLE_FLAG_INHERIT, 0
210
+ set_handle_information pr.first, HANDLE_FLAG_INHERIT, 0
211
+ set_handle_information pe.first, HANDLE_FLAG_INHERIT, 0
212
+
213
+ @phandle, hThread, @processId, dwThreadId = create_process cmdline.to_s, pw.first, pr.last, pe.last
214
+
215
+ # we have to close the handles, so the pipes terminate with the process
216
+ close_handle pw.first
217
+ close_handle pr.last
218
+ close_handle pe.last
219
+
220
+ finalizer = self.class.finalizer [@phandle, pw.last, pr.first, pe.first]
221
+ ObjectSpace.define_finalizer self, finalizer
222
+
223
+ @pr, @pe = pr, pe
224
+ end
225
+
226
+ def self.finalizer handles
227
+ proc do
228
+ handles.each { |h| close_handle h }
229
+ end
230
+ end
231
+
232
+ def run
233
+ loop do
234
+ relay
235
+
236
+ case WaitForSingleObject @phandle, 10
237
+ when WAIT_TIMEOUT
238
+ # ok...
239
+ when WAIT_OBJECT_0
240
+ relay
241
+ exit_code = [0].pack 'L'
242
+ if GetExitCodeProcess @phandle, exit_code
243
+ exit_status = exit_code.unpack('L')[0]
244
+ end
245
+ return exit_status
246
+ when -1, WAIT_ABANDONED, WAIT_FAILED
247
+ raise_last_error!
248
+ end
249
+
250
+ sleep 0.01
251
+ end
252
+ end
253
+
254
+ def relay
255
+ stdout = ''
256
+ avail = peek_named_pipe(@pr.first)
257
+ stdout = read_file(@pr.first, avail) if avail > 0
258
+
259
+ stderr = ''
260
+ avail = peek_named_pipe(@pe.first)
261
+ stderr = read_file(@pe.first, avail) if avail > 0
262
+
263
+ @stdout << stdout if !stdout.empty? and @stdout
264
+ @stderr << stderr if !stderr.empty? and @stderr
265
+ end
266
+ end
267
+
268
+ #
269
+ # Launches a process asynchronously, and returns a thread that can be used to
270
+ # retrieve exit status, or wait for completion. All calls used are
271
+ # non-blocking such that it works safely with ruby's green threads.
272
+ #
273
+ # Note that while broadly compatible there are a number of differences from
274
+ # original Open4.background - eg parameters like :status are ignored (in fact
275
+ # it works like :status => true in that it won't throw exceptions).
276
+ #
277
+ def self.background cmdline, params={}
278
+ ph = ProcessHandler.new cmdline, params
279
+ pid = ph.instance_variable_get :@processId
280
+ thread = Thread.new { ph.run }
281
+ sc = class << thread; self; end
282
+ sc.module_eval do
283
+ define_method(:pid) { pid }
284
+ define_method(:exitstatus) { value }
285
+ end
286
+ thread
287
+ end
288
+ end
289
+
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: procreate
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Charles Lowe
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-04 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: A library for process creation with a simple but powerful api, built on Open4. Includes compatible Win32 Open4 implementation.
23
+ email: aquasync@gmail.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README
30
+ - ChangeLog
31
+ files:
32
+ - README
33
+ - COPYING
34
+ - ChangeLog
35
+ - lib/procreate/posix/open4.rb
36
+ - lib/procreate/win32/open4.rb
37
+ - lib/procreate.rb
38
+ has_rdoc: true
39
+ homepage: http://github.com/aquasync/procreate
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --main
45
+ - README
46
+ - --title
47
+ - procreate documentation
48
+ - --tab-width
49
+ - "2"
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ hash: 3
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.3.7
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Create processes in a flexible and cross-platform way.
77
+ test_files: []
78
+