procreate 1.0.0

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/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
+