spectate 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ *.tmproj
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Stephen Eley
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ = spectate
2
+
3
+
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Stephen Eley. See LICENSE for details.
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "spectate"
8
+ gem.summary = %Q{General event framework for testing and monitoring}
9
+ gem.email = "sfeley@gmail.com"
10
+ gem.homepage = "http://github.com/SFEley/spectate"
11
+ gem.authors = ["Stephen Eley"]
12
+ gem.rubyforge_project = "spectate"
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+
15
+ # ADDED BY SFE
16
+ gem.requirements << "Tokyo Cabinet (easily installed from apt, MacPorts, etc.)"
17
+ gem.add_development_dependency "rspec", ">= 1.2.6"
18
+ gem.add_development_dependency "mocha", ">= 0.9.5"
19
+ gem.add_runtime_dependency "rufus-tokyo", ">=0.1.12"
20
+ end
21
+
22
+ Jeweler::RubyforgeTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
25
+ end
26
+
27
+ require 'spec/rake/spectask'
28
+ Spec::Rake::SpecTask.new(:spec) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.spec_files = FileList['spec/**/*_spec.rb']
31
+ end
32
+
33
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
34
+ spec.libs << 'lib' << 'spec'
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ begin
40
+ require 'cucumber/rake/task'
41
+ Cucumber::Rake::Task.new(:features)
42
+ rescue LoadError
43
+ task :features do
44
+ abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
45
+ end
46
+ end
47
+
48
+ task :default => :spec
49
+
50
+ require 'rake/rdoctask'
51
+ Rake::RDocTask.new do |rdoc|
52
+ if File.exist?('VERSION.yml')
53
+ config = YAML.load(File.read('VERSION.yml'))
54
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
55
+ else
56
+ version = ""
57
+ end
58
+
59
+ rdoc.rdoc_dir = 'rdoc'
60
+ rdoc.title = "spectate #{version}"
61
+ rdoc.rdoc_files.include('README*')
62
+ rdoc.rdoc_files.include('lib/**/*.rb')
63
+ end
64
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,58 @@
1
+ The Spectate Model
2
+ ==================
3
+
4
+ The Spectate server follows a consistent tree-based model for storing the status of...well, whatever. File changes. Unit test results. Git commits. We don't care. It's optimized for rapid and systematic notification of changes. There are three interesting types of applications in the Spectate system:
5
+
6
+ 1. The **Spectate server** provides REST-based access to the data store and notifies spectators when data changes. The reference implementation is written in Sinatra, with a Tokyo Cabinet B+ tree for a data store. Individual nodes or arbitrarily deep collections can be retrieved quickly and with the same mechanics.
7
+ 2. **Agents** run on the client side and make updates to the server when something interesting happens.
8
+ 3. **Spectators** are Web hooks which register themselves to the server on one or more nodes, and are notified of changes to those nodes or any of their children.
9
+
10
+ There's also a command line client to add and remove services, but it's not as interesting.
11
+
12
+ The rest of this document deals with the Spectate server, its data hierarchy and event model, and the API for communicating with it. The architecture of agents and spectators left for service implementers.
13
+
14
+ Anatomy of a Node
15
+ -----------------
16
+ The Spectate data store is a key-value store in which the keys compose a tree structure consisting of _nodes_ and _properties._ These types differ only slightly in their key declaration and are consistent with URI standards, allowing for a flat and simple implementation.
17
+
18
+ * **Nodes** represent objects of any sort -- directories and files, tasks, unit tests, anything that can be graphed hierarchically. The _node key_ is the node's full URI for server retrieval: e.g., `http://localhost:20574/projects/spectate/doc/api.mdown`. Any node can have any number of child nodes and properties. _System nodes_ are ordinary nodes with a base name beginning with an underscore '\_'; this convention denotes data for Spectate's internal use, and is not ordinarily shown in query results.
19
+ * **Properties** are attributes of nodes, and are denoted by a URI fragment identifier (aka a hash sign, aka '#'). The _property key_ is the property's full URI for server retrieval: e.g., `http://localhost:20574/projects/spectate/doc/api.mdown#last_modified`. Properties may not contain children, but may have multiple values, i.e. an array.
20
+
21
+
22
+ ### (Stopped rewriting here.)
23
+ You can also define any other properties or behavior you wish in subclasses. You can make an property _persistent_ by using the **spectates** declaration:
24
+
25
+ class NiftySpectator < Spectate::Spectator
26
+ spectates :niftiness,
27
+ :coolness
28
+ spectates :hipness, :quiet => true
29
+ end
30
+
31
+ Internally, persistent properties are just standalone key/value pairs; the key is the concatenation of the spectator's path, the URI _fragment_ identifier (i.e. the hash symbol, '#') and the property's name. (E.g. `/niftyThings/NiftyExample1#coolness`.) The _quiet_ option prevents hooks from running when the property is updated.
32
+
33
+ Ordinarily, every property update is saved immediately, and each hook is notified upon each update. If you're assigning several properties at once or performing operations on a chain of spectators, you should use the **update** method:
34
+
35
+ spectator.update do |s|
36
+ s.niftiness = :very
37
+ s.coolness = -40
38
+ s.hipness = "tragic"
39
+ end
40
+
41
+ **update** offers simple transaction behavior: it defers saving and hook announcement _for all spectators_ until the block is completed. This is useful for changes that trickle down to a spectator's children. If an exception is raised out of the block, all changes are canceled and nothing is saved or announced.
42
+
43
+ If you want to defer saving and announcement _globally_, there is also a class method version (**Spectate::Spectator.update**) which supplies no parameters, but otherwise offers the same behavior as calling the instance method on the tree's root node.
44
+
45
+ Sociology of Spectators
46
+ -----------------------
47
+ [describe the tree]
48
+
49
+ The following read-only attributes are available for convenience:
50
+
51
+ * **path** is the URI path used to uniquely locate this spectator in REST requests. It also indicates the spectator's position in the storage tree, and is (not coincidentally) the key value used for hash storage. The path is the concatenation of the parent's path and a URI-safe version of the spectator's name. All of the following relationship attributes are based on transformations of the path.
52
+ * **parent** is the parent spectator.
53
+ * **children** is an array of all spectators immediately descended from this one.
54
+ * **descendants** is an array of the spectator's children, all of _their_ children, and so on, in breadth-first order.
55
+ * **siblings** is an array of all of the parent's children, excluding the current spectator.
56
+
57
+
58
+
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))
3
+
4
+ require 'spectate/command'
5
+ exit Spectate::Command.run
@@ -0,0 +1,16 @@
1
+ Feature: Command line utility
2
+ In order to easily control everything about Spectate
3
+ As a user
4
+ I want to call "spectate" from the command line
5
+
6
+ Scenario: Usage help
7
+ When I execute "spectate --help"
8
+ Then I should see "Usage:"
9
+
10
+ Scenario: Basic startup
11
+ Given no Spectate is running
12
+ When I execute "spectate"
13
+ Then I should see "Starting Spectate"
14
+ And Spectate should be running
15
+
16
+
@@ -0,0 +1,7 @@
1
+ When /^I execute "([^\"]*)"$/ do |command|
2
+ @output = `#{command}`
3
+ end
4
+
5
+ Then /^I should see "([^\"]*)"$/ do |text|
6
+ @output.should =~ /#{text}/m
7
+ end
@@ -0,0 +1,7 @@
1
+ Given /^no Spectate is running$/ do
2
+ true
3
+ end
4
+
5
+ Then /^Spectate should be running$/ do
6
+ Spectate.ping.should be_true
7
+ end
@@ -0,0 +1,24 @@
1
+ BASEDIR = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
2
+
3
+ $LOAD_PATH.unshift(BASEDIR + '/lib')
4
+
5
+ # Make sure the binary is in our path
6
+ ENV['PATH'] = BASEDIR + '/bin:' + ENV['PATH']
7
+
8
+ require 'spectate'
9
+
10
+ require 'spec/expectations'
11
+
12
+ require File.join(BASEDIR, 'spec', 'helpers', 'config_helpers')
13
+ World(Spectate::Spec::ConfigHelpers)
14
+
15
+ # Set up a config file that makes sense for us
16
+ Before do
17
+ create_config
18
+ end
19
+
20
+ After do
21
+ remove_config
22
+ end
23
+
24
+
@@ -0,0 +1,19 @@
1
+ # Spectate lives wherever this config.ru file lives.
2
+ module Spectate
3
+ ROOT_DIR = File.expand_path(File.dirname(__FILE__))
4
+ end
5
+
6
+ # If you're developing or customizing Spectate, you can create a 'src'
7
+ # directory and it'll load Spectate from here instead of the gem. If you want to
8
+ # use your own projects directory instead, make a symlink. (If you don't know
9
+ # how to do THAT, you probably shouldn't be developing or customizing Spectate.)
10
+ if File.exists?(File.join(Spectate::ROOT_DIR, 'src'))
11
+ SOURCE_DIR = File.join(Spectate::ROOT_DIR, 'src')
12
+ require File.join(SOURCE_DIR, 'lib', 'spectate')
13
+ else
14
+ require 'rubygems'
15
+ require 'spectate'
16
+ end
17
+
18
+ # Make the magic happen
19
+ run Spectate::Server
@@ -0,0 +1,22 @@
1
+ # Jump to the front of the line
2
+ $:.unshift(File.dirname(__FILE__)) unless
3
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
4
+
5
+ require 'rubygems'
6
+ require 'spectate/config'
7
+ require 'spectate/exceptions'
8
+
9
+ module Spectate
10
+ autoload :Client, 'spectate/client'
11
+ autoload :Encode, 'spectate/encode'
12
+ autoload :Ping, 'spectate/ping'
13
+ autoload :Server, 'spectate/server'
14
+ autoload :Spectator, 'spectate/spectator'
15
+ autoload :Status, 'spectate/status'
16
+ autoload :Tree, 'spectate/tree'
17
+ # Set vital defaults
18
+ VERSION = File.read(File.join(File.dirname(__FILE__), '..', 'VERSION')).chomp
19
+ ROOT_DIR = File.expand_path(File.join('~','.spectate')) unless const_defined?(:ROOT_DIR)
20
+ # Config['basedir'] = ENV['SPECTATE_DIR'] || File.expand_path(File.join('~','.spectate'))
21
+ end
22
+
@@ -0,0 +1,35 @@
1
+ require 'spectate'
2
+ require 'restclient'
3
+ require 'json'
4
+
5
+ module Spectate
6
+ class Client
7
+ attr_accessor :protocol, :host, :port, :accept
8
+
9
+ def initialize(options = {})
10
+ Spectate::Config.load_configuration unless Spectate::Config.loaded?
11
+ @protocol = options[:protocol] || Spectate::Config['protocol'] || 'http'
12
+ @host = options[:host] || Spectate::Config['host'] || 'localhost'
13
+ @port = options[:port] || Spectate::Config['port'] || 0
14
+ @accept = options[:accept] || Spectate::Config['accept'] || 'application/json'
15
+
16
+ @driver = RestClient::Resource.new(root, :accept => accept)
17
+ end
18
+
19
+ def root
20
+ protocol.tr(':/','') +
21
+ '://' +
22
+ host +
23
+ ((port.to_i > 0) ? ":#{port}" : '')
24
+ end
25
+
26
+ def get
27
+ response = @driver.get
28
+ s = Spectate::Status.new(root,'/',JSON.parse(response)) if accept == 'application/json'
29
+ rescue RestClient::ResourceNotFound
30
+ nil
31
+ rescue Errno::ECONNREFUSED
32
+ nil
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,99 @@
1
+ require 'optparse'
2
+ require 'spectate'
3
+
4
+ module Spectate
5
+ SERVER_TYPES = %w[passenger thin mongrel webrick]
6
+
7
+ PASSENGER_DEFAULTS = {
8
+ 'rackup' => false,
9
+ 'server' => 'passenger',
10
+ 'host' => 'spectate.local'
11
+ }
12
+
13
+ RACKUP_DEFAULTS = {
14
+ 'rackup' => true,
15
+ 'host' => 'localhost',
16
+ 'port' => 20574
17
+ }
18
+
19
+ # Drives the 'spectate' command line utility.
20
+ class Command
21
+ extend Spectate::Ping
22
+
23
+ # The primary event called by the command line
24
+ def self.run
25
+ skip_server = false
26
+ only_setup = false
27
+ stop_server = false
28
+ unless ARGV.empty?
29
+ options = OptionParser.new
30
+ options.on("-d", "--directory", "=DIR", String, "Set base directory for support files (default is ~/.spectate)") do |val|
31
+ Spectate::Config['basedir'] = val
32
+ puts "Directory set to #{Spectate::Config['basedir']}"
33
+ end
34
+ options.on("--setup", "=TYPE", SERVER_TYPES, "Create the base directory and initialize config.yml with the given server type (#{SERVER_TYPES.join(', ')})") do |type|
35
+ only_setup = true
36
+ Spectate::Config['server'] = type
37
+ case type
38
+ when 'passenger':
39
+ Spectate::Config.default(PASSENGER_DEFAULTS)
40
+ else
41
+ Spectate::Config.default(RACKUP_DEFAULTS)
42
+ end
43
+ end
44
+ options.on("--host", "-h", "=NAME", String, "Set hostname or IP address for server access") do |host|
45
+ Spectate::Config['host'] = host
46
+ end
47
+ options.on("--port", "-p", "=PORT", Integer, "Set port number for server access") do |port|
48
+ Spectate::Config['port'] = port
49
+ end
50
+ options.on("--help", "-?", "--usage", "Displays this help screen") {|o| puts options.to_s; skip_server = true}
51
+ options.on("--stop", "Stops the Spectate server if running") {stop_server = true}
52
+ unparsed = options.parse(ARGV)
53
+ end
54
+
55
+ if only_setup
56
+ Spectate::Config.generate_configuration
57
+ else
58
+ Spectate::Config.load_configuration
59
+ if stop_server
60
+ self.kill_server
61
+ else
62
+ self.ensure_server unless skip_server
63
+ end
64
+ end
65
+ true
66
+ rescue OptionParser::ParseError
67
+ puts "Oops... #{$!}"
68
+ puts options
69
+ false
70
+ rescue StandardError
71
+ puts $!
72
+ puts options
73
+ false
74
+ end
75
+
76
+ private
77
+ def self.ensure_server
78
+ if ping
79
+ puts "Spectate is already running!"
80
+ else
81
+ puts "Starting Spectate on #{Spectate::Config['host']}:#{Spectate::Config['port']}..."
82
+ Dir.chdir Spectate::Config.basedir do |dir|
83
+ system "rackup -D -o #{Spectate::Config['host']} -p #{Spectate::Config['port']} -P spectate.pid config.ru"
84
+ end
85
+ end
86
+ end
87
+
88
+ def self.kill_server()
89
+ pidfile = File.join(Spectate::Config.basedir, 'spectate.pid')
90
+ pid = File.read(pidfile).to_i if File.exists?(pidfile)
91
+ if pid and `ps x #{pid}` =~ /rackup.*-p #{Spectate::Config['port']}/
92
+ Process.kill("KILL",pid) and puts "Spectate stopped."
93
+ File.delete(pidfile)
94
+ else
95
+ puts "Spectate wasn't running! (Or you're using Passenger, or your PID file is incorrect,\n or it's on a different server. In any case, you're on your own.)"
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,71 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ module Spectate
5
+ module Config
6
+ @config = Hash.new
7
+ @loaded = false
8
+
9
+ def self.to_hash
10
+ @config
11
+ end
12
+
13
+ def self.[](key)
14
+ @config[key]
15
+ end
16
+
17
+ def self.[]=(key, val)
18
+ @config[key] = val
19
+ end
20
+
21
+ def self.delete(key)
22
+ @config.delete(key)
23
+ end
24
+
25
+ def self.clear!
26
+ @config = Hash.new
27
+ @loaded = false
28
+ end
29
+
30
+ def self.default(hash)
31
+ @config = hash.merge(@config)
32
+ end
33
+
34
+ def self.basedir
35
+ @basedir
36
+ end
37
+
38
+ def self.loaded?
39
+ @loaded
40
+ end
41
+
42
+ # Loads persistent values from the config.yml file in the base directory.
43
+ # Assumes that the basedir configuration variable has already been set before calling.
44
+ def self.load_configuration(confdir = nil)
45
+ @basedir = confdir || self['basedir'] || ENV['SPECTATE_DIR'] || ROOT_DIR
46
+ raise "Could not find a base directory!\nRun spectate --setup to initialize things or tell us your base directory\nwith the -d option." unless basedir
47
+ raise "Directory #{basedir} not found!\nRun spectate --setup to initialize things." unless File.directory?(basedir)
48
+ conffile = File.join(basedir, "config.yml")
49
+ raise "File #{conffile} not found!\nRun spectate --setup to initialize things." unless File.exists?(conffile)
50
+ @loaded = @config.merge!(YAML.load_file(conffile))
51
+ end
52
+
53
+ # Confirms that the config.yml file doesn't already exist, then creates one from passed parameters
54
+ def self.generate_configuration
55
+ configfile = File.join(self['basedir'], "config.yml")
56
+ raise "You already have a config.yml file! We don't want to mess with a good thing.\n" +
57
+ "If you really want to start over, delete #{configfile} and run spectate --setup again." if File.exists?(configfile)
58
+ unless File.directory?(self['basedir'])
59
+ puts "Creating directory #{self['basedir']}"
60
+ FileUtils.makedirs self['basedir']
61
+ end
62
+ puts "Creating config.yml file"
63
+ File.open(configfile, 'w') {|config| YAML.dump(self.to_hash, config)}
64
+ puts "Creating rackup file"
65
+ FileUtils.copy File.join(File.dirname(__FILE__), '..', '..', 'generators', 'config.ru'), self['basedir']
66
+ end
67
+
68
+
69
+ end
70
+
71
+ end