vuderacha-syrup 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,57 @@
1
+ = Syrup - Service Management
2
+ Syrup provides mechanism for managing your application daemons. Instead of needing
3
+ to manually background processes (such as queue workers), you just configure
4
+ a config.sy file, and then start and stop it.
5
+
6
+ == Getting Started
7
+ Syrup assumes that you'll deploy one application group per user account. You can
8
+ run more if you want to, but Syrup makes it a lot easier if you don't...
9
+
10
+ Firstly, download and install syrup
11
+ git clone git://github.com/vuderacha/syrup.git
12
+ cd syrup
13
+ rake install
14
+
15
+ Assuming you have a simple application such as:
16
+ # test_service.rb
17
+ while true
18
+ File.open('/tmp/test.txt', 'a') {|f| f << "a\n"}
19
+ sleep 2
20
+
21
+ Create a configuration file for Syrup to run it:
22
+ # config.sy
23
+ service 'myservice', 'test-service.rb'
24
+
25
+ For Syrup to use an application, you need to "activate" it. This informs Syrup about the
26
+ path of the application, and allows it to automatically start the app again later when you
27
+ may not be around to tell it about it! Assuming that you've put your app into /home/syruptest/app,
28
+ then you'll just issue the command:
29
+ syrup activate /home/syruptest/app
30
+
31
+ This will create a file called "activated" in /home/syruptest/.syrup containing the path to this app.
32
+
33
+ Finally, start the application with:
34
+ syrup start
35
+
36
+ Conversely, stop it with:
37
+ syrup stop
38
+
39
+ == Supported Application Types
40
+ Syrup currently supports two types of application. Services are declared in the form
41
+ service "<name>", "<file>"
42
+
43
+ Rack applications are also supported. To run a rack application, add the following to your config.sy:
44
+ rack "<name>", "<rack arguments>"
45
+ Note that if your config.ru doesn't require any arguments, then the second parameter is entirely optional.
46
+
47
+ == Persistent Configuration
48
+ Often, applications need per deployment configuration. One example is configuring whether a given host is
49
+ production or development. Whilst there are many ways to do this, Syrup adds yet another. Within a given
50
+ account, commands like the following can be issued:
51
+ syrup set PROP=VALUE
52
+ This property will be permanently stored (in ~/.syrup/props), and will be applied into the environment of
53
+ all applications loaded through Syrup at their next start. So, for example, if your app had a line that read
54
+ puts RACK_ENV
55
+ then executing
56
+ syrup set RACK_ENV=production
57
+ would result in the application reporting "production" upon reaching the aforementioned line.
data/Rakefile ADDED
@@ -0,0 +1,69 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run all specs"
5
+ Spec::Rake::SpecTask.new('spec') do |t|
6
+ t.spec_opts = ["-cfs"]
7
+ t.spec_files = FileList['spec/**/*_spec.rb']
8
+ t.libs = ['lib']
9
+ end
10
+
11
+ desc "Print specdocs"
12
+ Spec::Rake::SpecTask.new(:doc) do |t|
13
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
14
+ t.spec_files = FileList['spec/*_spec.rb']
15
+ end
16
+
17
+ desc "Generate RCov code coverage report"
18
+ Spec::Rake::SpecTask.new('rcov') do |t|
19
+ t.spec_files = FileList['spec/*_spec.rb']
20
+ t.rcov = true
21
+ t.rcov_opts = ['--exclude', 'examples']
22
+ end
23
+
24
+ task :default => :spec
25
+
26
+ ######################################################
27
+
28
+ require 'rake'
29
+ require 'rake/testtask'
30
+ require 'rake/clean'
31
+ require 'rake/gempackagetask'
32
+ require 'rake/rdoctask'
33
+ require 'fileutils'
34
+ include FileUtils
35
+
36
+ begin
37
+ require 'jeweler'
38
+ Jeweler::Tasks.new do |s|
39
+ s.name = "syrup"
40
+ s.summary = "Ruby service application manager."
41
+ s.description = "Syrup is a process manager for working with services. It provides the ability to deploy and manage long-running services."
42
+ s.author = "Paul Jones"
43
+ s.email = "pauljones23@gmail.com"
44
+ s.homepage = "http://github.com/vuderacha/syrup/"
45
+ s.executables = [ "syrup" ]
46
+ s.default_executable = "syrup"
47
+
48
+ s.files = %w(Rakefile README.rdoc VERSION.yml) + Dir.glob("{bin,lib,spec}/**/*")
49
+
50
+ s.require_path = "lib"
51
+ s.bindir = "bin"
52
+ end
53
+ rescue LoadError
54
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
55
+ end
56
+
57
+ task :test => [ :spec ]
58
+
59
+ Rake::RDocTask.new do |t|
60
+ t.rdoc_dir = 'rdoc'
61
+ t.title = "Syrup -- Process Manager"
62
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
63
+ t.options << '--charset' << 'utf-8'
64
+ t.rdoc_files.include('README.rdoc')
65
+ t.rdoc_files.include('lib/syrup/*.rb')
66
+ end
67
+
68
+ CLEAN.include [ 'build/*', '**/*.o', '**/*.so', '**/*.a', 'lib/*-*', '**/*.log', 'pkg', 'lib/*.bundle', '*.gem', '.config' ]
69
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 0
4
+ :minor: 1
data/bin/syrup ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'syrup'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'syrup'
8
+ end
9
+ a = Syrup::Application.new
10
+ a.run ARGV
data/lib/launcher.rb ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'syrup'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'syrup'
8
+ end
9
+ a = Syrup::Application.new
10
+ a.run ARGV
@@ -0,0 +1,169 @@
1
+ module Syrup
2
+ # Module that allows a given object to store a Fabric. Requires the including module to have
3
+ # a fabric_fn method.
4
+ module WithFabric
5
+ def fabric
6
+ return File.read(fabric_fn) if File.file? fabric_fn
7
+ return nil
8
+ end
9
+
10
+ def fabric=(path)
11
+ unless path.nil?
12
+ File.open(fabric_fn, 'w'){ |f| f.write(File.expand_path(path)) }
13
+ else
14
+ File.delete fabric_fn if File.file? fabric_fn
15
+ end
16
+ end
17
+ end
18
+
19
+ class ConfigStore
20
+ include WithFabric
21
+
22
+ attr_reader :properties
23
+
24
+ # Initializes the configuration store with the given storage directory.
25
+ def initialize(path)
26
+ @path = path
27
+ @properties = StoredHash.new File.join(@path, 'props')
28
+ end
29
+
30
+ # Retrieves all of the applications that are configured within the system
31
+ def applications
32
+ apps = {}
33
+ Dir[File.join(@path, '*')].each do |store_dir|
34
+ name = File.basename(store_dir)
35
+
36
+ apps[name] = ApplicationConfiguration.new name, store_dir
37
+ end
38
+
39
+ apps
40
+ end
41
+
42
+ # Creates a new application
43
+ def create_application(name)
44
+ store_dir = File.join(@path, name)
45
+ FileUtils.mkdir_p store_dir
46
+
47
+ ApplicationConfiguration.new name, store_dir
48
+ end
49
+
50
+ def fabric_fn
51
+ File.join(@path, 'fabric')
52
+ end
53
+ end
54
+
55
+ class ApplicationConfiguration
56
+ include WithFabric
57
+
58
+ attr_reader :name
59
+ attr_reader :properties
60
+
61
+ def initialize(name, path)
62
+ @name = name
63
+ @path = path
64
+ @properties = StoredHash.new File.join(@path, 'props')
65
+ @start_parameters = if File.file? start_params_fn then YAML::load_file(start_params_fn) else [] end
66
+ end
67
+
68
+ def app
69
+ return File.read(activated_fn) if File.file? activated_fn
70
+ return nil
71
+ end
72
+
73
+ def app=(path)
74
+ unless path.nil?
75
+ File.open(activated_fn, 'w'){ |f| f.write(File.expand_path(path)) }
76
+ else
77
+ File.delete activated_fn if File.file? activated_fn
78
+ end
79
+ end
80
+
81
+ def pid
82
+ result = File.read(pid_fn) rescue nil
83
+ return result.to_i unless result.nil?
84
+
85
+ nil
86
+ end
87
+
88
+ def pid=(i)
89
+ unless i.nil?
90
+ File.open(pid_fn, 'w'){ |f| f.write(i) }
91
+ else
92
+ File.delete pid_fn if File.file? pid_fn
93
+ end
94
+ end
95
+
96
+ def start_parameters
97
+ @start_parameters
98
+ end
99
+
100
+ def start_parameters=(params)
101
+ @start_parameters = params
102
+
103
+ unless params.nil? or params.length == 0
104
+ File.open(start_params_fn, 'w') { |f| f.write(params.to_yaml) }
105
+ else
106
+ File.delete start_params_fn if File.file? start_params_fn
107
+ end
108
+ end
109
+
110
+ def activated_fn
111
+ File.join(@path, 'activated')
112
+ end
113
+
114
+ def props_fn
115
+ File.join(@path, 'props')
116
+ end
117
+
118
+ def fabric_fn
119
+ File.join(@path, 'fabric')
120
+ end
121
+
122
+ def start_params_fn
123
+ File.join(@path, 'start_params')
124
+ end
125
+
126
+ def pid_fn
127
+ File.join(@path, 'pid')
128
+ end
129
+ end
130
+
131
+ class StoredHash
132
+ def initialize(file)
133
+ @file = file
134
+ @props = if File.file? file then YAML::load_file(file) else {} end
135
+ end
136
+
137
+ def [](k)
138
+ @props[k]
139
+ end
140
+
141
+ def []=(k, v)
142
+ @props[k] = v
143
+
144
+ save!
145
+ end
146
+
147
+ def length
148
+ @props.length
149
+ end
150
+
151
+ def clear
152
+ @props.clear
153
+ File.delete @file if File.file? @file
154
+ end
155
+
156
+ def delete(key)
157
+ @props.delete(key)
158
+ save!
159
+ end
160
+
161
+ def each(&block)
162
+ @props.each(&block)
163
+ end
164
+
165
+ def save!
166
+ File.open(@file, 'w') {|f| f << @props.to_yaml}
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,58 @@
1
+ require 'fileutils'
2
+
3
+ module Syrup
4
+ class Daemon
5
+ def initialize(config_directory, app)
6
+ @config_directory = config_directory
7
+ @app = app
8
+ end
9
+
10
+ def start
11
+ # Check if the process is already running first
12
+ if File.file? pid_fn
13
+ running = (not Process.getpgid(recall_pid).nil?) rescue false
14
+ if running
15
+ puts "Syrup is already running. Stop Syrup before trying to start it"
16
+ exit
17
+ end
18
+ end
19
+
20
+ fork do
21
+ Process.setsid
22
+ exit if fork
23
+ store_pid(Process.pid)
24
+ #Dir.chdir WorkingDirectory
25
+ File.umask 0000
26
+ STDIN.reopen "/dev/null"
27
+ STDOUT.reopen "/dev/null", "a"
28
+ STDERR.reopen STDOUT
29
+ trap("TERM") {@app.stop; exit}
30
+ @app.start
31
+ end
32
+ end
33
+
34
+ def stop
35
+ if !File.file?(pid_fn)
36
+ puts "Pid file not found. Is the daemon started?"
37
+ exit
38
+ end
39
+ pid = recall_pid
40
+ FileUtils.rm(pid_fn)
41
+ pid && Process.kill("TERM", pid)
42
+ end
43
+
44
+ private
45
+ def pid_fn
46
+ File.join(File.expand_path(@config_directory), 'syrup.pid')
47
+ end
48
+
49
+ def store_pid(pid)
50
+ FileUtils.mkdir_p File.dirname(pid_fn)
51
+ File.open(pid_fn, 'w') {|f| f << pid}
52
+ end
53
+
54
+ def recall_pid
55
+ IO.read(pid_fn).to_i rescue nil
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,20 @@
1
+ module Syrup
2
+ class Executor
3
+ # Executes a script in the "foreground" (ie, it only forks once). The pid is recorded
4
+ # in memory, and the process will return.
5
+ def execute_foreground(name, script, fabric, props)
6
+
7
+ end
8
+
9
+ # Kills all processes running in the foreground.
10
+ def kill_all_foreground
11
+ end
12
+
13
+ # Starts a script as a daemon. Returns the pid.
14
+ def execute_daemon(name, script, fabric, props)
15
+ end
16
+
17
+ def stop_daemon(name, pid)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ # Support functions added to the kernel for Fabrics to work with
2
+ module Kernel
3
+ # Indicates that a Fabric feature with the given name is required for the application to
4
+ # proceed. If the feature is initialized on demand, then it is expected that this feature
5
+ # will be initialized before this method returns.
6
+ def fabric_requirement(name)
7
+ Syrup.fabric_support.require_feature(name)
8
+ end
9
+ end
10
+
11
+ module Syrup
12
+ class FabricSupport
13
+ def initialize
14
+ @features = []
15
+ end
16
+
17
+ def require_feature(name)
18
+ raise "Feature #{name} was not provided by the Fabric!" unless @features.include? name
19
+ end
20
+
21
+ def register_feature(name)
22
+ @features << name
23
+ end
24
+ end
25
+
26
+ class <<self
27
+ def fabric_support
28
+ @fabric_support ||= Syrup::FabricSupport.new
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ # Very simple default fabric. Just execute the application!
2
+ Syrup::Runner.run_application
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'rack'
3
+
4
+ Syrup::Runner.run_application
@@ -0,0 +1,253 @@
1
+ require 'syrup/config_store'
2
+ require 'yaml'
3
+
4
+ module Syrup
5
+ # Manager for controlling the Syrup functionality
6
+ class Manager
7
+ def initialize(config_dir, logger, verbose)
8
+ @config_dir = config_dir
9
+ @logger = logger
10
+ @verbose = verbose
11
+
12
+ @config_store = Syrup::ConfigStore.new @config_dir
13
+ @runner = Syrup::Runner.new @config_store, @logger
14
+ end
15
+
16
+ # Informs the manager to start all configured applications
17
+ def start_all
18
+ if @config_store.applications.length == 0
19
+ @logger.warn 'No Applications activated yet. Nothing to do!'
20
+ return true
21
+ end
22
+
23
+ @config_store.applications.each do |app_name, app|
24
+ start(app_name)
25
+ end
26
+ end
27
+
28
+ # Informs the manager to start the specified application
29
+ def start(app_name)
30
+ # Retrieve the details of the application to check that it is valid
31
+ app = get_application(app_name, false)
32
+ return false if app.nil?
33
+
34
+ fork do
35
+ Process.setsid
36
+ exit if fork
37
+
38
+ # Record our PID
39
+ app.pid = Process.pid
40
+
41
+ #Dir.chdir @working_dir
42
+ File.umask 0000
43
+ STDIN.reopen "/dev/null"
44
+ STDOUT.reopen "log/#{app_name}.txt", "a"
45
+ STDERR.reopen STDOUT
46
+ trap("TERM") {exit}
47
+
48
+ at_exit do
49
+ # Release our pid
50
+ app.pid = nil
51
+ end
52
+
53
+ # Run the application
54
+ run_app(app_name)
55
+ end
56
+
57
+ return true
58
+ end
59
+
60
+ # Informs the manager to stop all configured applications
61
+ def stop_all
62
+ if @config_store.applications.length == 0
63
+ @logger.warn 'No Applications activated yet. Nothing to do!'
64
+ return true
65
+ end
66
+
67
+ stop(@config_store.applications.keys)
68
+ end
69
+
70
+ # Informs the manager to stop the named applications
71
+ def stop(app_names)
72
+ pids = []
73
+ @config_store.applications.each do |app_name, app|
74
+ pids << app.pid if app_names.include? app_name and not app.pid.nil?
75
+ end
76
+
77
+ kill_pids pids
78
+ end
79
+
80
+ # Informs the manager to activate the given path
81
+ def activate name, path, args
82
+ # Retrieve ourselves an application object
83
+ app = @config_store.applications[name]
84
+ app = @config_store.create_application(name) if app.nil?
85
+
86
+ # Store the path to the application
87
+ app.app = File.expand_path(path)
88
+ app.start_parameters = args
89
+
90
+ true
91
+ end
92
+
93
+ # Informs the manager to run the given application in the foreground
94
+ def run(names)
95
+ # Create sub-processes for each of the named applications
96
+ pids = []
97
+ names.each do |name|
98
+ pids << fork do
99
+ run_app(name)
100
+ end
101
+ end
102
+
103
+ # Register an at_exit handler to kill off all the pids
104
+ at_exit do
105
+ kill_pids pids
106
+ end
107
+
108
+ # Wait for all children to die
109
+ Process.waitall.each do |pid, status|
110
+ # We won't need to kill the processes that successfully exited
111
+ pids.delete pid if status.exited?
112
+ end
113
+ end
114
+
115
+ # Informs the manager to run the given application within the current process
116
+ def run_app(name)
117
+ @runner.run(name)
118
+ end
119
+
120
+ # Requests that the manager store the given variables as persistent configuration for the given application.
121
+ def set_app_properties(app_name, properties)
122
+ app = @config_store.applications[app_name]
123
+ if app.nil?
124
+ @logger.error "Unknown Application. Cannot set properties."
125
+ return false
126
+ end
127
+
128
+ props.each do |pair|
129
+ k,v = pair.split('=')
130
+ if k.nil? or v.nil?
131
+ @logger.error "Invalid set command. #{pair} not in the form K=V"
132
+ return false
133
+ end
134
+
135
+ app.properties[k] = v
136
+ end
137
+ end
138
+
139
+ # Requests that the manager store the given variables as persistent configuration that will be
140
+ # restored when all applications are started
141
+ def set_global_properties(props)
142
+ props.each do |pair|
143
+ k,v = pair.split('=')
144
+ if k.nil? or v.nil?
145
+ @logger.error "Invalid set command. #{pair} not in the form K=V"
146
+ return false
147
+ end
148
+
149
+ @config_store.properties[k] = v
150
+ end
151
+ end
152
+
153
+ # Requests that the manager remove the given keys from the stored properties for the given app.
154
+ def unset_app_properties(app_name, props)
155
+ app = @config_store.applications[app_name]
156
+ if app.nil?
157
+ @logger.error "Unknown Application. Cannot unset properties."
158
+ return false
159
+ end
160
+
161
+ props.each do |prop|
162
+ app.properties.delete prop
163
+ end
164
+ end
165
+
166
+ # Requests that the manager remove the given keys from the stored properties
167
+ def unset_global_properties(props)
168
+ props.each do |prop|
169
+ @config_store.properties.delete prop
170
+ end
171
+ end
172
+
173
+ # Removes all stored properties for the current profile
174
+ def clear_app_properties
175
+ app = @config_store.applications[app_name]
176
+ return true if app.nil?
177
+
178
+ app.properties.clear
179
+ end
180
+
181
+ # Removes all stored properties for the current profile
182
+ def clear_global_properties
183
+ @config_store.properties.clear
184
+ end
185
+
186
+ # Requests that the manager load the given fabric for applications within the current profile
187
+ def weave_global(fabric)
188
+ @config_store.fabric = fabric
189
+ end
190
+
191
+ def unweave_global
192
+ @config_store.fabric = nil
193
+ end
194
+
195
+ def weave_for_application(app_name, fabric)
196
+ app = get_application(app_name, false)
197
+ return false if app.nil?
198
+
199
+ app.fabric = fabric
200
+ end
201
+
202
+ def unweave_for_application(app_name)
203
+ app = get_application(app_name, false)
204
+ return false if app.nil?
205
+
206
+ app.fabric = nil
207
+ end
208
+
209
+ private
210
+ def get_application(app_name, create_if_missing)
211
+ app = @config_store.applications[app_name]
212
+ app = @config_store.create_application(app_name) if create_if_missing and app.nil?
213
+
214
+ if app.nil?
215
+ @logger.error "Unknown Application #{app_name}"
216
+ end
217
+
218
+ app
219
+ end
220
+
221
+ def kill_pids(pids)
222
+ # List of pids waiting at each round
223
+ waiting_pids = []
224
+
225
+ (1..5).each do
226
+ waiting_pids = []
227
+ pids.each do |pid|
228
+ pid && Process.kill("TERM", pid) && waiting_pids << pid rescue puts "WARNING: Failed to kill #{pid}"
229
+ end
230
+
231
+ # Wait for the process to die
232
+ (1..20).each do
233
+ waiting_pids.each do |pid|
234
+ running = (not Process.getpgid(pid).nil?) rescue false
235
+ waiting_pids.delete pid unless running
236
+ end
237
+
238
+ break if waiting_pids.empty?
239
+ STDERR.write "."
240
+ sleep 0.5
241
+ end
242
+
243
+ break if waiting_pids.empty?
244
+ pids.replace waiting_pids
245
+ end
246
+
247
+ # Write a newline to clear the '.'s
248
+ STDERR.write "\n"
249
+
250
+ @logger.warn "Process(es) #{waiting_pids.inspect} did not terminate" unless waiting_pids.empty?
251
+ end
252
+ end
253
+ end