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 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: []