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
data/README.rdoc
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
= RExec
|
2
|
+
|
3
|
+
Author:: Samuel Williams (http://www.oriontransfer.co.nz/)
|
4
|
+
Copyright:: Copyright (C) 2009, 2010 Samuel Williams
|
5
|
+
License:: GPLv3
|
6
|
+
|
7
|
+
RExec stands for Ruby Execute or Remote Execute (depending on how you use it). It provides a number of different things to assist with running Ruby code:
|
8
|
+
|
9
|
+
* A framework to send Ruby code to a remote server for execution
|
10
|
+
* A framework for writing command line daemons (i.e. <tt>start</tt>, <tt>restart</tt>, <tt>stop</tt>, <tt>status</tt>)
|
11
|
+
* A comprehensive <tt>Task</tt> class for launching tasks, managing input and output, exit status, etc
|
12
|
+
* Basic privilege management code for changing the processes owner
|
13
|
+
* A bunch of helpers for various different things (such as reading a file backwards)
|
14
|
+
* <tt>daemon-exec</tt> executable for running regular shell tasks in the background
|
data/bin/daemon-exec
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Copyright (c) 2010 Samuel Williams. Released under the GNU GPLv3.
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
require 'rubygems'
|
19
|
+
|
20
|
+
require 'rexec'
|
21
|
+
require 'optparse'
|
22
|
+
|
23
|
+
OPTIONS = {
|
24
|
+
:out => "/tmp/daemon-exec.log",
|
25
|
+
:err => "/tmp/daemon-exec-error.log",
|
26
|
+
:in => "/dev/null",
|
27
|
+
:print_pid => false,
|
28
|
+
:root => "/",
|
29
|
+
:verbose => false,
|
30
|
+
:relocate => true,
|
31
|
+
:read_stdin => false,
|
32
|
+
}
|
33
|
+
|
34
|
+
ARGV.options do |o|
|
35
|
+
script_name = File.basename($0)
|
36
|
+
|
37
|
+
o.set_summary_indent("\t")
|
38
|
+
o.banner = "Usage: #{script_name} [-I stdin] [-O stdout] [-E stderr] [script/stdin]"
|
39
|
+
o.define_head "Copyright (c) 2010 Samuel Williams <http://www.oriontransfer.co.nz/>."
|
40
|
+
|
41
|
+
o.on("-d [dir]", String, "Daemons working path, default /") do |dir|
|
42
|
+
OPTIONS[:root] = dir
|
43
|
+
end
|
44
|
+
|
45
|
+
o.on("-s", "Don't attempt to relocate arguments to absolute paths") do
|
46
|
+
OPTIONS[:relocate] = false
|
47
|
+
end
|
48
|
+
|
49
|
+
o.define "File / Pipe Options:"
|
50
|
+
|
51
|
+
o.on("-I [path]", String, "File for STDIN, defaults to #{OPTIONS[:in]}; Note: Use -I - to send data from current STDIN") do |path|
|
52
|
+
OPTIONS[:in] = path
|
53
|
+
end
|
54
|
+
|
55
|
+
o.on("-O [path]", String, "File for STDOUT, defaults to #{OPTIONS[:out]}") do |path|
|
56
|
+
OPTIONS[:in] = path
|
57
|
+
end
|
58
|
+
|
59
|
+
o.on("-E [path]", String, "File for STDERR, defaults to #{OPTIONS[:err]}") do |path|
|
60
|
+
OPTIONS[:err] = path
|
61
|
+
end
|
62
|
+
|
63
|
+
o.define "Misc Options:"
|
64
|
+
|
65
|
+
o.on("-p", "Print out the PID of the forked process") do
|
66
|
+
OPTIONS[:print_pid] = true
|
67
|
+
end
|
68
|
+
|
69
|
+
o.on("-V", "Print verbose information about what is going on") do
|
70
|
+
OPTIONS[:verbose] = true
|
71
|
+
end
|
72
|
+
|
73
|
+
o.on("-h", "Show this help/version information and exit") do
|
74
|
+
puts o
|
75
|
+
exit 0
|
76
|
+
end
|
77
|
+
end.parse!
|
78
|
+
|
79
|
+
if OPTIONS[:relocate]
|
80
|
+
ARGV.collect! do |value|
|
81
|
+
if File.exist?(value)
|
82
|
+
File.expand_path(value)
|
83
|
+
else
|
84
|
+
value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
[:in, :out, :err].each do |path|
|
89
|
+
OPTIONS[path] = File.expand_path(OPTIONS[path]) unless OPTIONS[path] == "-"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if OPTIONS[:verbose]
|
94
|
+
puts "Running #{ARGV.inspect}"
|
95
|
+
end
|
96
|
+
|
97
|
+
task_options = {
|
98
|
+
:daemonize => true,
|
99
|
+
:out => File.open(OPTIONS[:out], "a"),
|
100
|
+
:err => File.open(OPTIONS[:err], "a")
|
101
|
+
}
|
102
|
+
|
103
|
+
if OPTIONS[:in] == '-'
|
104
|
+
task_options[:passthrough] = [:in]
|
105
|
+
end
|
106
|
+
|
107
|
+
daemon = lambda do
|
108
|
+
Dir.chdir(OPTIONS[:root])
|
109
|
+
system("env", *ARGV)
|
110
|
+
end
|
111
|
+
|
112
|
+
task = RExec::Task.open(daemon, task_options)
|
113
|
+
|
114
|
+
if OPTIONS[:print_pid]
|
115
|
+
puts task.pid
|
116
|
+
end
|
data/lib/rexec.rb
ADDED
@@ -0,0 +1,33 @@
|
|
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
|
+
# = Summary =
|
17
|
+
# This gem provides a very simple connection based API for communicating
|
18
|
+
# with remote instances of ruby. These can either be local, or remote, such
|
19
|
+
# as over SSH.
|
20
|
+
#
|
21
|
+
# The API is very simple and deals with sending and receiving objects using
|
22
|
+
# Marshal. One of the primary goals was to impose as little structure as
|
23
|
+
# possible on the end user of this library, while still maintaining a level
|
24
|
+
# of convenience.
|
25
|
+
#
|
26
|
+
# Author:: Samuel Williams (samuel AT oriontransfer DOT org)
|
27
|
+
# Copyright:: Copyright (c) 2009 Samuel Williams.
|
28
|
+
# License:: Released under the GNU GPLv3.
|
29
|
+
|
30
|
+
require 'rexec/version'
|
31
|
+
require 'rexec/connection'
|
32
|
+
require 'rexec/server'
|
33
|
+
require 'rexec/priviledges'
|
data/lib/rexec/client.rb
ADDED
@@ -0,0 +1,23 @@
|
|
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
|
+
# This code is executed in a remote ruby process.
|
17
|
+
|
18
|
+
$stdout.sync = true
|
19
|
+
$stderr.sync = true
|
20
|
+
|
21
|
+
# We don't connect to $stderr here as this is a client. Clients write to regular $stderr.
|
22
|
+
$connection = RExec::Connection.new($stdin, $stdout)
|
23
|
+
|
@@ -0,0 +1,153 @@
|
|
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
|
+
# This class is as small and independant as possible as it will get sent to clients for execution.
|
17
|
+
|
18
|
+
require 'thread'
|
19
|
+
|
20
|
+
module RExec
|
21
|
+
|
22
|
+
# This class represents an abstract connection to another ruby process. The interface does not impose
|
23
|
+
# any structure on the way this communication link works, except for the fact you can send and receive
|
24
|
+
# objects. You can implement whatever kind of idiom you need for communication on top of this library.
|
25
|
+
#
|
26
|
+
# Depending on how you set things up, this can connect to a local ruby process, or a remote ruby process
|
27
|
+
# via SSH (for example).
|
28
|
+
class Connection
|
29
|
+
public
|
30
|
+
|
31
|
+
def self.build(process, options, &block)
|
32
|
+
cin = process.input
|
33
|
+
cout = process.output
|
34
|
+
cerr = process.error
|
35
|
+
|
36
|
+
# We require both cin and cout to be connected in order for connection to work
|
37
|
+
raise InvalidConnectionError.new("Input (#{cin}) or Output (#{cout}) is not connected!") unless cin and cout
|
38
|
+
|
39
|
+
yield cin
|
40
|
+
|
41
|
+
cin.puts("\004")
|
42
|
+
|
43
|
+
return self.new(cout, cin, cerr)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create a new connection. You need to supply a pipe for reading input, a pipe for sending output,
|
47
|
+
# and optionally a pipe for errors to be read from.
|
48
|
+
def initialize(input, output, error = nil)
|
49
|
+
@input = input
|
50
|
+
@output = output
|
51
|
+
@running = true
|
52
|
+
|
53
|
+
@error = error
|
54
|
+
|
55
|
+
@receive_mutex = Mutex.new
|
56
|
+
@send_mutex = Mutex.new
|
57
|
+
end
|
58
|
+
|
59
|
+
# The pipe used for reading data
|
60
|
+
def input
|
61
|
+
@input
|
62
|
+
end
|
63
|
+
|
64
|
+
# The pipe used for writing data
|
65
|
+
def output
|
66
|
+
@output
|
67
|
+
end
|
68
|
+
|
69
|
+
# The pipe used for receiving errors. On the client side this pipe is writable, on the server
|
70
|
+
# side this pipe is readable. You should avoid using it on the client side and simply use $stderr.
|
71
|
+
def error
|
72
|
+
@error
|
73
|
+
end
|
74
|
+
|
75
|
+
# Stop the connection, and close the output pipe.
|
76
|
+
def stop
|
77
|
+
@running = false
|
78
|
+
@output.close
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return whether or not the connection is running.
|
82
|
+
def running?
|
83
|
+
@running
|
84
|
+
end
|
85
|
+
|
86
|
+
# This is a very simple runloop. It provides an object when it is received.
|
87
|
+
def run(&block)
|
88
|
+
while @running
|
89
|
+
pipes = IO.select([@input])
|
90
|
+
|
91
|
+
if pipes[0].size > 0
|
92
|
+
object = receive_object
|
93
|
+
|
94
|
+
if object == nil
|
95
|
+
@running = false
|
96
|
+
return
|
97
|
+
end
|
98
|
+
|
99
|
+
begin
|
100
|
+
yield object
|
101
|
+
rescue Exception => ex
|
102
|
+
send_object(ex)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Dump any text which has been written to $stderr in the child process.
|
109
|
+
def dump_errors(to = $stderr)
|
110
|
+
if @error and !@error.closed?
|
111
|
+
while true
|
112
|
+
result = IO.select([@error], [], [], 0)
|
113
|
+
|
114
|
+
break if result == nil
|
115
|
+
|
116
|
+
to.puts @error.readline.chomp
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Receive an object from the connection. This function is thread-safe. This function may block.
|
122
|
+
def receive_object
|
123
|
+
object = nil
|
124
|
+
|
125
|
+
@receive_mutex.synchronize do
|
126
|
+
begin
|
127
|
+
object = Marshal.load(@input)
|
128
|
+
rescue EOFError
|
129
|
+
object = nil
|
130
|
+
@running = false
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
if object and object.kind_of?(Exception)
|
135
|
+
raise object
|
136
|
+
end
|
137
|
+
|
138
|
+
return object
|
139
|
+
end
|
140
|
+
|
141
|
+
# Send object(s). This function is thread-safe.
|
142
|
+
def send_object(*objects)
|
143
|
+
@send_mutex.synchronize do
|
144
|
+
objects.each do |o|
|
145
|
+
data = Marshal.dump(o)
|
146
|
+
@output.write(data)
|
147
|
+
end
|
148
|
+
|
149
|
+
@output.flush
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/rexec/daemon.rb
ADDED
@@ -0,0 +1,27 @@
|
|
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/base'
|
17
|
+
|
18
|
+
module RExec
|
19
|
+
module Daemon
|
20
|
+
|
21
|
+
# Would this kind of API be useful?
|
22
|
+
#def run_daemon(options = {}, &block)
|
23
|
+
#
|
24
|
+
#end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,151 @@
|
|
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 'fileutils'
|
17
|
+
require 'rexec/daemon/controller'
|
18
|
+
require 'rexec/reverse_io'
|
19
|
+
|
20
|
+
module RExec
|
21
|
+
module Daemon
|
22
|
+
# This class is the base daemon class. If you are writing a daemon, you should inherit from this class.
|
23
|
+
class Base
|
24
|
+
@@var_directory = nil
|
25
|
+
@@log_directory = nil
|
26
|
+
@@pid_directory = nil
|
27
|
+
|
28
|
+
# Return the name of the daemon
|
29
|
+
def self.daemon_name
|
30
|
+
return name.gsub(/[^a-zA-Z0-9]+/, '-')
|
31
|
+
end
|
32
|
+
|
33
|
+
# Base directory for daemon log files / run files
|
34
|
+
def self.var_directory
|
35
|
+
@@var_directory || File.join("", "var")
|
36
|
+
end
|
37
|
+
|
38
|
+
# The directory the daemon will run in (Dir.chdir)
|
39
|
+
def self.working_directory
|
40
|
+
var_directory
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return the directory to store log files in
|
44
|
+
def self.log_directory
|
45
|
+
@@log_directory || File.join(var_directory, "log", daemon_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Standard log file for errors
|
49
|
+
def self.err_fn
|
50
|
+
File.join(log_directory, "stderr.log")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Standard log file for normal output
|
54
|
+
def self.log_fn
|
55
|
+
File.join(log_directory, "stdout.log")
|
56
|
+
end
|
57
|
+
|
58
|
+
# Standard location of pid file
|
59
|
+
def self.pid_directory
|
60
|
+
@@pid_directory || File.join(var_directory, "run", daemon_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Standard pid file
|
64
|
+
def self.pid_fn
|
65
|
+
File.join(pid_directory, "#{daemon_name}.pid")
|
66
|
+
end
|
67
|
+
|
68
|
+
# Mark the error log
|
69
|
+
def self.mark_err_log
|
70
|
+
fp = File.open(err_fn, "a")
|
71
|
+
fp.puts "=== Error Log Opened @ #{Time.now.to_s} ==="
|
72
|
+
fp.close
|
73
|
+
end
|
74
|
+
|
75
|
+
# Prints some information relating to daemon startup problems
|
76
|
+
def self.tail_err_log(outp)
|
77
|
+
lines = []
|
78
|
+
|
79
|
+
File.open(err_fn, "r") do |fp|
|
80
|
+
fp.seek_end
|
81
|
+
|
82
|
+
fp.reverse_each_line do |line|
|
83
|
+
lines << line
|
84
|
+
break if line.match("=== Error Log") || line.match("=== Daemon Exception Backtrace")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
lines.reverse_each do |line|
|
89
|
+
outp.puts line
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Check the last few lines of the log file to find out if
|
94
|
+
# the daemon crashed.
|
95
|
+
def self.crashed?
|
96
|
+
File.open(err_fn, "r") do |fp|
|
97
|
+
fp.seek_end
|
98
|
+
|
99
|
+
count = 2
|
100
|
+
fp.reverse_each_line do |line|
|
101
|
+
return true if line.match("=== Daemon Crashed")
|
102
|
+
|
103
|
+
count -= 1
|
104
|
+
|
105
|
+
break if count == 0
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
return false
|
110
|
+
end
|
111
|
+
|
112
|
+
# Corresponds to controller method of the same name
|
113
|
+
def self.daemonize
|
114
|
+
Controller.daemonize(self)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Corresponds to controller method of the same name
|
118
|
+
def self.start
|
119
|
+
Controller.start(self)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Corresponds to controller method of the same name
|
123
|
+
def self.stop
|
124
|
+
Controller.stop(self)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Corresponds to controller method of the same name
|
128
|
+
def self.status
|
129
|
+
Controller.status(self)
|
130
|
+
end
|
131
|
+
|
132
|
+
# The main function to setup any environment required by the daemon
|
133
|
+
def self.prefork
|
134
|
+
@@var_directory = File.expand_path(@@var_directory) if @@var_directory
|
135
|
+
@@log_directory = File.expand_path(@@log_directory) if @@log_directory
|
136
|
+
@@pid_directory = File.expand_path(@@pid_directory) if @@pid_directory
|
137
|
+
|
138
|
+
FileUtils.mkdir_p(log_directory)
|
139
|
+
FileUtils.mkdir_p(pid_directory)
|
140
|
+
end
|
141
|
+
|
142
|
+
# The main function to start the daemon
|
143
|
+
def self.run
|
144
|
+
end
|
145
|
+
|
146
|
+
# The main function to stop the daemon
|
147
|
+
def self.shutdown
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|