yajm 0.0.3
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 +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/lib/yajm/job.rb +85 -0
- data/lib/yajm/job_manager.rb +231 -0
- data/lib/yajm/version.rb +3 -0
- data/lib/yajm.rb +2 -0
- data/yajm.gemspec +23 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2a29284d81f85cc02371a19dab30aa94ea5f81e0
|
4
|
+
data.tar.gz: 98ec5d62ceeb18572517ae2a3ec9c0bfbeb5fad2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2758736cb64b6798e87fafbbe06b07f2d16dbf7db71a3d56bc264bacaf86aec04f65d8739995f4d7a0eed31c33d11d2c189fc8bd04b22724f5018135e0c759b7
|
7
|
+
data.tar.gz: 10d6f882c764e92be9efdf12d544d5972e3863d7723c8acd78d42c592391a4dba9b86921cb83cf66cdf198a35a5d8219b85e2ce38b15817f5ff675bf027b6b4a
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jose Luis Salas
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Yajm
|
2
|
+
|
3
|
+
A simple job manager inspired by Twke job manager.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'yajm'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install yajm
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/[my-github-username]/yajm/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
data/lib/yajm/job.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
module Yajm
|
5
|
+
class Job < EventMachine::Connection
|
6
|
+
attr_reader :start_time, :end_time, :pid, :command
|
7
|
+
|
8
|
+
def initialize(params)
|
9
|
+
@start_time = params[:start_time]
|
10
|
+
@end_time = nil
|
11
|
+
@pid = params[:pid]
|
12
|
+
@command = params[:command]
|
13
|
+
|
14
|
+
@opts = params
|
15
|
+
@dfr = EM::DefaultDeferrable.new
|
16
|
+
|
17
|
+
# All output is sent immediately to disk
|
18
|
+
@out_filename = File.join(@opts[:tmpdir], "output.txt")
|
19
|
+
@out_file = File.open(@out_filename, "w+")
|
20
|
+
@out_file.sync = true
|
21
|
+
@out_file.close_on_exec = true
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
alias_method :jid, :pid
|
26
|
+
|
27
|
+
def callback(&blk)
|
28
|
+
@dfr.callback(&blk)
|
29
|
+
end
|
30
|
+
|
31
|
+
def errback(&blk)
|
32
|
+
@dfr.errback(&blk)
|
33
|
+
end
|
34
|
+
|
35
|
+
def output
|
36
|
+
File.read(@out_filename)
|
37
|
+
end
|
38
|
+
|
39
|
+
def output_tail
|
40
|
+
%x{tail -n 20 #{@out_filename}}
|
41
|
+
end
|
42
|
+
|
43
|
+
def kill!
|
44
|
+
# Kill the process group
|
45
|
+
Process.kill("-TERM", self.pid) rescue 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def notify_readable
|
49
|
+
begin
|
50
|
+
result = @io.read_nonblock(1024)
|
51
|
+
@out_file.write(result)
|
52
|
+
rescue IO::WaitReadable
|
53
|
+
rescue EOFError
|
54
|
+
detach
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Invoked when the process completes and is passed the status
|
59
|
+
#
|
60
|
+
def finished(status)
|
61
|
+
@end_time = Time.now
|
62
|
+
if status.success?
|
63
|
+
@dfr.succeed(self)
|
64
|
+
else
|
65
|
+
@dfr.fail(self)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def unbind
|
70
|
+
@io.close
|
71
|
+
@out_file.fsync
|
72
|
+
@out_file.close
|
73
|
+
@out_file = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# Remove any temporary files
|
77
|
+
def cleanup
|
78
|
+
FileUtils.rm(@out_filename)
|
79
|
+
|
80
|
+
# XXX: could be rm-rf, but be safe for now. Might have
|
81
|
+
# problems if app creates files in $PWD
|
82
|
+
FileUtils.rmdir(@opts[:tmpdir])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yajm/job'
|
3
|
+
|
4
|
+
module Yajm
|
5
|
+
module JobManager
|
6
|
+
MAX_FD_CLOSE = 1024
|
7
|
+
|
8
|
+
# Watch the read end of the SIGCLD notication pipe
|
9
|
+
class ProcessPipeWatch < EM::Connection
|
10
|
+
def initialize(procwatch)
|
11
|
+
@procwatch = procwatch
|
12
|
+
end
|
13
|
+
|
14
|
+
def notify_readable
|
15
|
+
@procwatch.handle_process_exit
|
16
|
+
rescue EOFError
|
17
|
+
detach
|
18
|
+
end
|
19
|
+
|
20
|
+
def unbind
|
21
|
+
@io.close
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class ProcessWatch
|
26
|
+
# How long do we keep finished jobs around?
|
27
|
+
FINISHED_JOB_WAIT_SECS = 1800
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
# Active running jobs
|
31
|
+
@active = {}
|
32
|
+
|
33
|
+
# Finished queue -- entries are evicted after an hour
|
34
|
+
@finished = {}
|
35
|
+
|
36
|
+
rd, wr = IO::pipe
|
37
|
+
|
38
|
+
rd.close_on_exec = true
|
39
|
+
wr.close_on_exec = true
|
40
|
+
|
41
|
+
@watched_pids_fd = {:rd => rd, :wr => wr}
|
42
|
+
end
|
43
|
+
|
44
|
+
def start
|
45
|
+
conn = EM::watch(@watched_pids_fd[:rd], ProcessPipeWatch, self)
|
46
|
+
conn.notify_readable = true
|
47
|
+
|
48
|
+
EM::PeriodicTimer.new(300) do
|
49
|
+
purge_finished_jobs
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def purge_finished_jobs
|
54
|
+
now = Time.now
|
55
|
+
@finished.delete_if do |pid, job|
|
56
|
+
done = (now - job.end_time) > FINISHED_JOB_WAIT_SECS
|
57
|
+
job.cleanup if done
|
58
|
+
done
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Watch the PID and notify the spawned job
|
63
|
+
def watch_pid(pid, sj)
|
64
|
+
@active[pid] = sj
|
65
|
+
end
|
66
|
+
|
67
|
+
def active_jobs
|
68
|
+
@active.values.sort{|a, b| a.start_time <=> b.start_time }
|
69
|
+
end
|
70
|
+
|
71
|
+
def finished_jobs
|
72
|
+
@finished.values.sort{|a, b| a.end_time <=> b.end_time }
|
73
|
+
end
|
74
|
+
|
75
|
+
def job(id)
|
76
|
+
@active[id] || @finished[id]
|
77
|
+
end
|
78
|
+
|
79
|
+
def alert_exit
|
80
|
+
# Don't handle signal, just wake up the reactor
|
81
|
+
@watched_pids_fd[:wr].write_nonblock("1")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Invoked when a SIGCLD occurs
|
85
|
+
def handle_process_exit
|
86
|
+
begin
|
87
|
+
# Drain the pipe
|
88
|
+
begin
|
89
|
+
result = @watched_pids_fd[:rd].read_nonblock(1024)
|
90
|
+
rescue IO::WaitReadable
|
91
|
+
result = nil
|
92
|
+
end
|
93
|
+
end while result
|
94
|
+
|
95
|
+
# Check all processes waiting.
|
96
|
+
begin
|
97
|
+
begin
|
98
|
+
pid, status = Process.waitpid2(-1, Process::WNOHANG)
|
99
|
+
rescue Errno::ECHILD => err
|
100
|
+
pid = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
if pid
|
104
|
+
# If there is a callback, invoke it. The process may
|
105
|
+
# not belong to us.
|
106
|
+
#
|
107
|
+
proc = @active.delete(pid)
|
108
|
+
if proc
|
109
|
+
proc.finished(status)
|
110
|
+
@finished[proc.pid] = proc
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end while pid
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class << self
|
118
|
+
|
119
|
+
def init
|
120
|
+
return if @process_watcher
|
121
|
+
|
122
|
+
@process_watcher = ProcessWatch.new
|
123
|
+
@process_watcher.start
|
124
|
+
|
125
|
+
trap("CHLD") do
|
126
|
+
# Alert the process watcher that a process exited.
|
127
|
+
@process_watcher.alert_exit
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def list
|
132
|
+
# Return a list of the jobs
|
133
|
+
jobs = { :active => [], :finished => [] }
|
134
|
+
return jobs unless @process_watcher
|
135
|
+
|
136
|
+
jobs[:active] = @process_watcher.active_jobs
|
137
|
+
jobs[:finished] = @process_watcher.finished_jobs
|
138
|
+
jobs
|
139
|
+
end
|
140
|
+
|
141
|
+
def getjob(jid)
|
142
|
+
return nil unless @process_watcher
|
143
|
+
|
144
|
+
@process_watcher.job(jid)
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# When invoked, will spawn the command in 'cmdstr' using
|
149
|
+
# exec. Returns an EM:Deferrable and the success callback will
|
150
|
+
# be invoked if the command succeeds or else the errback will be
|
151
|
+
# invoked. Both callbacks are passed the program output.
|
152
|
+
#
|
153
|
+
def spawn(cmdstr, opts = {})
|
154
|
+
self.init
|
155
|
+
|
156
|
+
# All jobs have a temporary directory.
|
157
|
+
tmproot = ENV['TMPDIR'] || "/tmp"
|
158
|
+
jobtmpdir = File.join(tmproot, "jobs/job_#{rand 9999999}")
|
159
|
+
|
160
|
+
FileUtils.mkdir_p(jobtmpdir)
|
161
|
+
|
162
|
+
rd, wr = IO::pipe
|
163
|
+
start_time = Time.now
|
164
|
+
pid = fork do
|
165
|
+
rd.close
|
166
|
+
|
167
|
+
# Job control
|
168
|
+
#
|
169
|
+
Process.setpgid(Process.pid, Process.pid)
|
170
|
+
|
171
|
+
# Reset signals
|
172
|
+
trap("INT", "DEFAULT")
|
173
|
+
trap("QUIT", "DEFAULT")
|
174
|
+
trap("TSTP", "DEFAULT")
|
175
|
+
trap("TTIN", "DEFAULT")
|
176
|
+
trap("TTOU", "DEFAULT")
|
177
|
+
trap("CHLD", "DEFAULT")
|
178
|
+
|
179
|
+
# Fix lack of close-on-exec use
|
180
|
+
3.upto(MAX_FD_CLOSE) do |fd|
|
181
|
+
next if fd == wr.fileno
|
182
|
+
|
183
|
+
begin
|
184
|
+
f = IO.new(fd)
|
185
|
+
f.close
|
186
|
+
rescue
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Tie stdout and stderr together
|
191
|
+
$stdout.reopen wr
|
192
|
+
$stderr.reopen wr
|
193
|
+
|
194
|
+
# Set environs if specified
|
195
|
+
opts[:environ].each_pair do |k, v|
|
196
|
+
ENV[k] = v
|
197
|
+
end if opts[:environ]
|
198
|
+
|
199
|
+
dir = opts[:dir] || jobtmpdir
|
200
|
+
|
201
|
+
Dir.chdir(dir) do
|
202
|
+
exec(cmdstr)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Shouldn't get here unless the exec fails
|
206
|
+
exit 127
|
207
|
+
end
|
208
|
+
|
209
|
+
wr.close
|
210
|
+
rd.close_on_exec = true
|
211
|
+
|
212
|
+
params = {
|
213
|
+
:chdir => opts[:dir] || jobtmpdir,
|
214
|
+
:tmpdir => jobtmpdir,
|
215
|
+
:pid => pid,
|
216
|
+
:command => cmdstr,
|
217
|
+
:start_time => start_time,
|
218
|
+
}
|
219
|
+
|
220
|
+
d = EM::watch(rd, Job, params)
|
221
|
+
d.notify_readable = true
|
222
|
+
|
223
|
+
# Watch the process to notify when it completes
|
224
|
+
@process_watcher.watch_pid(pid, d)
|
225
|
+
|
226
|
+
return d
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
data/lib/yajm/version.rb
ADDED
data/lib/yajm.rb
ADDED
data/yajm.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'yajm/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "yajm"
|
8
|
+
spec.version = Yajm::VERSION
|
9
|
+
spec.authors = ["Jose Luis Salas"]
|
10
|
+
spec.email = ["josacar@gmail.com"]
|
11
|
+
spec.summary = %q{Yet another job manager.}
|
12
|
+
spec.description = %q{A simple job manager inspired by Twke job manager.}
|
13
|
+
spec.homepage = "https://github.com/josacar/yajm"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'eventmachine'
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: yajm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jose Luis Salas
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: eventmachine
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
description: A simple job manager inspired by Twke job manager.
|
42
|
+
email:
|
43
|
+
- josacar@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- Gemfile
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
- lib/yajm.rb
|
53
|
+
- lib/yajm/job.rb
|
54
|
+
- lib/yajm/job_manager.rb
|
55
|
+
- lib/yajm/version.rb
|
56
|
+
- yajm.gemspec
|
57
|
+
homepage: https://github.com/josacar/yajm
|
58
|
+
licenses:
|
59
|
+
- MIT
|
60
|
+
metadata: {}
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 2.2.2
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: Yet another job manager.
|
81
|
+
test_files: []
|