tap-server 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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