rexec 1.1.10
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/README.rdoc +14 -0
- data/bin/daemon-exec +116 -0
- data/lib/rexec.rb +33 -0
- data/lib/rexec/client.rb +23 -0
- data/lib/rexec/connection.rb +153 -0
- data/lib/rexec/daemon.rb +27 -0
- data/lib/rexec/daemon/base.rb +151 -0
- data/lib/rexec/daemon/controller.rb +192 -0
- data/lib/rexec/daemon/pidfile.rb +64 -0
- data/lib/rexec/priviledges.rb +28 -0
- data/lib/rexec/reverse_io.rb +70 -0
- data/lib/rexec/server.rb +62 -0
- data/lib/rexec/task.rb +325 -0
- data/lib/rexec/version.rb +24 -0
- data/test/client.rb +31 -0
- data/test/daemon.rb +68 -0
- data/test/daemon_test.rb +44 -0
- data/test/listing_example.rb +49 -0
- data/test/remote_server_test.rb +63 -0
- data/test/server_test.rb +89 -0
- data/test/task.rb +24 -0
- data/test/task_test.rb +165 -0
- metadata +87 -0
@@ -0,0 +1,192 @@
|
|
1
|
+
# Copyright (c) 2007, 2009 Samuel Williams. Released under the GNU GPLv3.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require 'rexec/daemon/pidfile'
|
17
|
+
require 'rexec/task'
|
18
|
+
|
19
|
+
module RExec
|
20
|
+
module Daemon
|
21
|
+
# Daemon startup timeout
|
22
|
+
TIMEOUT = 5
|
23
|
+
|
24
|
+
# This module contains functionality related to starting and stopping the daemon, and code for processing command line input.
|
25
|
+
module Controller
|
26
|
+
# This function is called from the daemon executable. It processes ARGV and checks whether the user is asking for
|
27
|
+
# <tt>start</tt>, <tt>stop</tt>, <tt>restart</tt> or <tt>status</tt>.
|
28
|
+
def self.daemonize(daemon)
|
29
|
+
#puts "Running in #{WorkingDirectory}, logs in #{LogDirectory}"
|
30
|
+
case !ARGV.empty? && ARGV[0]
|
31
|
+
when 'start'
|
32
|
+
start(daemon)
|
33
|
+
status(daemon)
|
34
|
+
when 'stop'
|
35
|
+
stop(daemon)
|
36
|
+
status(daemon)
|
37
|
+
PidFile.cleanup(daemon)
|
38
|
+
when 'restart'
|
39
|
+
stop(daemon)
|
40
|
+
PidFile.cleanup(daemon)
|
41
|
+
start(daemon)
|
42
|
+
status(daemon)
|
43
|
+
when 'status'
|
44
|
+
status(daemon)
|
45
|
+
else
|
46
|
+
puts "Invalid command. Please specify start, restart, stop or status."
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# This function starts the supplied daemon
|
52
|
+
def self.start(daemon)
|
53
|
+
puts "Starting daemon..."
|
54
|
+
|
55
|
+
case PidFile.status(daemon)
|
56
|
+
when :running
|
57
|
+
$stderr.puts "Daemon already running!"
|
58
|
+
return
|
59
|
+
when :stopped
|
60
|
+
# We are good to go...
|
61
|
+
else
|
62
|
+
$stderr.puts "Daemon in unknown state! Will clear previous state and continue."
|
63
|
+
status(daemon)
|
64
|
+
PidFile.clear(daemon)
|
65
|
+
end
|
66
|
+
|
67
|
+
daemon.prefork
|
68
|
+
daemon.mark_err_log
|
69
|
+
|
70
|
+
fork do
|
71
|
+
Process.setsid
|
72
|
+
exit if fork
|
73
|
+
|
74
|
+
PidFile.store(daemon, Process.pid)
|
75
|
+
|
76
|
+
File.umask 0000
|
77
|
+
Dir.chdir daemon.working_directory
|
78
|
+
|
79
|
+
$stdin.reopen "/dev/null"
|
80
|
+
$stdout.reopen daemon.log_fn, "a"
|
81
|
+
$stderr.reopen daemon.err_fn, "a"
|
82
|
+
|
83
|
+
main = Thread.new do
|
84
|
+
begin
|
85
|
+
daemon.run
|
86
|
+
rescue
|
87
|
+
$stderr.puts "=== Daemon Exception Backtrace @ #{Time.now.to_s} ==="
|
88
|
+
$stderr.puts "#{$!.class}: #{$!.message}"
|
89
|
+
$!.backtrace.each { |at| $stderr.puts at }
|
90
|
+
$stderr.puts "=== Daemon Crashed ==="
|
91
|
+
|
92
|
+
$stderr.flush
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
trap("INT") do
|
97
|
+
daemon.shutdown
|
98
|
+
main.exit
|
99
|
+
end
|
100
|
+
|
101
|
+
trap("TERM") do
|
102
|
+
exit!
|
103
|
+
end
|
104
|
+
|
105
|
+
main.join
|
106
|
+
end
|
107
|
+
|
108
|
+
puts "Waiting for daemon to start..."
|
109
|
+
sleep 0.1
|
110
|
+
timer = TIMEOUT
|
111
|
+
pid = PidFile.recall(daemon)
|
112
|
+
|
113
|
+
while pid == nil and timer > 0
|
114
|
+
# Wait a moment for the forking to finish...
|
115
|
+
puts "Waiting for daemon to start (#{timer}/#{TIMEOUT})"
|
116
|
+
sleep 1
|
117
|
+
|
118
|
+
# If the daemon has crashed, it is never going to start...
|
119
|
+
break if daemon.crashed?
|
120
|
+
|
121
|
+
pid = PidFile.recall(daemon)
|
122
|
+
|
123
|
+
timer -= 1
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Prints out the status of the daemon
|
128
|
+
def self.status(daemon)
|
129
|
+
case PidFile.status(daemon)
|
130
|
+
when :running
|
131
|
+
puts "Daemon status: running pid=#{PidFile.recall(daemon)}"
|
132
|
+
when :unknown
|
133
|
+
if daemon.crashed?
|
134
|
+
puts "Daemon status: crashed"
|
135
|
+
|
136
|
+
$stdout.flush
|
137
|
+
daemon.tail_err_log($stderr)
|
138
|
+
else
|
139
|
+
puts "Daemon status: unknown"
|
140
|
+
end
|
141
|
+
when :stopped
|
142
|
+
puts "Daemon status: stopped"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Stops the daemon process.
|
147
|
+
def self.stop(daemon)
|
148
|
+
puts "Stopping daemon..."
|
149
|
+
|
150
|
+
# Check if the pid file exists...
|
151
|
+
if !File.file?(daemon.pid_fn)
|
152
|
+
puts "Pid file not found. Is the daemon running?"
|
153
|
+
return
|
154
|
+
end
|
155
|
+
|
156
|
+
pid = PidFile.recall(daemon)
|
157
|
+
|
158
|
+
# Check if the daemon is already stopped...
|
159
|
+
unless PidFile.running(daemon)
|
160
|
+
puts "Pid #{pid} is not running. Has daemon crashed?"
|
161
|
+
return
|
162
|
+
end
|
163
|
+
|
164
|
+
pid = PidFile.recall(daemon)
|
165
|
+
Process.kill("INT", pid)
|
166
|
+
sleep 0.1
|
167
|
+
|
168
|
+
# Kill/Term loop - if the daemon didn't die easily, shoot
|
169
|
+
# it a few more times.
|
170
|
+
attempts = 5
|
171
|
+
while PidFile.running(daemon) and attempts > 0
|
172
|
+
sig = (attempts < 2) ? "KILL" : "TERM"
|
173
|
+
|
174
|
+
puts "Sending #{sig} to pid #{pid}..."
|
175
|
+
Process.kill(sig, pid)
|
176
|
+
|
177
|
+
sleep 1 unless first
|
178
|
+
attempts -= 1
|
179
|
+
end
|
180
|
+
|
181
|
+
# If after doing our best the daemon is still running (pretty odd)...
|
182
|
+
if PidFile.running(daemon)
|
183
|
+
puts "Daemon appears to be still running!"
|
184
|
+
return
|
185
|
+
end
|
186
|
+
|
187
|
+
# Otherwise the daemon has been stopped.
|
188
|
+
PidFile.clear(daemon)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Copyright (c) 2007, 2009 Samuel Williams. Released under the GNU GPLv3.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
module RExec
|
17
|
+
module Daemon
|
18
|
+
# This module controls the storage and retrieval of process id files.
|
19
|
+
module PidFile
|
20
|
+
# Saves the pid for the given daemon
|
21
|
+
def self.store(daemon, pid)
|
22
|
+
File.open(daemon.pid_fn, 'w') {|f| f << pid}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Retrieves the pid for the given daemon
|
26
|
+
def self.recall(daemon)
|
27
|
+
IO.read(daemon.pid_fn).to_i rescue nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# Removes the pid saved for a particular daemon
|
31
|
+
def self.clear(daemon)
|
32
|
+
if File.exist? daemon.pid_fn
|
33
|
+
FileUtils.rm(daemon.pid_fn)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Checks whether the daemon is running by checking the saved pid and checking the corresponding process
|
38
|
+
def self.running(daemon)
|
39
|
+
pid = recall(daemon)
|
40
|
+
|
41
|
+
return false if pid == nil
|
42
|
+
|
43
|
+
gpid = Process.getpgid(pid) rescue nil
|
44
|
+
|
45
|
+
return gpid != nil ? true : false
|
46
|
+
end
|
47
|
+
|
48
|
+
# Remove the pid file if the daemon is not running
|
49
|
+
def self.cleanup(daemon)
|
50
|
+
clear(daemon) unless running(daemon)
|
51
|
+
end
|
52
|
+
|
53
|
+
# This function returns the status of the daemon. This can be one of <tt>:running</tt>, <tt>:unknown</tt> (pid file exists but no
|
54
|
+
# corresponding process can be found) or <tt>:stopped</tt>.
|
55
|
+
def self.status(daemon)
|
56
|
+
if File.exist? daemon.pid_fn
|
57
|
+
return PidFile.running(daemon) ? :running : :unknown
|
58
|
+
else
|
59
|
+
return :stopped
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
require 'etc'
|
3
|
+
|
4
|
+
module RExec
|
5
|
+
|
6
|
+
# Set the user of the current process. Supply either a user ID
|
7
|
+
# or a user name.
|
8
|
+
#
|
9
|
+
# Be aware that on Mac OS X / Ruby 1.8 there are bugs when the user id
|
10
|
+
# is negative (i.e. it doesn't work). For example "nobody" with uid -2
|
11
|
+
# won't work.
|
12
|
+
def self.change_user(user)
|
13
|
+
if user.kind_of?(String)
|
14
|
+
user = Etc.getpwnam(user).uid
|
15
|
+
end
|
16
|
+
|
17
|
+
Process::Sys.setuid(user)
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# Get the user of the current process. Returns the user name.
|
22
|
+
def self.current_user
|
23
|
+
uid = Process::Sys.getuid
|
24
|
+
|
25
|
+
Etc.getpwuid(uid).name
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
class File
|
3
|
+
# Seek to the end of the file
|
4
|
+
def seek_end
|
5
|
+
seek(0, IO::SEEK_END)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Read a chunk of data and then move the file pointer backwards.
|
9
|
+
#
|
10
|
+
# Calling this function multiple times will return new data and traverse the file backwards.
|
11
|
+
#
|
12
|
+
def read_reverse(length)
|
13
|
+
offset = tell
|
14
|
+
|
15
|
+
if offset == 0
|
16
|
+
return nil
|
17
|
+
end
|
18
|
+
|
19
|
+
start = [0, offset-length].max
|
20
|
+
|
21
|
+
seek(start, IO::SEEK_SET)
|
22
|
+
|
23
|
+
buf = read(offset-start)
|
24
|
+
|
25
|
+
seek(start, IO::SEEK_SET)
|
26
|
+
|
27
|
+
return buf
|
28
|
+
end
|
29
|
+
|
30
|
+
REVERSE_BUFFER_SIZE = 128
|
31
|
+
|
32
|
+
# This function is very similar to gets but it works in reverse.
|
33
|
+
#
|
34
|
+
# You can use it to efficiently read a file line by line backwards.
|
35
|
+
#
|
36
|
+
# It returns nil when there are no more lines.
|
37
|
+
def reverse_gets(sep_string=$/)
|
38
|
+
end_pos = tell
|
39
|
+
|
40
|
+
offset = nil
|
41
|
+
buf = ""
|
42
|
+
|
43
|
+
while offset == nil
|
44
|
+
chunk = read_reverse(REVERSE_BUFFER_SIZE)
|
45
|
+
return (buf == "" ? nil : buf) if chunk == nil
|
46
|
+
|
47
|
+
buf = chunk + buf
|
48
|
+
|
49
|
+
offset = buf.rindex(sep_string)
|
50
|
+
end
|
51
|
+
|
52
|
+
line = buf[offset...buf.size].sub(sep_string, "")
|
53
|
+
|
54
|
+
seek((end_pos - buf.size) + offset, IO::SEEK_SET)
|
55
|
+
|
56
|
+
return line
|
57
|
+
end
|
58
|
+
|
59
|
+
# Similar to each_line but works in reverse. Don't forget to call
|
60
|
+
# seek_end before you start!
|
61
|
+
def reverse_each_line(sep_string=$/, &block)
|
62
|
+
line = reverse_gets(sep_string)
|
63
|
+
|
64
|
+
while line != nil
|
65
|
+
yield line
|
66
|
+
|
67
|
+
line = reverse_gets(sep_string)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/rexec/server.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Copyright (c) 2007 Samuel Williams. Released under the GNU GPLv3.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require 'pathname'
|
17
|
+
require 'rexec/task'
|
18
|
+
require 'rexec/connection'
|
19
|
+
|
20
|
+
module RExec
|
21
|
+
|
22
|
+
class InvalidConnectionError < Exception
|
23
|
+
end
|
24
|
+
|
25
|
+
@@connection_code = (Pathname.new(__FILE__).dirname + "connection.rb").read
|
26
|
+
@@client_code = (Pathname.new(__FILE__).dirname + "client.rb").read
|
27
|
+
|
28
|
+
# Start a remote ruby server. This function is a structural cornerstone. This code runs the command you
|
29
|
+
# supply (this command should start an instance of ruby somewhere), sends it the code in
|
30
|
+
# <tt>connection.rb</tt> and <tt>client.rb</tt> as well as the code you supply.
|
31
|
+
#
|
32
|
+
# Once the remote ruby instance is set up and ready to go, this code will return (or yield) the connection
|
33
|
+
# and pid of the executed command.
|
34
|
+
#
|
35
|
+
# From this point, you can send and receive objects, and interact with the code you provided within a
|
36
|
+
# remote ruby instance.
|
37
|
+
#
|
38
|
+
# If <tt>command</tt> is a shell such as "/bin/sh", and we need to start ruby separately, you can supply
|
39
|
+
# <tt>options[:ruby] = "/usr/bin/ruby"</tt> to explicitly start the ruby command.
|
40
|
+
def self.start_server(code, command, options = {}, &block)
|
41
|
+
options[:passthrough] = :err unless options[:passthrough]
|
42
|
+
|
43
|
+
send_code = Proc.new do |cin|
|
44
|
+
cin.puts(@@connection_code)
|
45
|
+
cin.puts(@@client_code)
|
46
|
+
cin.puts(code)
|
47
|
+
end
|
48
|
+
|
49
|
+
if block_given?
|
50
|
+
Task.open(command, options) do |process|
|
51
|
+
conn = Connection.build(process, options, &send_code)
|
52
|
+
|
53
|
+
yield conn, process.pid
|
54
|
+
end
|
55
|
+
else
|
56
|
+
process = Task.open(command, options)
|
57
|
+
conn = Connection.build(process, options, &send_code)
|
58
|
+
|
59
|
+
return conn, process.pid
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/rexec/task.rb
ADDED
@@ -0,0 +1,325 @@
|
|
1
|
+
# Copyright (c) 2007 Samuel Williams. Released under the GNU GPLv3.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
class String
|
17
|
+
# Helper for turning a string into a shell argument
|
18
|
+
def to_arg
|
19
|
+
match(/\s/) ? dump : self
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_cmd
|
23
|
+
return self
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Array
|
28
|
+
# Helper for turning an array of items into a command line string
|
29
|
+
# <tt>["ls", "-la", "/My Path"].to_cmd => "ls -la \"/My Path\""</tt>
|
30
|
+
def to_cmd
|
31
|
+
collect{ |a| a.to_arg }.join(" ")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Pathname
|
36
|
+
# Helper for turning a pathname into a command line string
|
37
|
+
def to_cmd
|
38
|
+
to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module RExec
|
43
|
+
RD = 0
|
44
|
+
WR = 1
|
45
|
+
|
46
|
+
# This function closes all IO other than $stdin, $stdout, $stderr
|
47
|
+
def self.close_io(except = [$stdin, $stdout, $stderr])
|
48
|
+
# Make sure all file descriptors are closed
|
49
|
+
ObjectSpace.each_object(IO) do |io|
|
50
|
+
unless except.include?(io)
|
51
|
+
io.close rescue nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Task
|
57
|
+
private
|
58
|
+
def self.pipes_for_options(options)
|
59
|
+
pipes = [[nil, nil], [nil, nil], [nil, nil]]
|
60
|
+
|
61
|
+
if options[:passthrough]
|
62
|
+
passthrough = options[:passthrough]
|
63
|
+
|
64
|
+
if passthrough == :all
|
65
|
+
passthrough = [:in, :out, :err]
|
66
|
+
elsif passthrough.kind_of?(Symbol)
|
67
|
+
passthrough = [passthrough]
|
68
|
+
end
|
69
|
+
|
70
|
+
passthrough.each do |name|
|
71
|
+
case(name)
|
72
|
+
when :in
|
73
|
+
options[:in] = $stdin
|
74
|
+
when :out
|
75
|
+
options[:out] = $stdout
|
76
|
+
when :err
|
77
|
+
options[:err] = $stderr
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
modes = [RD, WR, WR]
|
83
|
+
{:in => 0, :out => 1, :err => 2}.each do |name, idx|
|
84
|
+
m = modes[idx]
|
85
|
+
p = options[name]
|
86
|
+
|
87
|
+
if p.kind_of?(IO)
|
88
|
+
pipes[idx][m] = p
|
89
|
+
elsif p.kind_of?(Array) and p.size == 2
|
90
|
+
pipes[idx] = p
|
91
|
+
else
|
92
|
+
pipes[idx] = IO.pipe
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
return pipes
|
97
|
+
end
|
98
|
+
|
99
|
+
# Close all the supplied pipes
|
100
|
+
def close_pipes(*pipes)
|
101
|
+
pipes.compact!
|
102
|
+
|
103
|
+
pipes.each do |pipe|
|
104
|
+
pipe.close unless pipe.closed?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Dump any remaining data from the pipes, until they are closed.
|
109
|
+
def dump_pipes(*pipes)
|
110
|
+
pipes.compact!
|
111
|
+
|
112
|
+
pipes.delete_if { |pipe| pipe.closed? }
|
113
|
+
# Dump any output that was not consumed (errors, etc)
|
114
|
+
while pipes.size > 0
|
115
|
+
result = IO.select(pipes)
|
116
|
+
|
117
|
+
result[0].each do |pipe|
|
118
|
+
if pipe.closed? || pipe.eof?
|
119
|
+
pipes.delete(pipe)
|
120
|
+
next
|
121
|
+
end
|
122
|
+
|
123
|
+
$stderr.puts pipe.readline.chomp
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
public
|
129
|
+
# Returns true if the given pid is a current process
|
130
|
+
def self.running?(pid)
|
131
|
+
gpid = Process.getpgid(pid) rescue nil
|
132
|
+
|
133
|
+
return gpid != nil ? true : false
|
134
|
+
end
|
135
|
+
|
136
|
+
# Very simple method to spawn a child daemon. A daemon is detatched from the controlling tty, and thus is
|
137
|
+
# not killed when the parent process finishes.
|
138
|
+
# <tt>
|
139
|
+
# spawn_daemon do
|
140
|
+
# Dir.chdir("/")
|
141
|
+
# File.umask 0000
|
142
|
+
# puts "Hello from daemon!"
|
143
|
+
# sleep(600)
|
144
|
+
# puts "This code will not quit when parent process finishes..."
|
145
|
+
# puts "...but $stdout might be closed unless you set it to a file."
|
146
|
+
# end
|
147
|
+
# </tt>
|
148
|
+
def self.spawn_daemon(&block)
|
149
|
+
pid_pipe = IO.pipe
|
150
|
+
|
151
|
+
fork do
|
152
|
+
Process.setsid
|
153
|
+
exit if fork
|
154
|
+
|
155
|
+
# Send the pid back to the parent
|
156
|
+
pid_pipe[RD].close
|
157
|
+
pid_pipe[WR].write(Process.pid.to_s)
|
158
|
+
pid_pipe[WR].close
|
159
|
+
|
160
|
+
yield
|
161
|
+
|
162
|
+
exit(0)
|
163
|
+
end
|
164
|
+
|
165
|
+
pid_pipe[WR].close
|
166
|
+
pid = pid_pipe[RD].read
|
167
|
+
pid_pipe[RD].close
|
168
|
+
|
169
|
+
return pid.to_i
|
170
|
+
end
|
171
|
+
|
172
|
+
# Very simple method to spawn a child process
|
173
|
+
# <tt>
|
174
|
+
# spawn_child do
|
175
|
+
# puts "Hello from child!"
|
176
|
+
# end
|
177
|
+
# </tt>
|
178
|
+
def self.spawn_child(&block)
|
179
|
+
pid = fork do
|
180
|
+
yield
|
181
|
+
|
182
|
+
exit!(0)
|
183
|
+
end
|
184
|
+
|
185
|
+
return pid
|
186
|
+
end
|
187
|
+
|
188
|
+
# Open a process. Similar to IO.popen, but provides a much more generic interface to stdin, stdout,
|
189
|
+
# stderr and the pid. We also attempt to tidy up as much as possible given some kind of error or
|
190
|
+
# exception. You are expected to write to output, and read from input and error.
|
191
|
+
#
|
192
|
+
# = Options =
|
193
|
+
#
|
194
|
+
# We can specify a pipe that will be redirected to the current processes pipe. A typical one is
|
195
|
+
# :err, so that errors in the child process are printed directly to $stderr of the parent process.
|
196
|
+
# <tt>:passthrough => :err</tt>
|
197
|
+
# <tt>:passthrough => [:in, :out, :err]</tt> or <tt>:passthrough => :all</tt>
|
198
|
+
#
|
199
|
+
# We can specify a set of pipes other than the standard ones for redirecting to other things, eg
|
200
|
+
# <tt>:out => File.open("output.log", "a")</tt>
|
201
|
+
#
|
202
|
+
# If you need to supply a pipe manually, you can do that too:
|
203
|
+
# <tt>:in => IO.pipe</tt>
|
204
|
+
#
|
205
|
+
# You can specify <tt>:daemon => true</tt> to cause the child process to detatch. In this
|
206
|
+
# case you will generally want to specify files for <tt>:in, :out, :err</tt> e.g.
|
207
|
+
# <tt>
|
208
|
+
# :in => File.open("/dev/null"),
|
209
|
+
# :out => File.open("/var/log/my.log", "a"),
|
210
|
+
# :err => File.open("/var/log/my.err", "a")
|
211
|
+
# </tt>
|
212
|
+
def self.open(command, options = {}, &block)
|
213
|
+
cin, cout, cerr = pipes_for_options(options)
|
214
|
+
stdpipes = [STDIN, STDOUT, STDERR]
|
215
|
+
|
216
|
+
spawn = options[:daemonize] ? :spawn_daemon : :spawn_child
|
217
|
+
|
218
|
+
cid = self.send(spawn) do
|
219
|
+
[cin[WR], cout[RD], cerr[RD]].compact.each { |pipe| pipe.close }
|
220
|
+
|
221
|
+
STDIN.reopen(cin[RD]) if cin[RD] and !stdpipes.include?(cin[RD])
|
222
|
+
STDOUT.reopen(cout[WR]) if cout[WR] and !stdpipes.include?(cout[WR])
|
223
|
+
STDERR.reopen(cerr[WR]) if cerr[WR] and !stdpipes.include?(cerr[WR])
|
224
|
+
|
225
|
+
if command.respond_to? :call
|
226
|
+
command.call
|
227
|
+
else
|
228
|
+
# If command is a Pathname, we need to convert it to an absolute path if possible,
|
229
|
+
# otherwise if it is relative it might cause problems.
|
230
|
+
if command.respond_to? :realpath
|
231
|
+
command = command.realpath
|
232
|
+
end
|
233
|
+
|
234
|
+
if command.respond_to? :to_cmd
|
235
|
+
exec(command.to_cmd)
|
236
|
+
else
|
237
|
+
exec(command.to_s)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Don't close stdin, stdout, stderr.
|
243
|
+
[cin[RD], cout[WR], cerr[WR]].compact.each { |pipe| pipe.close unless stdpipes.include?(pipe) }
|
244
|
+
|
245
|
+
task = Task.new(cin[WR], cout[RD], cerr[RD], cid)
|
246
|
+
|
247
|
+
if block_given?
|
248
|
+
begin
|
249
|
+
yield task
|
250
|
+
task.close_input
|
251
|
+
return task.wait
|
252
|
+
ensure
|
253
|
+
task.stop
|
254
|
+
end
|
255
|
+
else
|
256
|
+
return task
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def initialize(input, output, error, pid)
|
261
|
+
@input = input
|
262
|
+
@output = output
|
263
|
+
@error = error
|
264
|
+
|
265
|
+
@pid = pid
|
266
|
+
@result = nil
|
267
|
+
end
|
268
|
+
|
269
|
+
attr :input
|
270
|
+
attr :output
|
271
|
+
attr :error
|
272
|
+
attr :pid
|
273
|
+
attr :result
|
274
|
+
|
275
|
+
def running?
|
276
|
+
return self.class.running?(@pid)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Close all connections to the child process
|
280
|
+
def close
|
281
|
+
close_pipes(@input, @output, @error)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Close input pipe to child process (if applicable)
|
285
|
+
def close_input
|
286
|
+
@input.close if @input and !@input.closed?
|
287
|
+
end
|
288
|
+
|
289
|
+
# Send a signal to the child process
|
290
|
+
def kill(signal = "INT")
|
291
|
+
Process.kill("INT", @pid)
|
292
|
+
end
|
293
|
+
|
294
|
+
# Wait for the child process to finish, return the exit status.
|
295
|
+
def wait
|
296
|
+
begin
|
297
|
+
close_input
|
298
|
+
|
299
|
+
_pid, @result = Process.wait2(@pid)
|
300
|
+
|
301
|
+
dump_pipes(@output, @error)
|
302
|
+
ensure
|
303
|
+
close_pipes(@input, @output, @error)
|
304
|
+
end
|
305
|
+
|
306
|
+
return @result
|
307
|
+
end
|
308
|
+
|
309
|
+
# Forcefully stop the child process.
|
310
|
+
def stop
|
311
|
+
# The process has already been stoped/waited upon
|
312
|
+
return if @result
|
313
|
+
|
314
|
+
begin
|
315
|
+
close_input
|
316
|
+
kill
|
317
|
+
wait
|
318
|
+
|
319
|
+
dump_pipes(@output, @error)
|
320
|
+
ensure
|
321
|
+
close_pipes(@output, @error)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|