vuderacha-syrup 0.1.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/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