yajm 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in yajm.gemspec
4
+ gemspec
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
+
@@ -0,0 +1,3 @@
1
+ module Yajm
2
+ VERSION = "0.0.3"
3
+ end
data/lib/yajm.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'yajm/version'
2
+ require 'yajm/job_manager'
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: []