sequenceserver 1.1.0.beta2 → 1.1.0.beta3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +16 -5
- data/README.md +3 -0
- data/lib/sequenceserver/api_errors.rb +67 -0
- data/lib/sequenceserver/blast/constants.rb +1 -1
- data/lib/sequenceserver/blast/formatter.rb +4 -2
- data/lib/sequenceserver/blast/job.rb +17 -62
- data/lib/sequenceserver/blast/report.rb +15 -2
- data/lib/sequenceserver/blast.rb +1 -0
- data/lib/sequenceserver/exceptions.rb +2 -3
- data/lib/sequenceserver/job.rb +72 -44
- data/lib/sequenceserver/routes.rb +29 -25
- data/lib/sequenceserver/version.rb +1 -1
- data/lib/sequenceserver.rb +32 -20
- data/public/js/errormodal.js +55 -0
- data/public/js/report.js +6 -2
- data/public/js/sequenceserver.js +0 -12
- data/public/sequenceserver-report.min.js +15 -15
- data/public/sequenceserver-search.min.js +3 -3
- data/sequenceserver.gemspec +2 -1
- data/spec/capybara_spec.rb +120 -21
- data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhd +8 -0
- data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhi +0 -0
- data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhr +0 -0
- data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nin +0 -0
- data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nog +0 -0
- data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nsd +16 -0
- data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nsi +0 -0
- data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nsq +0 -0
- data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.txt +8 -0
- data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phd +9140 -0
- data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phi +0 -0
- data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phr +0 -0
- data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pin +0 -0
- data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pog +0 -0
- data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.psd +18280 -0
- data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.psi +0 -0
- data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.psq +0 -0
- data/spec/database/sample/proteins/uniprot/URL +1 -0
- data/spec/nucleotide_query.fa +21 -0
- data/spec/protein_query.fa +21 -0
- data/spec/routes_spec.rb +2 -0
- data/spec/sample_reports/blastn_sample/job.yaml +1 -0
- data/spec/sample_reports/blastn_sample/{rfile → stdout} +0 -0
- data/spec/sample_reports/blastp_sample/job.yaml +1 -0
- data/spec/sample_reports/blastp_sample/{rfile → stdout} +0 -0
- data/spec/sample_reports/blastx_sample/job.yaml +1 -0
- data/spec/sample_reports/blastx_sample/{rfile → stdout} +0 -0
- data/spec/sample_reports/no_hits_sample/job.yaml +1 -0
- data/spec/sample_reports/no_hits_sample/{rfile → stdout} +0 -0
- data/spec/sample_reports/tblastn_sample/job.yaml +1 -0
- data/spec/sample_reports/tblastn_sample/{rfile → stdout} +0 -0
- data/spec/sample_reports/tblastx_sample/job.yaml +1 -0
- data/spec/sample_reports/tblastx_sample/{rfile → stdout} +0 -0
- data/spec/sample_reports/with_hits_sample/job.yaml +1 -0
- data/spec/sample_reports/with_hits_sample/{rfile → stdout} +0 -0
- data/spec/sequenceserver_spec.rb +1 -1
- data/spec/spec_helper.rb +2 -16
- data/views/layout.erb +0 -45
- metadata +55 -16
- data/lib/sequenceserver/blast/exceptions.rb +0 -27
- data/views/400.erb +0 -29
- data/views/500.erb +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e60bdefc921484208426a507ff32229e5945b1b
|
4
|
+
data.tar.gz: 85a162161cae976cc790ae3fed7b06ab81f11786
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4511c2651001127d25f0b319984ed4cbc5b4e4d6dccde42823b1c33fd14a99e86045d8416da1d8e514e6825df44933359d32eafe1167e0bdb30243d2017a295
|
7
|
+
data.tar.gz: 3917c29dfb8be319b92029295f3996338195aa3bab09a176c625b22e1f7b7fe56588aedf8d16b8724aa07acdfbe35c1c3982a3f2a3bbf47d165745d11cef4523
|
data/.travis.yml
CHANGED
@@ -6,7 +6,7 @@ rvm:
|
|
6
6
|
- 2.5
|
7
7
|
|
8
8
|
addons:
|
9
|
-
|
9
|
+
sauce_connect: true
|
10
10
|
|
11
11
|
branches:
|
12
12
|
only:
|
@@ -14,15 +14,26 @@ branches:
|
|
14
14
|
- master
|
15
15
|
|
16
16
|
install:
|
17
|
-
- wget -c "https://github.com/mozilla/geckodriver/releases/download/v0.20.0/geckodriver-v0.20.0-linux64.tar.gz"
|
18
|
-
- mkdir geckodriver && tar xvf geckodriver-*.tar.gz -C geckodriver
|
19
|
-
- export PATH="$PWD/geckodriver:$PATH"
|
20
17
|
- wget -c "ftp://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/2.6.0/ncbi-blast-2.6.0+-x64-linux.tar.gz"
|
21
18
|
- tar xvf ncbi-blast-*.tar.gz
|
22
19
|
- gem install bundler && bundle
|
23
20
|
- bundle exec bin/sequenceserver -s -b ncbi-blast-2.6.0+/bin -d spec/database/sample
|
24
21
|
|
25
|
-
|
22
|
+
before_script:
|
23
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter && chmod +x ./cc-test-reporter
|
24
|
+
- ./cc-test-reporter before-build
|
25
|
+
script:
|
26
|
+
- bundle exec rspec --exclude-pattern 'capybara*' # unit tests
|
27
|
+
- platform='macOS 10.13' browserName=safari browserVersion=11 bundle exec rspec spec/capybara_spec.rb
|
28
|
+
- platform='macOS 10.13' browserName=chrome browserVersion=65 bundle exec rspec spec/capybara_spec.rb
|
29
|
+
- platform='macOS 10.13' browserName=firefox browserVersion=59 bundle exec rspec spec/capybara_spec.rb
|
30
|
+
- platform='Windows 10' browserName=microsoftedge browserVersion='16.16299' bundle exec rspec spec/capybara_spec.rb
|
31
|
+
- platform='Windows 10' browserName=chrome browserVersion=65 bundle exec rspec spec/capybara_spec.rb
|
32
|
+
- platform='Windows 10' browserName=firefox browserVersion=59 bundle exec rspec spec/capybara_spec.rb
|
33
|
+
- platform='Linux' browserName=chrome browserVersion=48 bundle exec rspec spec/capybara_spec.rb
|
34
|
+
- platform='Linux' browserName=firefox browserVersion=45 bundle exec rspec spec/capybara_spec.rb
|
35
|
+
after_script:
|
36
|
+
- ./cc-test-reporter after-build # --exit-code $TRAVIS_TEST_RESULT
|
26
37
|
|
27
38
|
cache:
|
28
39
|
directories:
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
[![build status](https://secure.travis-ci.org/wurmlab/sequenceserver.png)](https://travis-ci.org/wurmlab/sequenceserver)
|
2
|
+
[![Sauce test status](https://saucelabs.com/buildstatus/yeban)](https://saucelabs.com/u/yeban)
|
2
3
|
[![code climate](https://codeclimate.com/github/wurmlab/sequenceserver/badges/gpa.svg)](https://codeclimate.com/github/wurmlab/sequenceserver)
|
3
4
|
[![coverage](https://codeclimate.com/github/wurmlab/sequenceserver/badges/coverage.svg)](https://codeclimate.com/github/wurmlab/sequenceserver)
|
4
5
|
[![gem version](https://badge.fury.io/rb/sequenceserver.svg)](http://rubygems.org/gems/sequenceserver)
|
@@ -6,6 +7,8 @@
|
|
6
7
|
|
7
8
|
[![gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/wurmlab/sequenceserver)
|
8
9
|
|
10
|
+
[![browser matrix](https://saucelabs.com/browser-matrix/yeban.svg)](https://saucelabs.com/u/yeban)
|
11
|
+
|
9
12
|
# SequenceServer - BLAST searching made easy!
|
10
13
|
|
11
14
|
SequenceServer lets you rapidly set up a BLAST+ server with an intuitive user
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module SequenceServer
|
2
|
+
|
3
|
+
# API errors have an http status, title, message, and additional information
|
4
|
+
# like stacktrace or information from program output.
|
5
|
+
class APIError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Job not found (404).
|
9
|
+
class NotFound < APIError
|
10
|
+
def http_status
|
11
|
+
404
|
12
|
+
end
|
13
|
+
|
14
|
+
def title
|
15
|
+
'Job not found'
|
16
|
+
end
|
17
|
+
|
18
|
+
def message
|
19
|
+
'The requested job could not be found'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Errors caused due to incorrect user input.
|
24
|
+
class InputError < APIError
|
25
|
+
def initialize(more_info)
|
26
|
+
@more_info = more_info
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def http_status
|
31
|
+
400
|
32
|
+
end
|
33
|
+
|
34
|
+
def title
|
35
|
+
'Input error'
|
36
|
+
end
|
37
|
+
|
38
|
+
def message
|
39
|
+
<<MSG
|
40
|
+
Looks like there's a problem with one of the query sequences, selected
|
41
|
+
databases, or advanced parameters.
|
42
|
+
MSG
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :more_info
|
46
|
+
end
|
47
|
+
|
48
|
+
# Errors caused by everything other than invalid user input.
|
49
|
+
class SystemError < APIError
|
50
|
+
def http_status
|
51
|
+
500
|
52
|
+
end
|
53
|
+
|
54
|
+
def title
|
55
|
+
'Job failed'
|
56
|
+
end
|
57
|
+
|
58
|
+
def message
|
59
|
+
<<MSG
|
60
|
+
Sorry BLAST failed - please try again. If this message persists, there is a
|
61
|
+
problem with the server. In this case, please report the bug on our
|
62
|
+
<a href="https://github.com/wurmlab/sequenceserver/issues" target="_blank">
|
63
|
+
issue tracker</a>.
|
64
|
+
MSG
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -24,7 +24,7 @@ module SequenceServer
|
|
24
24
|
attr_reader :format, :mime, :specifiers
|
25
25
|
|
26
26
|
def file
|
27
|
-
@file
|
27
|
+
@file ||= File.join(File.dirname(archive_file), filename)
|
28
28
|
end
|
29
29
|
|
30
30
|
def filename
|
@@ -41,12 +41,14 @@ module SequenceServer
|
|
41
41
|
" -outfmt '#{format} #{specifiers}'" \
|
42
42
|
" -out '#{file}'"
|
43
43
|
sys(command, path: config[:bin], dir: DOTDIR)
|
44
|
+
rescue CommandFailed => e
|
45
|
+
fail SystemError, e.stderr
|
44
46
|
end
|
45
47
|
|
46
48
|
def validate
|
47
49
|
return true if archive_file && format &&
|
48
50
|
File.exist?(archive_file)
|
49
|
-
fail
|
51
|
+
fail InputError, <<MSG
|
50
52
|
Incorrect request parameters. Please ensure that requested file name is
|
51
53
|
correct and the file type is either xml or tsv.
|
52
54
|
MSG
|
@@ -1,17 +1,10 @@
|
|
1
|
-
require 'sequenceserver/pool'
|
2
1
|
require 'sequenceserver/job'
|
3
2
|
|
4
|
-
require_relative 'exceptions'
|
5
|
-
require_relative 'constants'
|
6
|
-
|
7
3
|
module SequenceServer
|
8
4
|
|
9
5
|
# BLAST module.
|
10
6
|
module BLAST
|
11
7
|
# Extends SequenceServer::Job to describe a BLAST job.
|
12
|
-
#
|
13
|
-
# `BLAST::ArgumentError` and `BLAST::RuntimeError` signal errors
|
14
|
-
# encountered when attempting a BLAST search.
|
15
8
|
class Job < Job
|
16
9
|
|
17
10
|
def initialize(params)
|
@@ -25,60 +18,23 @@ module SequenceServer
|
|
25
18
|
end
|
26
19
|
end
|
27
20
|
|
28
|
-
|
29
21
|
attr_reader :advanced_params
|
22
|
+
|
30
23
|
# :nodoc:
|
31
|
-
# Attributes used by us.
|
32
|
-
#
|
33
|
-
# Should be considered private.
|
24
|
+
# Attributes used by us - should be considered private.
|
34
25
|
attr_reader :method, :qfile, :databases, :options
|
35
26
|
|
36
|
-
#
|
37
|
-
#
|
38
|
-
def
|
39
|
-
command
|
40
|
-
|
41
|
-
|
42
|
-
sys(command, stdout: rfile, stderr: efile, path: config[:bin])
|
43
|
-
done!
|
44
|
-
rescue CommandFailed => e
|
45
|
-
# Capture error.
|
46
|
-
status = e.exitstatus
|
47
|
-
case status
|
48
|
-
when 1 # error in query sequence or options; see [1]
|
49
|
-
efile.open
|
50
|
-
|
51
|
-
# Most of the time BLAST+ generates a verbose error message with
|
52
|
-
# details we don't require. So we parse out the relevant lines.
|
53
|
-
error = efile.each_line do |l|
|
54
|
-
break Regexp.last_match[1] if l.match(ERROR_LINE)
|
55
|
-
end
|
56
|
-
|
57
|
-
# But sometimes BLAST+ returns the exact/ relevant error message.
|
58
|
-
# Trying to parse such messages returns nil, and we use the error
|
59
|
-
# message from BLAST+ as it is.
|
60
|
-
error = efile.rewind && efile.read unless error.is_a? String
|
61
|
-
|
62
|
-
efile.close
|
63
|
-
fail ArgumentError, error
|
64
|
-
when 2, 3, 4, 255 # see [1]
|
65
|
-
efile.open
|
66
|
-
error = efile.read
|
67
|
-
efile.close
|
68
|
-
fail RuntimeError.new(status, error)
|
69
|
-
end
|
70
|
-
|
71
|
-
success!
|
72
|
-
end
|
73
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
74
|
-
# rubocop:enable Metrics/MethodLength
|
75
|
-
|
76
|
-
def rfile
|
77
|
-
File.join(dir, 'rfile')
|
27
|
+
# Returns the command that will be executed. Job super class takes care
|
28
|
+
# of actual execution.
|
29
|
+
def command
|
30
|
+
@command ||= "#{method} -db '#{databases.map(&:name).join(' ')}'" \
|
31
|
+
" -query '#{qfile}' #{options}"
|
78
32
|
end
|
79
33
|
|
80
|
-
|
81
|
-
|
34
|
+
# BLAST's exit status is not definitive of success or error, so we
|
35
|
+
# override success? to define custom criteria. :TODO:
|
36
|
+
def success?
|
37
|
+
super
|
82
38
|
end
|
83
39
|
|
84
40
|
private
|
@@ -114,38 +70,37 @@ module SequenceServer
|
|
114
70
|
|
115
71
|
def validate_method(method)
|
116
72
|
return true if ALGORITHMS.include? method
|
117
|
-
fail
|
73
|
+
fail InputError, 'BLAST algorithm should be one of:' \
|
118
74
|
" #{ALGORITHMS.join(', ')}."
|
119
75
|
end
|
120
76
|
|
121
77
|
def validate_sequences(sequences)
|
122
78
|
return true if sequences.is_a?(String) && !sequences.empty?
|
123
|
-
fail
|
79
|
+
fail InputError, 'Sequences should be a non-empty string.'
|
124
80
|
end
|
125
81
|
|
126
82
|
def validate_databases(database_ids)
|
127
83
|
ids = Database.ids
|
128
84
|
return true if database_ids.is_a?(Array) && !database_ids.empty? &&
|
129
85
|
(ids & database_ids).length == database_ids.length
|
130
|
-
fail
|
131
|
-
" #{ids.join("\n")}."
|
86
|
+
fail InputError, "Database id should be one of: #{ids.join("\n")}."
|
132
87
|
end
|
133
88
|
|
134
89
|
# Advanced options are specified by the user. Here they are checked for
|
135
90
|
# interference with SequenceServer operations.
|
136
91
|
#
|
137
|
-
# Raise
|
92
|
+
# Raise InputError if an error has occurred.
|
138
93
|
def validate_options(options)
|
139
94
|
return true if !options || (options.is_a?(String) &&
|
140
95
|
options.strip.empty?)
|
141
96
|
|
142
97
|
unless allowed_chars.match(options)
|
143
|
-
fail
|
98
|
+
fail InputError, 'Invalid characters detected in options.'
|
144
99
|
end
|
145
100
|
|
146
101
|
if disallowed_options.match(options)
|
147
102
|
failedopt = Regexp.last_match[0]
|
148
|
-
fail
|
103
|
+
fail InputError, "Option \"#{failedopt}\" is prohibited."
|
149
104
|
end
|
150
105
|
|
151
106
|
true
|
@@ -43,14 +43,27 @@ module SequenceServer
|
|
43
43
|
|
44
44
|
# Generate report.
|
45
45
|
def generate
|
46
|
-
|
47
|
-
|
46
|
+
assert_job_completed_successfully
|
47
|
+
xml_ir = parse_xml File.read(Formatter.run(job.stdout, 'xml').file)
|
48
|
+
tsv_ir = parse_tsv File.read(Formatter.run(job.stdout, 'custom_tsv').file)
|
48
49
|
extract_program_info xml_ir
|
49
50
|
extract_params xml_ir
|
50
51
|
extract_stats xml_ir
|
51
52
|
extract_queries xml_ir, tsv_ir
|
52
53
|
end
|
53
54
|
|
55
|
+
def assert_job_completed_successfully
|
56
|
+
return true if job.success?
|
57
|
+
|
58
|
+
stderr = File.readlines(job.stderr)
|
59
|
+
if job.exitstatus == 1 # error in query sequence or options; see [1]
|
60
|
+
error = stderr.grep(ERROR_LINE)
|
61
|
+
error = stderr if error.empty?
|
62
|
+
fail InputError, error.join
|
63
|
+
end
|
64
|
+
fail SystemError, stderr.join
|
65
|
+
end
|
66
|
+
|
54
67
|
# Make program name and program name + version available via `program`
|
55
68
|
# and `program_version` attributes.
|
56
69
|
def extract_program_info(ir)
|
data/lib/sequenceserver/blast.rb
CHANGED
@@ -148,13 +148,12 @@ MSG
|
|
148
148
|
# Raised if the 'sys' method could not successfully execute a shell command.
|
149
149
|
class CommandFailed < StandardError
|
150
150
|
|
151
|
-
def initialize(stdout, stderr
|
151
|
+
def initialize(exitstatus, stdout: nil, stderr: nil)
|
152
|
+
@exitstatus = exitstatus
|
152
153
|
@stdout = stdout
|
153
154
|
@stderr = stderr
|
154
|
-
@exitstatus = exitstatus
|
155
155
|
end
|
156
156
|
|
157
157
|
attr_reader :stdout, :stderr, :exitstatus
|
158
|
-
|
159
158
|
end
|
160
159
|
end
|
data/lib/sequenceserver/job.rb
CHANGED
@@ -5,44 +5,46 @@ require 'digest/md5'
|
|
5
5
|
require 'forwardable'
|
6
6
|
require 'securerandom'
|
7
7
|
|
8
|
+
require 'sequenceserver/pool'
|
9
|
+
require 'sequenceserver/api_errors'
|
10
|
+
|
8
11
|
module SequenceServer
|
9
|
-
#
|
12
|
+
# Abstract job super class.
|
13
|
+
#
|
14
|
+
# Provides a simple framework to store job data, execute shell commands
|
15
|
+
# asynchronously and capture stdout, stderr and exit status. Subclasses
|
16
|
+
# must provide a concrete implementation for `command` and may override
|
17
|
+
# any other methods as required.
|
10
18
|
#
|
11
|
-
#
|
12
|
-
#
|
19
|
+
# Global `config` and `logger` object are available as instance methods.
|
20
|
+
#
|
21
|
+
# Singleton methods provide the facility to create and queue a job,
|
22
|
+
# fetch a job or all jobs, and delete a job.
|
13
23
|
class Job
|
14
|
-
UUID_PATTERN = /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/
|
15
|
-
|
16
24
|
class << self
|
17
25
|
# Creates and queues a job. Returns created job object.
|
18
|
-
#
|
19
|
-
# TODO: Implement dynamic dispatch.
|
20
26
|
def create(params)
|
21
|
-
|
27
|
+
job = BLAST::Job.new(params)# TODO: Dynamic dispatch.
|
28
|
+
SequenceServer.pool.queue { job.run }
|
29
|
+
job
|
22
30
|
end
|
23
31
|
|
24
|
-
# Fetches job
|
25
|
-
#
|
26
|
-
# TODO: What if the given job id does not exist?
|
32
|
+
# Fetches job with the given id.
|
27
33
|
def fetch(id)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def all
|
32
|
-
jobdirs = Dir["#{DOTDIR}/*"].select{ |f| f =~ UUID_PATTERN }
|
33
|
-
jobdirs.map{ |fname| fetch File.basename(fname) }
|
34
|
+
job_file = File.join(DOTDIR, id, 'job.yaml')
|
35
|
+
fail NotFound unless File.exist?(job_file)
|
36
|
+
YAML.load_file(job_file)
|
34
37
|
end
|
35
38
|
|
39
|
+
# Deletes job with the given id.
|
36
40
|
def delete(id)
|
37
41
|
FileUtils.rm_r File.join(DOTDIR, id)
|
38
42
|
end
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
SequenceServer.pool.queue { job.run }
|
45
|
-
job
|
44
|
+
# Returns an Array of all jobs.
|
45
|
+
def all
|
46
|
+
Dir["#{DOTDIR}/**/job.yaml"].
|
47
|
+
map { |f| fetch File.basename File.dirname f }
|
46
48
|
end
|
47
49
|
end
|
48
50
|
|
@@ -66,32 +68,49 @@ module SequenceServer
|
|
66
68
|
raise e
|
67
69
|
end
|
68
70
|
|
69
|
-
attr_reader :id, :completed_at
|
71
|
+
attr_reader :id, :completed_at, :exitstatus
|
70
72
|
|
71
|
-
#
|
73
|
+
# Shell out and execute the job.
|
72
74
|
#
|
73
|
-
#
|
74
|
-
# call `done!` and `success!`, as appropriate, to indicate if the job is
|
75
|
-
# done and whether it was successful.
|
75
|
+
# NOTE: This method is called asynchronously by thread pool.
|
76
76
|
def run
|
77
|
-
|
77
|
+
sys(command, path: config[:bin], stdout: stdout, stderr: stderr)
|
78
|
+
done!
|
79
|
+
rescue CommandFailed => e
|
80
|
+
done! e.exitstatus
|
78
81
|
end
|
79
82
|
|
80
|
-
#
|
81
|
-
|
82
|
-
|
83
|
+
# Returns shell command that will be executed. Subclass needs to provide a
|
84
|
+
# concrete implementation.
|
85
|
+
def command
|
86
|
+
raise "Not implemented."
|
83
87
|
end
|
84
88
|
|
85
|
-
#
|
89
|
+
# Where will the stdout be written to during execution and read from later.
|
90
|
+
def stdout
|
91
|
+
File.join(dir, 'stdout')
|
92
|
+
end
|
93
|
+
|
94
|
+
# Where will the stderr be written to during execution and read from later.
|
95
|
+
def stderr
|
96
|
+
File.join(dir, 'stderr')
|
97
|
+
end
|
98
|
+
|
99
|
+
# Was the job successful?
|
86
100
|
def success?
|
87
|
-
|
101
|
+
exitstatus == 0
|
102
|
+
end
|
103
|
+
|
104
|
+
# Is the job done? Yes if exitstatus of the job is available. No otherwise.
|
105
|
+
def done?
|
106
|
+
!!@exitstatus
|
88
107
|
end
|
89
108
|
|
90
109
|
private
|
91
110
|
|
92
|
-
#
|
111
|
+
# Saves job object to a YAML file in job directory.
|
93
112
|
def save
|
94
|
-
File.write(
|
113
|
+
File.write(yfile, to_yaml)
|
95
114
|
end
|
96
115
|
|
97
116
|
# Save arbitrary blob of data for this job to a file. Returns absolute path
|
@@ -108,16 +127,20 @@ module SequenceServer
|
|
108
127
|
filename
|
109
128
|
end
|
110
129
|
|
111
|
-
#
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
130
|
+
# Retrieve file from job dir with the given name. Raises RuntimeError if
|
131
|
+
# the file can't be found.
|
132
|
+
#
|
133
|
+
# NOTE: Not used.
|
134
|
+
def fetch(key)
|
135
|
+
filename = File.join(dir, key)
|
136
|
+
raise if !File.exist? filename
|
137
|
+
filename
|
116
138
|
end
|
117
139
|
|
118
|
-
# Marks the job as
|
119
|
-
def
|
120
|
-
@
|
140
|
+
# Marks the job as done and save its exitstatus.
|
141
|
+
def done!(status = 0)
|
142
|
+
@completed_at = Time.now
|
143
|
+
@exitstatus = status
|
121
144
|
save
|
122
145
|
end
|
123
146
|
|
@@ -125,5 +148,10 @@ module SequenceServer
|
|
125
148
|
def dir
|
126
149
|
File.join(DOTDIR, id)
|
127
150
|
end
|
151
|
+
|
152
|
+
# Where to write serialised job object.
|
153
|
+
def yfile
|
154
|
+
File.join(dir, 'job.yaml')
|
155
|
+
end
|
128
156
|
end
|
129
157
|
end
|
@@ -12,16 +12,11 @@ module SequenceServer
|
|
12
12
|
disable :method_override
|
13
13
|
|
14
14
|
# Ensure exceptions never leak out of the app. Exceptions raised within
|
15
|
-
# the app must be handled by the app.
|
16
|
-
# blocks to exceptions we know how to handle and attaching to Exception
|
17
|
-
# as fallback.
|
15
|
+
# the app must be handled by the app.
|
18
16
|
disable :show_exceptions, :raise_errors
|
19
17
|
|
20
18
|
# Make it a policy to dump to 'rack.errors' any exception raised by the
|
21
|
-
# app
|
22
|
-
# to always work, Exceptions defined by us should not respond to `code`
|
23
|
-
# or `http_status` methods. Error blocks must explicitly set http
|
24
|
-
# status, if needed, by calling `status` method.
|
19
|
+
# app.
|
25
20
|
enable :dump_errors
|
26
21
|
|
27
22
|
# We don't want Sinatra do setup any loggers for us. We will use our own.
|
@@ -89,8 +84,7 @@ module SequenceServer
|
|
89
84
|
get '/:jid.json' do |jid|
|
90
85
|
job = Job.fetch(jid)
|
91
86
|
halt 202 unless job.done?
|
92
|
-
|
93
|
-
rep.to_json
|
87
|
+
Report.generate(job).to_json
|
94
88
|
end
|
95
89
|
|
96
90
|
# Returns base HTML. Rest happens client-side: polling for and rendering
|
@@ -132,24 +126,34 @@ module SequenceServer
|
|
132
126
|
send_file out.file, :filename => out.filename, :type => out.mime
|
133
127
|
end
|
134
128
|
|
135
|
-
#
|
136
|
-
#
|
137
|
-
#
|
138
|
-
|
139
|
-
|
129
|
+
# Catches any exception raised within the app and returns error message and
|
130
|
+
# the backtrace as response body. If the error class defines `http_status`
|
131
|
+
# instance method, its return value will be used to set HTTP status. HTTP
|
132
|
+
# status is set to 500 otherwise.
|
133
|
+
error 400..500 do
|
140
134
|
error = env['sinatra.error']
|
141
|
-
erb :'400', :layout => nil, :locals => { :error => error }
|
142
|
-
end
|
143
135
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
136
|
+
# All errors will have a message.
|
137
|
+
error_data = {message: error.message}
|
138
|
+
|
139
|
+
# If error object has a title method, use that, or use error class for
|
140
|
+
# title.
|
141
|
+
if error.respond_to? :title
|
142
|
+
error_data[:title] = error.title
|
143
|
+
else
|
144
|
+
error_data[:title] = error.class.to_s
|
145
|
+
end
|
146
|
+
|
147
|
+
# If error object has a more_info method, use that. If the error does
|
148
|
+
# not have more_info and the error is not APIError, use backtrace as
|
149
|
+
# more_info.
|
150
|
+
if error.respond_to? :more_info
|
151
|
+
error_data[:more_info] = error.more_info
|
152
|
+
else
|
153
|
+
error_data[:more_info] = error.backtrace.join("\n")
|
154
|
+
end
|
155
|
+
|
156
|
+
error_data.to_json
|
153
157
|
end
|
154
158
|
end
|
155
159
|
end
|