tap-server 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/History ADDED
@@ -0,0 +1,5 @@
1
+ == 0.1.0 / 2009-02-17
2
+
3
+ As of the 0.12.0 release, Tap is distributed as several independent modules.
4
+ This is the server module. In the initial release, the server is working
5
+ as a proof-of-principle, and not as a serious interface for tap.
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009, Regents of the University of Colorado.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
+ software and associated documentation files (the "Software"), to deal in the Software
5
+ without restriction, including without limitation the rights to use, copy, modify, merge,
6
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7
+ to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or
10
+ substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
16
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
19
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,49 @@
1
+ = {Tap Server}[http://tap.rubyforge.org/tap-server]
2
+
3
+ A web interface for {Tap}[http://tap.rubyforge.org/rdoc].
4
+
5
+ == Description
6
+
7
+ {Tap Server}[http://tap.rubyforge.org/tap-server] provides a web interface for
8
+ Tap tasks. The basic interface allows the construction and execution of
9
+ workflows, rendering of task results, and facilitates distributable
10
+ controllers. The intention is not to make websites for the masses, but rather
11
+ a local interface for individuals and small groups.
12
+
13
+ {Tap Server}[http://tap.rubyforge.org/tap-server] is a part of the
14
+ {Tap-Suite}[http://tap.rubyforge.org/tap-suite]. Check out these links for
15
+ documentation, development, and bug tracking.
16
+
17
+ * Website[http://tap.rubyforge.org]
18
+ * Lighthouse[http://bahuvrihi.lighthouseapp.com/projects/9908-tap-task-application/tickets]
19
+ * Github[http://github.com/bahuvrihi/tap/tree/master]
20
+ * {Google Group}[http://groups.google.com/group/ruby-on-tap]
21
+
22
+ == Usage
23
+
24
+ To get a peek, use the command:
25
+
26
+ % tap server
27
+
28
+ Then go to the url 'localhost:8080'. Currently tap-server is in alpha and
29
+ should not be considered stable.
30
+
31
+ == Installation
32
+
33
+ Tap Server is available as a gem on RubyForge[http://rubyforge.org/projects/tap]. Use:
34
+
35
+ % gem install tap-server
36
+
37
+ Tap requires an updated version of RubyGems[http://docs.rubygems.org/]
38
+ (>= 1.2.0). To check the version and update RubyGems:
39
+
40
+ % gem --version
41
+ % gem --update system
42
+
43
+ == Info
44
+
45
+ Copyright (c) 2009, Regents of the University of Colorado.
46
+ Developer:: {Simon Chiang}[http://bahuvrihi.wordpress.com], {Biomolecular Structure Program}[http://biomol.uchsc.edu/], {Hansen Lab}[http://hsc-proteomics.uchsc.edu/hansenlab/]
47
+ Support:: CU Denver School of Medicine Deans Academic Enrichment Fund
48
+ Licence:: {MIT-Style}[link:files/MIT-LICENSE.html]
49
+
data/cmd/server.rb ADDED
@@ -0,0 +1,34 @@
1
+ # tap server {options}
2
+ #
3
+ # Initializes a tap server.
4
+
5
+ require 'tap'
6
+ require 'tap/server'
7
+
8
+ env = Tap::Env.instance
9
+ app = Tap::App.instance
10
+
11
+ #
12
+ # handle options
13
+ #
14
+
15
+ config_path = nil
16
+ opts = ConfigParser.new do |opts|
17
+
18
+ opts.separator ""
19
+ opts.separator "options:"
20
+ opts.add(Tap::Server.configurations)
21
+
22
+ # add option to print help
23
+ opts.on("-h", "--help", "Show this message") do
24
+ puts Lazydoc.usage(__FILE__)
25
+ puts opts
26
+ exit
27
+ end
28
+ end
29
+
30
+ # parse!
31
+ argv = opts.parse(ARGV)
32
+ server = Tap::Server.new(env, app, opts.config)
33
+ cookie_server = Rack::Session::Pool.new(server)
34
+ Rack::Handler::WEBrick.run(cookie_server, :Port => server.port)
@@ -0,0 +1,92 @@
1
+ require 'tap/controller'
2
+ require 'rack/mime'
3
+ require 'time'
4
+
5
+ class AppController < Tap::Controller
6
+ set :default_layout, 'layout.erb'
7
+
8
+ def call(env)
9
+ # serve public files before actions
10
+ server = env['tap.server'] ||= Tap::Server.new
11
+
12
+ if path = server.public_path(env['PATH_INFO'])
13
+ content = File.read(path)
14
+ headers = {
15
+ "Last-Modified" => [File.mtime(path).httpdate],
16
+ "Content-Type" => [Rack::Mime.mime_type(File.extname(path), 'text/plain')],
17
+ "Content-Length" => [content.size.to_s]
18
+ }
19
+
20
+ [200, headers, [content]]
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def index
27
+ env_names = {}
28
+ server.env.minimap.each do |name, environment|
29
+ env_names[environment] = name
30
+ end
31
+
32
+ render('index.erb', :locals => {:env => server.env, :env_names => env_names}, :layout => true)
33
+ end
34
+
35
+ def info
36
+ if request.post?
37
+ app.info
38
+ else
39
+ render('info.erb', :locals => {:update => true, :content => app.info}, :layout => true)
40
+ end
41
+ end
42
+
43
+ #--
44
+ # Currently tail is hard-coded to tail the server log only.
45
+ def tail(id=nil)
46
+ begin
47
+ path = app.subpath(:log, 'server.log')
48
+ raise unless File.exists?(path)
49
+ rescue
50
+ raise Tap::ServerError.new("invalid path", 404)
51
+ end
52
+
53
+ pos = request['pos'].to_i
54
+ if pos > File.size(path)
55
+ raise Tap::ServerError.new("tail position out of range (try update)", 500)
56
+ end
57
+
58
+ content = File.open(path) do |file|
59
+ file.pos = pos
60
+ file.read
61
+ end
62
+
63
+ if request.post?
64
+ content
65
+ else
66
+ render('tail.erb', :locals => {
67
+ :id => id,
68
+ :path => File.basename(path),
69
+ :update => true,
70
+ :content => content
71
+ }, :layout => true)
72
+ end
73
+ end
74
+
75
+ def run
76
+ Thread.new { app.run }
77
+ redirect("/app/tail")
78
+ end
79
+
80
+ def stop
81
+ app.stop
82
+ redirect("/app/info")
83
+ end
84
+
85
+ def terminate
86
+ app.terminate
87
+ redirect("/app/info")
88
+ end
89
+
90
+ def help(key=nil)
91
+ end
92
+ end
@@ -0,0 +1,255 @@
1
+ require 'tap/controller'
2
+
3
+ class SchemaController < Tap::Controller
4
+ set :default_layout, 'layout.erb'
5
+
6
+ # Initializes a new schema and redirects to display.
7
+ def index
8
+ id = initialize_schema
9
+ redirect("/schema/display/#{id}")
10
+ end
11
+
12
+ # Loads the schema indicated by id and renders 'schema.erb' with the default
13
+ # layout.
14
+ def display(id)
15
+ schema = load_schema(id)
16
+ render 'schema.erb', :locals => {
17
+ :id => id,
18
+ :schema => schema
19
+ }, :layout => true
20
+ end
21
+
22
+ # Updates the specified schema with the request parameters. Update forwards
23
+ # the request to the action ('add' or 'remove') specified in the action
24
+ # parameter.
25
+ def update(id)
26
+ case request['action']
27
+ when 'add' then add(id)
28
+ when 'remove' then remove(id)
29
+ when 'echo' then echo
30
+ else raise Tap::ServerError, "unknown action: #{request['action']}"
31
+ end
32
+ end
33
+
34
+ # Adds nodes or joins to the schema. Parameters:
35
+ #
36
+ # nodes[]:: An array of nodes to add to the schema. Each entry is split using
37
+ # Shellwords to yield an argv; the argv initializes the node. The
38
+ # index of each new node is added to targets[].
39
+ # sources[]:: An array of source node indicies used to create a join.
40
+ # targets[]:: An array of target node indicies used to create a join (note
41
+ # the indicies of new nodes are added to targets).
42
+ #
43
+ # Add creates and pushes new nodes onto schema as specified in nodes, then
44
+ # creates joins between the sources and targets. The join class is inferred
45
+ # by Utils.infer_join; if no join can be inferred the join class is
46
+ # effectively nil, and consistent with that, the node output for sources
47
+ # and the node input for targets is set to nil.
48
+ #
49
+ # === Notes
50
+ #
51
+ # The nomenclature for source and target is relative to the join, and may
52
+ # seem backwards for the node (ex: 'sources[]=0&targets[]=1' makes a join
53
+ # like '0:1')
54
+ #
55
+ def add(id)
56
+ unless request.post?
57
+ raise Tap::ServerError, "add must be performed with post"
58
+ end
59
+
60
+ round = (request['round'] || 0).to_i
61
+ outputs = (request['outputs[]'] || []).collect {|index| index.to_i }
62
+ inputs = (request['inputs[]'] || []).collect {|index| index.to_i }
63
+ nodes = request['nodes[]'] || []
64
+
65
+ load_schema(id) do |schema|
66
+ nodes.each do |arg|
67
+ next unless arg && !arg.empty?
68
+
69
+ outputs << schema.nodes.length
70
+ schema.nodes << Tap::Support::Node.new(Shellwords.shellwords(arg), round)
71
+ end
72
+
73
+ if inputs.empty? || outputs.empty?
74
+ inputs.each {|index| schema[index].output = nil }
75
+ outputs.each {|index| schema[index].input = round }
76
+ else
77
+
78
+ # temporary
79
+ if inputs.length > 1 && outputs.length > 1
80
+ raise "multi-way join specified"
81
+ end
82
+
83
+ schema.set(Tap::Support::Join, inputs, outputs)
84
+ end
85
+ end
86
+
87
+ redirect("/schema/display/#{id}")
88
+ end
89
+
90
+ # Removes nodes or joins from the schema. Parameters:
91
+ #
92
+ # sources[]:: An array of source node indicies to remove.
93
+ # targets[]:: An array of target node indicies to remove.
94
+ #
95
+ # Normally remove sets the node.output for each source to nil and the
96
+ # node.input for each target to nil. However, if a node is indicated in
97
+ # both sources and targets AND it has no join input/output, then it will
98
+ # be removed.
99
+ #
100
+ # === Notes
101
+ #
102
+ # The nomenclature for source and target is relative to the join, and may
103
+ # seem backwards for the node (ex: for the sequence '0:1:2', 'targets[]=1'
104
+ # breaks the join '0:1' while 'sources[]=1' breaks the join '1:2'.
105
+ #
106
+ def remove(id)
107
+ unless request.post?
108
+ raise Tap::ServerError, "remove must be performed with post"
109
+ end
110
+
111
+ round = (request['round'] || 0).to_i
112
+ outputs = (request['outputs[]'] || []).collect {|index| index.to_i }
113
+ inputs = (request['inputs[]'] || []).collect {|index| index.to_i }
114
+
115
+ load_schema(id) do |schema|
116
+ # Remove joins. Removed indicies are popped to ensure
117
+ # that if a join was removed the node will not be.
118
+ outputs.delete_if do |index|
119
+ next unless node = schema.nodes[index]
120
+ if node.input_join
121
+ node.input = round
122
+ true
123
+ else
124
+ false
125
+ end
126
+ end
127
+
128
+ inputs.delete_if do |index|
129
+ next unless node = schema.nodes[index]
130
+ if node.output_join
131
+ node.output = nil
132
+ true
133
+ else
134
+ false
135
+ end
136
+ end
137
+
138
+ # Remove nodes. Setting a node to nil causes it's removal during
139
+ # compact; orphaned joins are removed during compact as well.
140
+ (inputs & outputs).each do |index|
141
+ schema.nodes[index] = nil
142
+ end
143
+ end
144
+
145
+ redirect("/schema/display/#{id}")
146
+ end
147
+
148
+ def submit(id)
149
+ case request['action']
150
+ when 'save' then save(id)
151
+ when 'preview' then preview(id)
152
+ when 'echo' then echo
153
+ when 'run'
154
+ dump_schema(id, schema)
155
+ run(id)
156
+ else raise Tap::ServerError, "unknown action: #{request['action']}"
157
+ end
158
+ end
159
+
160
+ def save(id)
161
+ unless request.post?
162
+ raise Tap::ServerError, "submit must be performed with post"
163
+ end
164
+
165
+ dump_schema(id, schema)
166
+ redirect("/schema/display/#{id}")
167
+ end
168
+
169
+ def preview(id)
170
+ response.headers['Content-Type'] = 'text/plain'
171
+ render('preview.erb', :locals => {:id => id, :schema => schema})
172
+ end
173
+
174
+ def run(id)
175
+ unless request.post?
176
+ raise Tap::ServerError, "run must be performed with post"
177
+ end
178
+
179
+ # it would be nice to someday put all this on a separate thread...
180
+ schema = load_schema(id)
181
+ tasks = server.env.tasks
182
+ schema.build(app) do |(key, *args)|
183
+ if const = tasks.search(key)
184
+ const.constantize.parse(args, app) do |help|
185
+ raise "help not implemented"
186
+ #redirect("/app/help/#{key}")
187
+ end
188
+ else
189
+ raise ArgumentError, "unknown task: #{key}"
190
+ end
191
+ end
192
+
193
+ Thread.new { app.run }
194
+ redirect("/app/tail")
195
+ end
196
+
197
+ protected
198
+
199
+ # Parses a Tap::Support::Schema from the request.
200
+ def schema
201
+ argv = request['argv[]'] || []
202
+ argv.delete_if {|arg| arg.empty? }
203
+ Tap::Support::Schema.parse(argv)
204
+ end
205
+
206
+ def initialize_schema
207
+ current = app.glob(:schema, "*").collect {|path| File.basename(path).chomp(".yml") }
208
+
209
+ id = random_key(current.length)
210
+ while current.include?(id)
211
+ id = random_key(current.length)
212
+ end
213
+
214
+ dump_schema(id, schema)
215
+ id
216
+ end
217
+
218
+ def load_schema(id)
219
+ unless path = app.filepath(:schema, "#{id}.yml")
220
+ raise ServerError, "no schema for id: #{id}"
221
+ end
222
+ schema = Tap::Support::Schema.load_file(path)
223
+
224
+ if block_given?
225
+ result = yield(schema)
226
+ dump_schema(id, schema)
227
+ result
228
+ else
229
+ schema
230
+ end
231
+ end
232
+
233
+ def dump_schema(id, schema=nil)
234
+ app.prepare(:schema, "#{id}.yml") do |file|
235
+ file << schema.dump.to_yaml if schema
236
+ end
237
+ end
238
+
239
+ def instantiate(*argv)
240
+ key = argv.shift
241
+ tasc = server.env.tasks.search(key).constantize
242
+ tasc.parse(argv)
243
+ end
244
+
245
+ # helper to echo requests back... good for debugging
246
+ def echo # :nodoc:
247
+ "<pre>#{request.params.to_yaml}</pre>"
248
+ end
249
+
250
+ # Generates a random integer key.
251
+ def random_key(length) # :nodoc:
252
+ length = 1 if length < 1
253
+ rand(length * 10000).to_s
254
+ end
255
+ end
@@ -0,0 +1,231 @@
1
+ require 'tap/server'
2
+ autoload(:ERB, 'erb')
3
+
4
+ module Tap
5
+
6
+ # === Declaring Actions
7
+ # By default all public methods in subclasses are declared as actions. You
8
+ # can declare a private or protected method as an action by:
9
+ #
10
+ # * manually adding it directly to actions
11
+ # * defining it as a public method and then call private(:method) or protected(:method)
12
+ #
13
+ # Similarly, public method can be made non-action by actions by:
14
+ #
15
+ # * manually deleting it from actions
16
+ # * define it private or protected then call public(:method)
17
+ #
18
+ class Controller
19
+ class << self
20
+
21
+ # Initialize instance variables on the child and inherit as necessary.
22
+ def inherited(child) # :nodoc:
23
+ super
24
+ child.set(:actions, actions.dup)
25
+ child.set(:middleware, middleware.dup)
26
+ child.set(:default_layout, default_layout)
27
+ child.set(:define_action, true)
28
+ end
29
+
30
+ # An array of methods that can be called as actions. Actions must be
31
+ # stored as symbols. Actions are inherited.
32
+ attr_reader :actions
33
+
34
+ # An array of Rack middleware that will be applied when handing requests
35
+ # through the class call method. Middleware is inherited.
36
+ attr_reader :middleware
37
+
38
+ # The default layout rendered when the render option :layout is true.
39
+ attr_reader :default_layout
40
+
41
+ # The base path prepended to render paths (ie render(<path>) renders
42
+ # <templates_dir/name/path>).
43
+ def name
44
+ @name ||= to_s.underscore
45
+ end
46
+
47
+ # Adds the specified middleware. Middleware classes are initialized
48
+ # with the specified args and block, and applied to in the order in
49
+ # which they are declared (ie first use processes requests first).
50
+ #
51
+ # Middleware is applied through the class call method, and on a per-call
52
+ # basis... middleware like Rack::Session::Pool that is supposed to
53
+ # persist for the life of an application will not work properly.
54
+ #
55
+ # Middleware is inherited.
56
+ def use(middleware, *args, &block)
57
+ @middleware << [middleware, args, block]
58
+ end
59
+
60
+ # Instantiates self and performs call. Middleware is applied in the
61
+ # order in which it was declared.
62
+ #--
63
+ # Note that middleware needs to be initialized in reverese, so that
64
+ # the first declared middleware runs first.
65
+ def call(env)
66
+ app = new
67
+ middleware.reverse_each do |(m, args, block)|
68
+ app = m.new(app, *args, &block)
69
+ end
70
+ app.call(env)
71
+ end
72
+
73
+ # Sets an instance variable for self, short for:
74
+ #
75
+ # instance_variable_set(:@attribute, input)
76
+ #
77
+ # Typically only these variables should be set:
78
+ #
79
+ # actions:: sets actions
80
+ # name:: the name of the controller
81
+ # default_layout:: the default layout (used by render)
82
+ #
83
+ def set(variable, input)
84
+ instance_variable_set("@#{variable}", input)
85
+ end
86
+
87
+ protected
88
+
89
+ # Overridden so that if declare_action is set, new methods
90
+ # are added to actions.
91
+ def method_added(sym) # :nodoc:
92
+ actions << sym if @define_action
93
+ super
94
+ end
95
+
96
+ # Turns on declare_action when changing method context.
97
+ def public(*symbols) # :nodoc:
98
+ @define_action = true if symbols.empty?
99
+ super
100
+ end
101
+
102
+ # Turns off declare_action when changing method context.
103
+ def protected(*symbols) # :nodoc:
104
+ @define_action = false if symbols.empty?
105
+ super
106
+ end
107
+
108
+ # Turns off declare_action when changing method context.
109
+ def private(*symbols) # :nodoc:
110
+ @define_action = false if symbols.empty?
111
+ super
112
+ end
113
+ end
114
+
115
+ set :actions, []
116
+ set :middleware, []
117
+ set :default_layout, nil
118
+ set :define_action, false
119
+
120
+ include Rack::Utils
121
+
122
+ # Accesses the 'tap.server' specified in env, set during call.
123
+ attr_accessor :server
124
+
125
+ # A Rack::Request wrapping env, set during call.
126
+ attr_accessor :request
127
+
128
+ # A Rack::Response. If the action returns a string, it will be written to
129
+ # response and response will be returned by call. Otherwise, call returns
130
+ # the action result and response is ignored.
131
+ attr_accessor :response
132
+
133
+ # Initializes a new instance of self. The input attributes are reset by
134
+ # call and are only provided for convenience during testing.
135
+ def initialize(server=nil, request=nil, response=nil)
136
+ @server = server
137
+ @request = request
138
+ @response = response
139
+ end
140
+
141
+ def call(env)
142
+ @server = env['tap.server'] || Tap::Server.new
143
+ @request = Rack::Request.new(env)
144
+ @response = Rack::Response.new
145
+
146
+ # route to an action
147
+ blank, action, *args = request.path_info.split("/").collect {|arg| unescape(arg) }
148
+ action = "index" if action == nil || action.empty?
149
+
150
+ unless self.class.actions.include?(action.to_sym)
151
+ raise ServerError.new("404 Error: page not found", 404)
152
+ end
153
+
154
+ result = send(action, *args)
155
+ if result.kind_of?(String)
156
+ response.write result
157
+ response.finish
158
+ else
159
+ result
160
+ end
161
+ end
162
+
163
+ def render(path, options={})
164
+ options, path = path, nil if path.kind_of?(Hash)
165
+
166
+ # lookup template
167
+ template_path = case
168
+ when options.has_key?(:template)
169
+ server.template_path(options[:template])
170
+ else
171
+ server.template_path("#{self.class.name}/#{path}")
172
+ end
173
+
174
+ unless template_path
175
+ raise "could not find template for: #{path}"
176
+ end
177
+
178
+ # render template
179
+ template = server.content(template_path)
180
+ content = render_erb(template, options)
181
+
182
+ # render layout
183
+ layout = options[:layout]
184
+ layout = self.class.default_layout if layout == true
185
+ if layout
186
+ render(:template => layout, :locals => {:content => content})
187
+ else
188
+ content
189
+ end
190
+ end
191
+
192
+ def render_erb(template, options={})
193
+ # assign locals to the render binding
194
+ # this almost surely may be optimized...
195
+ locals = options[:locals]
196
+ binding = empty_binding
197
+
198
+ locals.each_pair do |key, value|
199
+ @assignment_value = value
200
+ eval("#{key} = remove_instance_variable(:@assignment_value)", binding)
201
+ end if locals
202
+
203
+ ERB.new(template, nil, "<>").result(binding)
204
+ end
205
+
206
+ # Redirects to the specified uri.
207
+ def redirect(uri, status=302, headers={}, body="")
208
+ response.status = status
209
+ response.headers.merge!(headers)
210
+ response.body = body
211
+
212
+ response['Location'] = uri
213
+ response.finish
214
+ end
215
+
216
+ # Returns a session hash.
217
+ def session
218
+ request.env['rack.session'] ||= {}
219
+ end
220
+
221
+ # Returns the app for the current session.
222
+ def app
223
+ server.app(session[:id] ||= server.initialize_session)
224
+ end
225
+
226
+ # Generates an empty binding to self without any locals assigned.
227
+ def empty_binding # :nodoc:
228
+ binding
229
+ end
230
+ end
231
+ end