rubish 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +24 -0
- data/README.markdown +651 -0
- data/README.textile +651 -0
- data/Rakefile +52 -0
- data/VERSION.yml +4 -0
- data/bin/rubish +5 -0
- data/lib/rubish.rb +24 -0
- data/lib/rubish/awk.rb +54 -0
- data/lib/rubish/batch_executable.rb +27 -0
- data/lib/rubish/command.rb +91 -0
- data/lib/rubish/command_builder.rb +116 -0
- data/lib/rubish/context.rb +75 -0
- data/lib/rubish/executable.rb +251 -0
- data/lib/rubish/job.rb +90 -0
- data/lib/rubish/job_control.rb +62 -0
- data/lib/rubish/pipe.rb +68 -0
- data/lib/rubish/repl.rb +47 -0
- data/lib/rubish/sed.rb +37 -0
- data/lib/rubish/streamer.rb +347 -0
- data/lib/rubish/stub.rb +83 -0
- data/lib/rubish/unix_executable.rb +74 -0
- data/lib/rubish/workspace.rb +211 -0
- data/test/slowcat.rb +5 -0
- data/test/test.rb +896 -0
- data/test/test_dev.rb +50 -0
- metadata +81 -0
@@ -0,0 +1,251 @@
|
|
1
|
+
class Rubish::Executable
|
2
|
+
|
3
|
+
class ExecutableIO
|
4
|
+
class << self
|
5
|
+
# transactionally open a bunch of ios
|
6
|
+
# if one fails to open, close all others.
|
7
|
+
def ios(*specs)
|
8
|
+
success = false
|
9
|
+
begin
|
10
|
+
exe_ios = []
|
11
|
+
specs.each do |(spec,mode)|
|
12
|
+
exe_ios << prepare_io(spec,mode)
|
13
|
+
end
|
14
|
+
success =true
|
15
|
+
return exe_ios
|
16
|
+
ensure
|
17
|
+
unless success
|
18
|
+
exe_ios.each { |exe_io|
|
19
|
+
exe_io.close
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
# sorry, this is pretty hairy. This method
|
27
|
+
# instructs how exec should handle IO. (whether
|
28
|
+
# IO is done in a thread. whether it needs to be
|
29
|
+
# closed. (so on))
|
30
|
+
#
|
31
|
+
# an io could be
|
32
|
+
# String: interpreted as file name
|
33
|
+
# Number: file descriptor
|
34
|
+
# IO: Ruby IO object
|
35
|
+
# Block: executed in a thread, and a pipe connects the executable and the thread.
|
36
|
+
def prepare_io(io,mode)
|
37
|
+
# if the io given is a block, we execute it in a thread (passing it a pipe)
|
38
|
+
raise "invalid io mode: #{mode}" unless mode == "w" || mode == "r"
|
39
|
+
result =
|
40
|
+
case io
|
41
|
+
when $stdin, $stdout, $stderr
|
42
|
+
[io, false, nil]
|
43
|
+
when String
|
44
|
+
path = File.expand_path(io)
|
45
|
+
raise "path is a directory" if File.directory?(path)
|
46
|
+
[File.new(path,mode), true, nil]
|
47
|
+
when IO
|
48
|
+
[io, false,nil]
|
49
|
+
when Proc
|
50
|
+
proc = io
|
51
|
+
r,w = IO.pipe
|
52
|
+
# if we want to use a block to
|
53
|
+
# (1) input into executable
|
54
|
+
# - return "r" end from prepare_io, and
|
55
|
+
# the executable use this and standard
|
56
|
+
# input.
|
57
|
+
# - let the thread block writes to the "w" end
|
58
|
+
# (2) read from executable
|
59
|
+
# - return "w" from prepare_io
|
60
|
+
# - let thread reads from "r"
|
61
|
+
return_io, thread_io =
|
62
|
+
case mode
|
63
|
+
# case 1
|
64
|
+
when "r"
|
65
|
+
[r,w]
|
66
|
+
when "w"
|
67
|
+
# case 2
|
68
|
+
[w,r]
|
69
|
+
end
|
70
|
+
thread = Thread.new do
|
71
|
+
begin
|
72
|
+
proc.call(thread_io)
|
73
|
+
ensure
|
74
|
+
thread_io.close
|
75
|
+
end
|
76
|
+
end
|
77
|
+
[return_io, true, thread]
|
78
|
+
else
|
79
|
+
raise "not a valid input: #{io}"
|
80
|
+
end
|
81
|
+
return self.new(*result)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
attr_reader :thread
|
87
|
+
attr_reader :io
|
88
|
+
attr_reader :auto_close
|
89
|
+
|
90
|
+
def initialize(io,auto_close,thread)
|
91
|
+
@io = io
|
92
|
+
@auto_close = auto_close
|
93
|
+
@thread = thread
|
94
|
+
end
|
95
|
+
|
96
|
+
def close
|
97
|
+
if auto_close
|
98
|
+
io.close
|
99
|
+
else
|
100
|
+
#io.flush if io.stat.writable?
|
101
|
+
#io.flush rescue true # try flushing
|
102
|
+
end
|
103
|
+
if thread
|
104
|
+
begin
|
105
|
+
thread.join
|
106
|
+
ensure
|
107
|
+
if thread.alive?
|
108
|
+
thread.kill
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
attr_reader :working_directory
|
118
|
+
|
119
|
+
# only changes working directory for this executable
|
120
|
+
def cd(dir)
|
121
|
+
@working_directory = dir
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
def awk(a=nil,b=nil,&block)
|
126
|
+
if block
|
127
|
+
Rubish::Awk.new(self).act(a,b,&block)
|
128
|
+
else
|
129
|
+
Rubish::Awk.new(self)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def sed(a=nil,b=nil,&block)
|
134
|
+
if block
|
135
|
+
Rubish::Sed.new(self).act(a,b,&block)
|
136
|
+
else
|
137
|
+
Rubish::Sed.new(self)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def exec
|
142
|
+
raise "abstract"
|
143
|
+
end
|
144
|
+
|
145
|
+
def exec!
|
146
|
+
raise "abstract"
|
147
|
+
end
|
148
|
+
|
149
|
+
# methods for io redirection
|
150
|
+
def i(io=nil,&block)
|
151
|
+
return @__io_in unless io || block
|
152
|
+
@__io_in = io || block
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
def o(io=nil,&block)
|
157
|
+
return @__io_out unless io || block
|
158
|
+
@__io_out = io || block
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
def err(io=nil,&block)
|
163
|
+
return @__io_err unless io || block
|
164
|
+
@__io_err = io || block
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
def io(i=nil,o=nil)
|
169
|
+
i(i); o(o)
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
def i=(io)
|
174
|
+
@__io_in = io
|
175
|
+
end
|
176
|
+
|
177
|
+
def o=(io)
|
178
|
+
@__io_out = io
|
179
|
+
end
|
180
|
+
|
181
|
+
def err=(io)
|
182
|
+
@__io_err = io
|
183
|
+
end
|
184
|
+
|
185
|
+
def each!
|
186
|
+
self.o do |pipe|
|
187
|
+
pipe.each_line do |line|
|
188
|
+
line.chomp!
|
189
|
+
yield(line)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
job = self.exec!
|
193
|
+
return job
|
194
|
+
end
|
195
|
+
|
196
|
+
def each(&block)
|
197
|
+
job = self.each! &block
|
198
|
+
job.wait
|
199
|
+
end
|
200
|
+
|
201
|
+
# acc is the accumulator passed in by reference, and updated destructively.
|
202
|
+
def map!(acc,&block)
|
203
|
+
acc = Array.new unless acc
|
204
|
+
job = self.each! do |l|
|
205
|
+
acc << (block.nil? ? l : block.call(l))
|
206
|
+
end
|
207
|
+
return job
|
208
|
+
end
|
209
|
+
|
210
|
+
def map(&block)
|
211
|
+
acc = []
|
212
|
+
job = self.map!(acc,&block)
|
213
|
+
job.wait
|
214
|
+
return acc
|
215
|
+
end
|
216
|
+
|
217
|
+
def head(n=1,&block)
|
218
|
+
raise "n should be greater than 0: #{n}" unless n > 0
|
219
|
+
self.map do |l|
|
220
|
+
if n == 0
|
221
|
+
break
|
222
|
+
else
|
223
|
+
n -= 1
|
224
|
+
block ? block.call(l) : l
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def tail(n=1,&block)
|
230
|
+
raise "n should be greater than 0: #{n}" unless n > 0
|
231
|
+
acc = []
|
232
|
+
self.each do |l|
|
233
|
+
acc << (block ? block.call(l) : l)
|
234
|
+
if acc.size > n
|
235
|
+
acc.shift
|
236
|
+
end
|
237
|
+
end
|
238
|
+
return acc
|
239
|
+
end
|
240
|
+
|
241
|
+
def first
|
242
|
+
head(1).first
|
243
|
+
end
|
244
|
+
|
245
|
+
def last
|
246
|
+
tail(1).first
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
|
251
|
+
end
|
data/lib/rubish/job.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
|
2
|
+
# a job is not necessarily registered with job control.
|
3
|
+
class Rubish::Job
|
4
|
+
class Failure < Rubish::Error
|
5
|
+
attr_reader :job, :reason
|
6
|
+
def initialize(job,reason=nil)
|
7
|
+
raise "failure reason should be an Exception" unless reason.is_a?(Exception)
|
8
|
+
@job = job
|
9
|
+
@reason = reason
|
10
|
+
set_backtrace(reason.backtrace)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
@reason.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :result
|
19
|
+
attr_reader :job_control
|
20
|
+
|
21
|
+
# subclass initializer MUST call __start
|
22
|
+
def initialize(*args)
|
23
|
+
raise "abstract"
|
24
|
+
__start
|
25
|
+
end
|
26
|
+
|
27
|
+
def __start
|
28
|
+
@result = nil
|
29
|
+
@done = false
|
30
|
+
# when wait is called, the job control may or
|
31
|
+
# may not be the current job_control.
|
32
|
+
@job_control = Rubish::JobControl.current
|
33
|
+
@job_control.submit(self)
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def __finish
|
38
|
+
@job_control.remove(self)
|
39
|
+
@done = true
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# MUST call __finish in wait
|
44
|
+
# should return Job#result
|
45
|
+
def wait
|
46
|
+
raise "abstract"
|
47
|
+
__finish
|
48
|
+
end
|
49
|
+
|
50
|
+
# MUST result in calling __finish
|
51
|
+
def stop
|
52
|
+
raise "abstract"
|
53
|
+
end
|
54
|
+
|
55
|
+
def done?
|
56
|
+
@done
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
# carry out some computation in a thread.
|
62
|
+
class Rubish::Job::ThreadJob < Rubish::Job
|
63
|
+
attr_reader :thread
|
64
|
+
def initialize(&block)
|
65
|
+
# run block in a thread
|
66
|
+
@thread = ::Thread.new {
|
67
|
+
block.call
|
68
|
+
}
|
69
|
+
__start
|
70
|
+
end
|
71
|
+
|
72
|
+
def wait
|
73
|
+
# wait thread to completeggg64
|
74
|
+
begin
|
75
|
+
@thread.join
|
76
|
+
@result = @thread.value
|
77
|
+
return self.result
|
78
|
+
rescue => e
|
79
|
+
raise Rubish::Job::Failure.new(self,e)
|
80
|
+
ensure
|
81
|
+
__finish
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def stop
|
86
|
+
@thread.kill
|
87
|
+
wait
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
# A job is not necessarily registered with job
|
3
|
+
# control.
|
4
|
+
|
5
|
+
# TODO extend this for bg/fg (maybe?)
|
6
|
+
# Assume that job control is used by a single thread...
|
7
|
+
|
8
|
+
require 'thread'
|
9
|
+
class Rubish::JobControl
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def current
|
13
|
+
Rubish::Context.current.job_control
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@mutex = Mutex.new
|
19
|
+
@jobs = { }
|
20
|
+
end
|
21
|
+
|
22
|
+
def jobs
|
23
|
+
@jobs.values
|
24
|
+
end
|
25
|
+
|
26
|
+
# need to synchronize access to the jobs hash
|
27
|
+
def submit(job)
|
28
|
+
raise "expects a Rubish::JobControl::Job" unless job.is_a?(Rubish::Job)
|
29
|
+
@mutex.synchronize {
|
30
|
+
@jobs[job.object_id] = job
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove(job)
|
35
|
+
raise "expects a Rubish::JobControl::Job" unless job.is_a?(Rubish::Job)
|
36
|
+
raise Rubish::Error.new("Job not found: #{job}") unless @jobs.include?(job.object_id)
|
37
|
+
@mutex.synchronize {
|
38
|
+
@jobs.delete(job.object_id)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait(*jobs)
|
43
|
+
rss = jobs.map do |job|
|
44
|
+
job.wait
|
45
|
+
if block_given?
|
46
|
+
yield(job)
|
47
|
+
else
|
48
|
+
job
|
49
|
+
end
|
50
|
+
end
|
51
|
+
return *rss
|
52
|
+
end
|
53
|
+
|
54
|
+
# TODO handle interrupt
|
55
|
+
def waitall(&block)
|
56
|
+
wait(*@jobs.values,&block)
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
|
data/lib/rubish/pipe.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
class Rubish::Pipe < Rubish::UnixExecutable
|
3
|
+
attr_reader :cmds
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def build(workspace=nil,&block)
|
7
|
+
workspace ||= Rubish::Context.current.workspace
|
8
|
+
cmds = []
|
9
|
+
workspace.command_factory_hook = Proc.new { |cmd| cmds << cmd; cmd}
|
10
|
+
workspace.eval &block
|
11
|
+
self.new(cmds)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def initialize(cmds)
|
17
|
+
# dun wanna handle special case for now
|
18
|
+
raise "pipe length less than 2" if cmds.length < 2
|
19
|
+
raise "should build pipe only from Rubish::Command instances" unless cmds.all? { |c| c.is_a?(Rubish::Command)}
|
20
|
+
@cmds = cmds
|
21
|
+
end
|
22
|
+
|
23
|
+
def exec_with(pipe_in,pipe_out,pipe_err)
|
24
|
+
@cmds.each do |cmd|
|
25
|
+
cmd.normalize_args!
|
26
|
+
if cmd.i || cmd.o || cmd.err
|
27
|
+
raise "It's weird to redirect stdioe for command in a pipeline. Don't."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
# pipes == [i0,o1,i1,o2,i2...in,o0]
|
31
|
+
# i0 == $stdin
|
32
|
+
# o0 == $stdout
|
33
|
+
pipe = nil # [r, w]
|
34
|
+
pids = []
|
35
|
+
@cmds.each_index do |index|
|
36
|
+
tail = index == (@cmds.length - 1) # tail
|
37
|
+
head = index == 0 # head
|
38
|
+
if head
|
39
|
+
i = pipe_in
|
40
|
+
pipe = IO.pipe
|
41
|
+
o = pipe[1] # w
|
42
|
+
elsif tail
|
43
|
+
i = pipe[0]
|
44
|
+
o = pipe_out
|
45
|
+
else # middle
|
46
|
+
i = pipe[0] # r
|
47
|
+
pipe = IO.pipe
|
48
|
+
o = pipe[1]
|
49
|
+
end
|
50
|
+
|
51
|
+
cmd = @cmds[index]
|
52
|
+
if child = fork # children
|
53
|
+
#parent
|
54
|
+
pids << child
|
55
|
+
# it's important to close the pipes held
|
56
|
+
# by spawning parent, otherwise the pipes
|
57
|
+
# would not close after a program ends.
|
58
|
+
i.close unless head
|
59
|
+
o.close unless tail
|
60
|
+
else
|
61
|
+
# Rubish.set_stdioe((cmd.i || i),(cmd.o || o),(cmd.err || pipe_err))
|
62
|
+
cmd.system_exec(i,o,pipe_err)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
return pids
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|