strelka 0.11.0 → 0.12.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/History.rdoc +8 -0
- data/Manifest.txt +5 -0
- data/README.rdoc +25 -10
- data/Rakefile +2 -2
- data/bin/strelka +2 -411
- data/lib/strelka.rb +5 -10
- data/lib/strelka/cli.rb +393 -0
- data/lib/strelka/command/config.rb +35 -0
- data/lib/strelka/command/discover.rb +29 -0
- data/lib/strelka/command/start.rb +40 -0
- data/lib/strelka/discovery.rb +151 -97
- data/spec/strelka/cli_spec.rb +85 -0
- data/spec/strelka/discovery_spec.rb +69 -81
- data/spec/strelka_spec.rb +0 -12
- metadata +49 -45
- metadata.gz.sig +0 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'strelka/cli' unless defined?( Strelka::CLI )
|
5
|
+
|
6
|
+
|
7
|
+
# Command to show discovered Strelka apps
|
8
|
+
module Strelka::CLI::Discover
|
9
|
+
extend Strelka::CLI::Subcommand
|
10
|
+
|
11
|
+
desc 'Show installed Strelka apps'
|
12
|
+
command :discover do |cmd|
|
13
|
+
|
14
|
+
cmd.action do |globals, options, args|
|
15
|
+
prompt.say( headline_string "Searching for Strelka applications..." )
|
16
|
+
|
17
|
+
apps = Strelka::Discovery.discovered_apps
|
18
|
+
|
19
|
+
if apps.empty?
|
20
|
+
prompt.say "None found."
|
21
|
+
else
|
22
|
+
rows = apps.sort_by {|name, path| name }
|
23
|
+
display_table( rows )
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end # module Strelka::CLI::Discover
|
29
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'strelka/cli' unless defined?( Strelka::CLI )
|
5
|
+
|
6
|
+
|
7
|
+
# Command to start a Strelka application
|
8
|
+
module Strelka::CLI::Start
|
9
|
+
extend Strelka::CLI::Subcommand
|
10
|
+
|
11
|
+
desc 'Start a Strelka app'
|
12
|
+
arg :GEMNAME, :optional
|
13
|
+
arg :APPNAME
|
14
|
+
command :start do |cmd|
|
15
|
+
|
16
|
+
cmd.action do |globals, options, args|
|
17
|
+
appname = args.pop
|
18
|
+
gemname = args.pop
|
19
|
+
path = nil
|
20
|
+
|
21
|
+
gem( gemname ) if gemname
|
22
|
+
|
23
|
+
app = if File.exist?( appname )
|
24
|
+
Strelka::Discovery.load_file( appname ) or
|
25
|
+
exit_now!( "Didn't find an app while loading %p!" % [appname] )
|
26
|
+
else
|
27
|
+
Strelka::Discovery.load( appname ) or
|
28
|
+
exit_now!( "Couldn't find the %p app!" % [appname] )
|
29
|
+
end
|
30
|
+
|
31
|
+
Strelka::CLI.prompt.say "Starting %s (%p)." % [ appname, app ]
|
32
|
+
Strelka.load_config( options[:c] ) if options[:c]
|
33
|
+
unless_dryrun( "starting the app" ) do
|
34
|
+
app.run
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end # module Strelka::CLI::Start
|
40
|
+
|
data/lib/strelka/discovery.rb
CHANGED
@@ -7,6 +7,83 @@ require 'strelka' unless defined?( Strelka )
|
|
7
7
|
|
8
8
|
|
9
9
|
# The Strelka application-discovery system.
|
10
|
+
#
|
11
|
+
# This module provides a mechanism for registering Strelka apps and their
|
12
|
+
# resources for discovery by the strelka CLI and other systems.
|
13
|
+
#
|
14
|
+
# It's responsible for three kinds of discovery:
|
15
|
+
#
|
16
|
+
# - Discovery of Strelka app files via Rubygems discovery
|
17
|
+
# - Discovery and loading of Strelka app classes by name
|
18
|
+
# - Discovery of data directories for strelka apps
|
19
|
+
#
|
20
|
+
# As such it can be used in several different ways.
|
21
|
+
#
|
22
|
+
# == \App File \Discovery
|
23
|
+
#
|
24
|
+
# If you have an app that you wish to be discoverable, create a
|
25
|
+
# <tt>lib/strelka/apps.rb</tt> file. This file will be added to those returned
|
26
|
+
# by the ::app_discovery_files call, which is the list loaded by
|
27
|
+
# ::discovered_apps.
|
28
|
+
#
|
29
|
+
# == \App \Discovery Registration
|
30
|
+
#
|
31
|
+
# To add a name and file path to Strelka::Discovery.discovered_apps, you can
|
32
|
+
# call ::register_app. This will check to make sure no other apps are registered
|
33
|
+
# with the same name. To register several at the same time, call
|
34
|
+
# ::register_apps with a Hash of <tt>name => path</tt> pairs.
|
35
|
+
#
|
36
|
+
# == Loading Discovered Apps
|
37
|
+
#
|
38
|
+
# To load a discovered app, call ::load with its registered name.
|
39
|
+
#
|
40
|
+
# This will load the associated file and returns the first Ruby class to inherit
|
41
|
+
# from a discoverable app class like Strelka::App or Strelka::WebSocketServer.
|
42
|
+
#
|
43
|
+
# === Putting it all together
|
44
|
+
#
|
45
|
+
# Say, for example, you were putting together an <tt>acme-apps</tt> gem for the
|
46
|
+
# Acme company that contained Strelka apps for a web store and a CMS. You could
|
47
|
+
# add a <tt>lib/strelka/apps.rb</tt> file to the <tt>acme-apps</tt> gem that
|
48
|
+
# contained the following:
|
49
|
+
#
|
50
|
+
# # -*- ruby -*-
|
51
|
+
# require 'strelka/discovery'
|
52
|
+
#
|
53
|
+
# Strelka::Discovery.register_apps(
|
54
|
+
# 'acme-store' => 'lib/acme/store.rb',
|
55
|
+
# 'acme-cms' => 'lib/acme/cms.rb'
|
56
|
+
# )
|
57
|
+
#
|
58
|
+
# This would let you do:
|
59
|
+
#
|
60
|
+
# $ gem install acme-apps
|
61
|
+
# $ strelka start acme-store
|
62
|
+
#
|
63
|
+
#
|
64
|
+
# == Data Directory \Discovery
|
65
|
+
#
|
66
|
+
# If your app requires some filesystem resources, a good way to distribute these
|
67
|
+
# is in your gem's "data directory". This is a directory in your gem called
|
68
|
+
# <tt>data/«your gem name»</tt>, and can be found via:
|
69
|
+
#
|
70
|
+
# Gem.datadir( your_gem_name )
|
71
|
+
#
|
72
|
+
# Strelka::Discoverable builds on top of this, and can return a Hash of glob
|
73
|
+
# patterns that will match the data directories of all gems that depend on
|
74
|
+
# Strelka, keyed by gem name. You can use this to populate search paths for
|
75
|
+
# templates, static assets, etc.
|
76
|
+
#
|
77
|
+
# template_paths = Strelka::Discovery.discover_data_dirs.
|
78
|
+
# flat_map {|_, pattern| Dir.glob(pattern + '/templates') }
|
79
|
+
#
|
80
|
+
# == Making a class Discoverable
|
81
|
+
#
|
82
|
+
# If you write your own app base class (e.g., Strelka::App,
|
83
|
+
# Strelka::WebSocketServer), you can make it discoverable by extending it with
|
84
|
+
# this module. You typically won't have to do this unless you're working on
|
85
|
+
# Strelka itself.
|
86
|
+
#
|
10
87
|
module Strelka::Discovery
|
11
88
|
extend Loggability,
|
12
89
|
Configurability,
|
@@ -22,7 +99,7 @@ module Strelka::Discovery
|
|
22
99
|
|
23
100
|
# Default config
|
24
101
|
CONFIG_DEFAULTS = {
|
25
|
-
|
102
|
+
app_discovery_file: 'strelka/apps.rb',
|
26
103
|
local_data_dirs: 'data/*',
|
27
104
|
}.freeze
|
28
105
|
|
@@ -30,11 +107,11 @@ module Strelka::Discovery
|
|
30
107
|
##
|
31
108
|
# The Hash of Strelka::App subclasses, keyed by the Pathname of the file they were
|
32
109
|
# loaded from, or +nil+ if they weren't loaded via ::load.
|
33
|
-
singleton_attr_reader :
|
110
|
+
singleton_attr_reader :discovered_classes
|
34
111
|
|
35
112
|
##
|
36
|
-
# The glob(3) pattern for matching
|
37
|
-
singleton_attr_accessor :
|
113
|
+
# The glob(3) pattern for matching the discovery hook file.
|
114
|
+
singleton_attr_accessor :app_discovery_file
|
38
115
|
|
39
116
|
##
|
40
117
|
# The glob(3) pattern for matching local data directories during discovery. Local
|
@@ -46,11 +123,55 @@ module Strelka::Discovery
|
|
46
123
|
singleton_attr_reader :loading_file
|
47
124
|
|
48
125
|
|
49
|
-
#
|
50
|
-
@
|
51
|
-
@
|
52
|
-
@app_glob_pattern = CONFIG_DEFAULTS[:app_glob_pattern]
|
126
|
+
# Class instance variables
|
127
|
+
@discovered_classes = Hash.new {|h,k| h[k] = [] }
|
128
|
+
@app_discovery_file = CONFIG_DEFAULTS[:app_discovery_file]
|
53
129
|
@local_data_dirs = CONFIG_DEFAULTS[:local_data_dirs]
|
130
|
+
@discovered_apps = nil
|
131
|
+
|
132
|
+
|
133
|
+
### Register an app with the specified +name+ that can be loaded from the given
|
134
|
+
### +path+.
|
135
|
+
def self::register_app( name, path )
|
136
|
+
@discovered_apps ||= {}
|
137
|
+
|
138
|
+
if @discovered_apps.key?( name )
|
139
|
+
raise "Can't register a second '%s' app at %s; already have one at %s" %
|
140
|
+
[ name, path, @discovered_apps[name] ]
|
141
|
+
end
|
142
|
+
|
143
|
+
self.log.debug "Registered app at %s as %p" % [ path, name ]
|
144
|
+
@discovered_apps[ name ] = path
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
### Register multiple apps by passing +a_hash+ of names and paths.
|
149
|
+
def self::register_apps( a_hash )
|
150
|
+
a_hash.each do |name, path|
|
151
|
+
self.register_app( name, path )
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
### Return a Hash of apps discovered by loading #app_discovery_files.
|
157
|
+
def self::discovered_apps
|
158
|
+
unless @discovered_apps
|
159
|
+
@discovered_apps ||= {}
|
160
|
+
self.app_discovery_files.each do |path|
|
161
|
+
self.log.debug "Loading discovery file %p" % [ path ]
|
162
|
+
Kernel.load( path )
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
return @discovered_apps
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
### Return an Array of app discovery hook files found in the latest installed gems and
|
171
|
+
### the current $LOAD_PATH.
|
172
|
+
def self::app_discovery_files
|
173
|
+
return Gem.find_latest_files( self.app_discovery_file )
|
174
|
+
end
|
54
175
|
|
55
176
|
|
56
177
|
### Configure the App. Override this if you wish to add additional configuration
|
@@ -59,7 +180,7 @@ module Strelka::Discovery
|
|
59
180
|
def self::configure( config=nil )
|
60
181
|
config = self.defaults.merge( config || {} )
|
61
182
|
|
62
|
-
self.
|
183
|
+
self.app_discovery_file = config[:app_discovery_file]
|
63
184
|
self.local_data_dirs = config[:local_data_dirs]
|
64
185
|
end
|
65
186
|
|
@@ -92,103 +213,36 @@ module Strelka::Discovery
|
|
92
213
|
end
|
93
214
|
|
94
215
|
|
95
|
-
###
|
96
|
-
###
|
97
|
-
def self::
|
98
|
-
|
216
|
+
### Attempt to load the file associated with the specified +app_name+ and return
|
217
|
+
### the first Strelka::App class declared in the process.
|
218
|
+
def self::load( app_name )
|
219
|
+
apps = self.discovered_apps or return nil
|
220
|
+
file = apps[ app_name ] or return nil
|
99
221
|
|
100
|
-
self.
|
101
|
-
pattern = File.join( dir, self.app_glob_pattern )
|
102
|
-
appfiles[ gemname ] = Pathname.glob( pattern )
|
103
|
-
end
|
104
|
-
|
105
|
-
return appfiles
|
222
|
+
return self.load_file( file )
|
106
223
|
end
|
107
224
|
|
108
225
|
|
109
|
-
###
|
110
|
-
def self::
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
self.log.debug "Loading apps from %d discovered paths" % [ app_paths.length ]
|
115
|
-
app_paths.each do |gemname, paths|
|
116
|
-
self.log.debug " loading gem %s" % [ gemname ]
|
117
|
-
gem( gemname ) unless gemname == ''
|
118
|
-
|
119
|
-
self.log.debug " loading apps from %s: %d handlers" % [ gemname, paths.length ]
|
120
|
-
paths.each do |path|
|
121
|
-
classes = begin
|
122
|
-
self.load( path )
|
123
|
-
rescue StandardError, ScriptError => err
|
124
|
-
self.log.error "%p while loading Strelka apps from %s: %s" %
|
125
|
-
[ err.class, path, err.message ]
|
126
|
-
self.log.debug "Backtrace: %s" % [ err.backtrace.join("\n\t") ]
|
127
|
-
[]
|
128
|
-
end
|
129
|
-
self.log.debug " loaded app classes: %p" % [ classes ]
|
130
|
-
|
131
|
-
discovered_apps += classes
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
return discovered_apps
|
136
|
-
end
|
137
|
-
|
226
|
+
### Load the specified +file+ and return the first class that extends Strelka::Discovery.
|
227
|
+
def self::load_file( file )
|
228
|
+
self.log.debug "Loading application/s from %p" % [ file ]
|
229
|
+
Thread.current[ :__loading_file ] = loading_file = file
|
230
|
+
self.discovered_classes.delete( loading_file )
|
138
231
|
|
139
|
-
|
140
|
-
### the gem it's from. If the optional +gemname+ is given, only consider apps from that gem.
|
141
|
-
### Raises a RuntimeError if no app with the given +appname+ was found.
|
142
|
-
def self::find( appname, gemname=nil )
|
143
|
-
discovered_apps = self.discover_paths
|
144
|
-
|
145
|
-
path = nil
|
146
|
-
if gemname
|
147
|
-
discovered_apps[ gemname ].each do |apppath|
|
148
|
-
self.log.debug " %s (%s)" % [ apppath, apppath.basename('.rb') ]
|
149
|
-
if apppath.basename('.rb').to_s == appname
|
150
|
-
path = apppath
|
151
|
-
break
|
152
|
-
end
|
153
|
-
end
|
154
|
-
else
|
155
|
-
self.log.debug "No gem name; searching them all:"
|
156
|
-
discovered_apps.each do |disc_gemname, paths|
|
157
|
-
self.log.debug " %s: %d paths" % [ disc_gemname, paths.length ]
|
158
|
-
path = paths.find do |apppath|
|
159
|
-
self.log.debug " %s (%s)" % [ apppath, apppath.basename('.rb') ]
|
160
|
-
self.log.debug " %p vs. %p" % [ apppath.basename('.rb').to_s, appname ]
|
161
|
-
apppath.basename('.rb').to_s == appname
|
162
|
-
end or next
|
163
|
-
gemname = disc_gemname
|
164
|
-
break
|
165
|
-
end
|
166
|
-
end
|
232
|
+
Kernel.load( loading_file.to_s )
|
167
233
|
|
168
|
-
|
169
|
-
|
170
|
-
msg << " in the #{gemname} gem" if gemname
|
171
|
-
raise( msg )
|
172
|
-
end
|
173
|
-
self.log.debug " found: %s" % [ path ]
|
234
|
+
new_subclasses = self.discovered_classes[ loading_file ]
|
235
|
+
self.log.debug " loaded %d new app class/es" % [ new_subclasses.size ]
|
174
236
|
|
175
|
-
return
|
237
|
+
return new_subclasses.first
|
238
|
+
ensure
|
239
|
+
Thread.current[ :__loading_file ] = nil
|
176
240
|
end
|
177
241
|
|
178
242
|
|
179
|
-
###
|
180
|
-
|
181
|
-
|
182
|
-
self.log.debug "Loading application/s from %p" % [ file ]
|
183
|
-
@loading_file = Pathname( file ).expand_path
|
184
|
-
self.subclasses.delete( @loading_file )
|
185
|
-
Kernel.load( @loading_file.to_s )
|
186
|
-
new_subclasses = self.subclasses[ @loading_file ]
|
187
|
-
self.log.debug " loaded %d new app class/es" % [ new_subclasses.size ]
|
188
|
-
|
189
|
-
return new_subclasses
|
190
|
-
ensure
|
191
|
-
@loading_file = nil
|
243
|
+
### Return the Pathname of the file being loaded by the current thread (if there is one)
|
244
|
+
def self::loading_file
|
245
|
+
return Thread.current[ :__loading_file ]
|
192
246
|
end
|
193
247
|
|
194
248
|
|
@@ -196,7 +250,7 @@ module Strelka::Discovery
|
|
196
250
|
### with Discovery.
|
197
251
|
def self::add_inherited_class( subclass )
|
198
252
|
self.log.debug "Registering discovered subclass %p" % [ subclass ]
|
199
|
-
self.
|
253
|
+
self.discovered_classes[ self.loading_file ] << subclass
|
200
254
|
end
|
201
255
|
|
202
256
|
|
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/usr/bin/env rspec -cfd
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require_relative '../helpers'
|
5
|
+
|
6
|
+
require 'tempfile'
|
7
|
+
require 'rspec'
|
8
|
+
|
9
|
+
require 'strelka/cli'
|
10
|
+
|
11
|
+
describe Strelka::CLI do
|
12
|
+
|
13
|
+
before( :all ) do
|
14
|
+
testcommands = Module.new
|
15
|
+
testcommands.extend( Strelka::CLI::Subcommand )
|
16
|
+
testcommands.module_eval do
|
17
|
+
command :test_output do |cmd|
|
18
|
+
cmd.action do
|
19
|
+
prompt.say "Test command!"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
command :test_dryrun do |cmd|
|
24
|
+
cmd.action do
|
25
|
+
unless_dryrun( "Running the test." ) do
|
26
|
+
$stdout.puts "Ran it!"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
after( :each ) do
|
34
|
+
described_class.reset_prompt
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "output redirection" do
|
38
|
+
|
39
|
+
it "uses STDERR for user interaction" do
|
40
|
+
expect {
|
41
|
+
described_class.run([ 'test_output' ])
|
42
|
+
}.to output( /Test command!\n/ ).to_stderr
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
it "redirects its output to STDOUT when run with `-o -`" do
|
47
|
+
expect {
|
48
|
+
described_class.run([ '-o', '-', 'test_output' ])
|
49
|
+
}.to output( /Test command!\n/ ).to_stdout
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
it "redirects its output to the named file when run with `-o filename`" do
|
54
|
+
tmpfile = Dir::Tmpname.create( 'strelka-command-fileout' ) { }
|
55
|
+
|
56
|
+
begin
|
57
|
+
described_class.run([ '-o', tmpfile, 'test_output' ])
|
58
|
+
expect( IO.read(tmpfile) ).to match( /Test command!\n/ )
|
59
|
+
ensure
|
60
|
+
File.unlink( tmpfile ) if tmpfile && File.exist?( tmpfile )
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
describe "dry-run mode" do
|
68
|
+
|
69
|
+
it "executes the protected block if dry-run mode isn't enabled" do
|
70
|
+
expect {
|
71
|
+
described_class.run([ 'test_dryrun' ])
|
72
|
+
}.to output( /Ran it!/ ).to_stdout
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
it "doesn't execute the block if dry-run mode *is* enabled" do
|
77
|
+
expect {
|
78
|
+
described_class.run([ '-n', 'test_dryrun' ])
|
79
|
+
}.to_not output( /Ran it!/ ).to_stdout
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|