dtas 0.0.0
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/.gemtest +0 -0
- data/.gitignore +9 -0
- data/.rsync_doc +3 -0
- data/COPYING +674 -0
- data/Documentation/.gitignore +3 -0
- data/Documentation/GNUmakefile +46 -0
- data/Documentation/dtas-console.txt +42 -0
- data/Documentation/dtas-ctl.txt +64 -0
- data/Documentation/dtas-cueedit.txt +24 -0
- data/Documentation/dtas-enq.txt +29 -0
- data/Documentation/dtas-msinkctl.txt +45 -0
- data/Documentation/dtas-player.txt +110 -0
- data/Documentation/dtas-player_effects.txt +45 -0
- data/Documentation/dtas-player_protocol.txt +181 -0
- data/Documentation/dtas-sinkedit.txt +41 -0
- data/Documentation/dtas-sourceedit.txt +33 -0
- data/Documentation/dtas-xdelay.txt +57 -0
- data/Documentation/troubleshooting.txt +13 -0
- data/GIT-VERSION-GEN +30 -0
- data/GNUmakefile +9 -0
- data/HACKING +12 -0
- data/INSTALL +53 -0
- data/README +103 -0
- data/Rakefile +97 -0
- data/TODO +4 -0
- data/bin/dtas-console +160 -0
- data/bin/dtas-ctl +10 -0
- data/bin/dtas-cueedit +78 -0
- data/bin/dtas-enq +13 -0
- data/bin/dtas-msinkctl +51 -0
- data/bin/dtas-player +34 -0
- data/bin/dtas-sinkedit +58 -0
- data/bin/dtas-sourceedit +48 -0
- data/bin/dtas-xdelay +85 -0
- data/dtas-linux.gemspec +18 -0
- data/dtas-mpris.gemspec +16 -0
- data/examples/dtas_state.yml +18 -0
- data/lib/dtas.rb +7 -0
- data/lib/dtas/buffer.rb +90 -0
- data/lib/dtas/buffer/read_write.rb +102 -0
- data/lib/dtas/buffer/splice.rb +142 -0
- data/lib/dtas/command.rb +43 -0
- data/lib/dtas/compat_onenine.rb +18 -0
- data/lib/dtas/disclaimer.rb +18 -0
- data/lib/dtas/format.rb +151 -0
- data/lib/dtas/pipe.rb +39 -0
- data/lib/dtas/player.rb +393 -0
- data/lib/dtas/player/client_handler.rb +463 -0
- data/lib/dtas/process.rb +87 -0
- data/lib/dtas/replaygain.rb +41 -0
- data/lib/dtas/rg_state.rb +99 -0
- data/lib/dtas/serialize.rb +9 -0
- data/lib/dtas/sigevent.rb +10 -0
- data/lib/dtas/sigevent/efd.rb +20 -0
- data/lib/dtas/sigevent/pipe.rb +28 -0
- data/lib/dtas/sink.rb +121 -0
- data/lib/dtas/source.rb +147 -0
- data/lib/dtas/source/command.rb +40 -0
- data/lib/dtas/source/common.rb +14 -0
- data/lib/dtas/source/mp3.rb +37 -0
- data/lib/dtas/state_file.rb +33 -0
- data/lib/dtas/unix_accepted.rb +76 -0
- data/lib/dtas/unix_client.rb +51 -0
- data/lib/dtas/unix_server.rb +110 -0
- data/lib/dtas/util.rb +15 -0
- data/lib/dtas/writable_iter.rb +22 -0
- data/perl/dtas-graph +129 -0
- data/pkg.mk +26 -0
- data/setup.rb +1586 -0
- data/test/covshow.rb +30 -0
- data/test/helper.rb +76 -0
- data/test/player_integration.rb +121 -0
- data/test/test_buffer.rb +216 -0
- data/test/test_format.rb +61 -0
- data/test/test_format_change.rb +49 -0
- data/test/test_player.rb +47 -0
- data/test/test_player_client_handler.rb +86 -0
- data/test/test_player_integration.rb +220 -0
- data/test/test_rg_integration.rb +117 -0
- data/test/test_rg_state.rb +32 -0
- data/test/test_sink.rb +32 -0
- data/test/test_sink_tee_integration.rb +34 -0
- data/test/test_source.rb +102 -0
- data/test/test_unixserver.rb +66 -0
- data/test/test_util.rb +15 -0
- metadata +208 -0
@@ -0,0 +1,463 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
|
3
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
|
+
module DTAS::Player::ClientHandler # :nodoc:
|
5
|
+
|
6
|
+
# returns true on success, wait_ctl arg on error
|
7
|
+
def set_bool(io, kv, v)
|
8
|
+
case v
|
9
|
+
when "false" then yield(false)
|
10
|
+
when "true" then yield(true)
|
11
|
+
else
|
12
|
+
return io.emit("ERR #{kv} must be true or false")
|
13
|
+
end
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
def adjust_numeric(io, obj, k, v)
|
18
|
+
negate = !!v.sub!(/\A-/, '')
|
19
|
+
case v
|
20
|
+
when %r{\A\+?\d*\.\d+\z}
|
21
|
+
num = v.to_f
|
22
|
+
when %r{\A\+?\d+\z}
|
23
|
+
num = v.to_i
|
24
|
+
else
|
25
|
+
return io.emit("ERR #{k}=#{v} must be a float")
|
26
|
+
end
|
27
|
+
num = -num if negate
|
28
|
+
|
29
|
+
if k.sub!(/\+\z/, '') # increment existing
|
30
|
+
num += obj.__send__(k)
|
31
|
+
elsif k.sub!(/-\z/, '') # decrement existing
|
32
|
+
num = obj.__send__(k) - num
|
33
|
+
# else # normal assignment
|
34
|
+
end
|
35
|
+
obj.__send__("#{k}=", num)
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
# returns true on success, wait_ctl arg on error
|
40
|
+
def set_int(io, kv, v, null_ok)
|
41
|
+
case v
|
42
|
+
when %r{\A-?\d+\z}
|
43
|
+
yield(v.to_i)
|
44
|
+
when ""
|
45
|
+
null_ok or return io.emit("ERR #{kv} must be defined")
|
46
|
+
yield(nil)
|
47
|
+
else
|
48
|
+
return io.emit("ERR #{kv} must an integer")
|
49
|
+
end
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
# returns true on success, wait_ctl arg on error
|
54
|
+
def set_uint(io, kv, v, null_ok)
|
55
|
+
case v
|
56
|
+
when %r{\A\d+\z}
|
57
|
+
yield(v.to_i)
|
58
|
+
when %r{\A0x[0-9a-fA-F]+\z}i # hex
|
59
|
+
yield(v.to_i(16))
|
60
|
+
when ""
|
61
|
+
null_ok or return io.emit("ERR #{kv} must be defined")
|
62
|
+
yield(nil)
|
63
|
+
else
|
64
|
+
return io.emit("ERR #{kv} must an non-negative integer")
|
65
|
+
end
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def __sink_activate(sink)
|
70
|
+
return if sink.pid
|
71
|
+
@targets.concat(sink.spawn(@format))
|
72
|
+
@targets.sort_by! { |t| t.sink.prio }
|
73
|
+
end
|
74
|
+
|
75
|
+
def drop_sink(sink)
|
76
|
+
@targets.delete_if do |t|
|
77
|
+
if t.sink == sink
|
78
|
+
drop_target(t)
|
79
|
+
true
|
80
|
+
else
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# called to activate/deactivate a sink
|
87
|
+
def __sink_switch(sink)
|
88
|
+
if sink.active
|
89
|
+
if @current
|
90
|
+
# maybe it's still alive for now, but it's just being killed
|
91
|
+
# do not reactivate it until we've reaped it
|
92
|
+
if sink.pid
|
93
|
+
drop_sink(sink)
|
94
|
+
sink.respawn = true
|
95
|
+
else
|
96
|
+
__sink_activate(sink)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
else
|
100
|
+
drop_sink(sink)
|
101
|
+
end
|
102
|
+
# if we change any sinks, make sure the event loop watches it for
|
103
|
+
# readability again, since we new sinks should be writable, and
|
104
|
+
# we've stopped waiting on killed sinks
|
105
|
+
@srv.wait_ctl(@sink_buf, :wait_readable)
|
106
|
+
end
|
107
|
+
|
108
|
+
# returns a wait_ctl arg
|
109
|
+
def sink_handler(io, msg)
|
110
|
+
name = msg[1]
|
111
|
+
case msg[0]
|
112
|
+
when "ls"
|
113
|
+
io.emit(Shellwords.join(@sinks.keys.sort))
|
114
|
+
when "rm"
|
115
|
+
sink = @sinks.delete(name) or return io.emit("ERR #{name} not found")
|
116
|
+
drop_sink(sink)
|
117
|
+
io.emit("OK")
|
118
|
+
when "ed"
|
119
|
+
sink = @sinks[name] || (new_sink = DTAS::Sink.new)
|
120
|
+
|
121
|
+
# allow things that look like audio device names ("hw:1,0" , "/dev/dsp")
|
122
|
+
# or variable names.
|
123
|
+
sink.valid_name?(name) or return io.emit("ERR sink name invalid")
|
124
|
+
|
125
|
+
sink.name = name
|
126
|
+
active_before = sink.active
|
127
|
+
|
128
|
+
# multiple changes may be made at once
|
129
|
+
msg[2..-1].each do |kv|
|
130
|
+
k, v = kv.split(/=/, 2)
|
131
|
+
case k
|
132
|
+
when %r{\Aenv\.([^=]+)\z}
|
133
|
+
sink.env[$1] = v
|
134
|
+
when %r{\Aenv#([^=]+)\z}
|
135
|
+
v == nil or return io.emit("ERR unset env has no value")
|
136
|
+
sink.env.delete($1)
|
137
|
+
when "prio"
|
138
|
+
rv = set_int(io, kv, v, false) { |i| sink.prio = i }
|
139
|
+
rv == true or return rv
|
140
|
+
@targets.sort_by! { |t| t.sink.prio } if sink.active
|
141
|
+
when "nonblock", "active"
|
142
|
+
rv = set_bool(io, kv, v) { |b| sink.__send__("#{k}=", b) }
|
143
|
+
rv == true or return rv
|
144
|
+
when "pipe_size"
|
145
|
+
rv = set_uint(io, kv, v, true) { |u| sink.__send__("#{k}=", u) }
|
146
|
+
rv == true or return rv
|
147
|
+
when "command" # nothing to validate, this could be "rm -rf /" :>
|
148
|
+
sink.command = v.empty? ? DTAS::Sink::SINK_DEFAULTS["command"] : v
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
@sinks[name] = new_sink if new_sink # no errors? it's a new sink!
|
153
|
+
|
154
|
+
# start or stop a sink if its active= flag changed. Additionally,
|
155
|
+
# account for a crashed-but-marked-active sink. The user may have
|
156
|
+
# fixed the command to not crash it.
|
157
|
+
if (active_before != sink.active) || (sink.active && !sink.pid)
|
158
|
+
__sink_switch(sink)
|
159
|
+
end
|
160
|
+
io.emit("OK")
|
161
|
+
when "cat"
|
162
|
+
sink = @sinks[name] or return io.emit("ERR #{name} not found")
|
163
|
+
io.emit(sink.to_hsh.to_yaml)
|
164
|
+
else
|
165
|
+
io.emit("ERR unknown sink op #{msg[0]}")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def bytes_decoded(src = @current)
|
170
|
+
bytes = src.dst.bytes_xfer - src.dst_zero_byte
|
171
|
+
bytes = bytes < 0 ? 0 : bytes # maybe negative in case of sink errors
|
172
|
+
end
|
173
|
+
|
174
|
+
def __seek_offset_adj(dir, offset)
|
175
|
+
if offset.sub!(/s\z/, '')
|
176
|
+
offset = offset.to_i
|
177
|
+
else # time
|
178
|
+
offset = @current.format.hhmmss_to_samples(offset)
|
179
|
+
end
|
180
|
+
n = __current_decoded_samples + (dir * offset)
|
181
|
+
n = 0 if n < 0
|
182
|
+
"#{n}s"
|
183
|
+
end
|
184
|
+
|
185
|
+
def __current_decoded_samples
|
186
|
+
initial = @current.offset_samples
|
187
|
+
decoded = @format.bytes_to_samples(bytes_decoded)
|
188
|
+
decoded = out_samples(decoded, @format, @current.format)
|
189
|
+
initial + decoded
|
190
|
+
end
|
191
|
+
|
192
|
+
def __current_requeue
|
193
|
+
return unless @current && @current.pid
|
194
|
+
|
195
|
+
# no need to requeue if we're already due to die
|
196
|
+
return if @current.requeued
|
197
|
+
@current.requeued = true
|
198
|
+
|
199
|
+
dst = @current.dst
|
200
|
+
# prepare to seek to the desired point based on the number of bytes which
|
201
|
+
# passed through dst buffer we want the offset for the @current file,
|
202
|
+
# which may have a different rate than our internal @format
|
203
|
+
if @current.respond_to?(:infile)
|
204
|
+
# this offset in the @current.format (not player @format)
|
205
|
+
@queue.unshift([ @current.infile, "#{__current_decoded_samples}s" ])
|
206
|
+
else
|
207
|
+
# DTAS::Source::Command (hash), just rerun it
|
208
|
+
@queue.unshift(@current.to_hsh)
|
209
|
+
end
|
210
|
+
# We also want to hard drop the buffer so we do not get repeated audio.
|
211
|
+
__buf_reset(dst)
|
212
|
+
end
|
213
|
+
|
214
|
+
def out_samples(in_samples, infmt, outfmt)
|
215
|
+
in_rate = infmt.rate
|
216
|
+
out_rate = outfmt.rate
|
217
|
+
return in_samples if in_rate == out_rate # easy!
|
218
|
+
(in_samples * out_rate / in_rate.to_f).round
|
219
|
+
end
|
220
|
+
|
221
|
+
# returns the number of samples we expect from the source
|
222
|
+
# this takes into account sample rate differences between the source
|
223
|
+
# and internal player format
|
224
|
+
def current_expect_samples(in_samples) # @current.samples
|
225
|
+
out_samples(in_samples, @current.format, @format)
|
226
|
+
end
|
227
|
+
|
228
|
+
def rg_handler(io, msg)
|
229
|
+
return io.emit(@rg.to_hsh.to_yaml) if msg.empty?
|
230
|
+
before = @rg.to_hsh
|
231
|
+
msg.each do |kv|
|
232
|
+
k, v = kv.split(/=/, 2)
|
233
|
+
case k
|
234
|
+
when "mode"
|
235
|
+
case v
|
236
|
+
when "off"
|
237
|
+
@rg.mode = nil
|
238
|
+
else
|
239
|
+
DTAS::RGState::RG_MODE.include?(v) or
|
240
|
+
return io.emit("ERR rg mode invalid")
|
241
|
+
@rg.mode = v
|
242
|
+
end
|
243
|
+
when "fallback_track"
|
244
|
+
rv = set_bool(io, kv, v) { |b| @rg.fallback_track = b }
|
245
|
+
rv == true or return rv
|
246
|
+
when %r{(?:gain_threshold|norm_threshold|
|
247
|
+
preamp|norm_level|fallback_gain)[+-]?\z}x
|
248
|
+
rv = adjust_numeric(io, @rg, k, v)
|
249
|
+
rv == true or return rv
|
250
|
+
end
|
251
|
+
end
|
252
|
+
after = @rg.to_hsh
|
253
|
+
__current_requeue if before != after
|
254
|
+
io.emit("OK")
|
255
|
+
end
|
256
|
+
|
257
|
+
def active_sinks
|
258
|
+
sinks = @targets.map { |t| t.sink }
|
259
|
+
sinks.uniq!
|
260
|
+
sinks
|
261
|
+
end
|
262
|
+
|
263
|
+
# show current info about what's playing
|
264
|
+
# returns non-blocking iterator retval
|
265
|
+
def current_handler(io, msg)
|
266
|
+
tmp = {}
|
267
|
+
if @current
|
268
|
+
tmp["current"] = s = @current.to_hsh
|
269
|
+
s["spawn_at"] = @current.spawn_at
|
270
|
+
s["pid"] = @current.pid
|
271
|
+
|
272
|
+
# this offset and samples in the player @format (not @current.format)
|
273
|
+
decoded = @format.bytes_to_samples(bytes_decoded)
|
274
|
+
if @current.respond_to?(:infile)
|
275
|
+
initial = tmp["current_initial"] = @current.offset_samples
|
276
|
+
initial = out_samples(initial, @current.format, @format)
|
277
|
+
tmp["current_expect"] = current_expect_samples(s["samples"])
|
278
|
+
s["format"] = @current.format.to_hash.delete_if { |_,v| v.nil? }
|
279
|
+
else
|
280
|
+
initial = 0
|
281
|
+
tmp["current_expect"] = nil
|
282
|
+
s["format"] = @format.to_hash.delete_if { |_,v| v.nil? }
|
283
|
+
end
|
284
|
+
tmp["current_offset"] = initial + decoded
|
285
|
+
end
|
286
|
+
tmp["current_inflight"] = @sink_buf.inflight
|
287
|
+
tmp["format"] = @format.to_hash.delete_if { |_,v| v.nil? }
|
288
|
+
tmp["paused"] = @paused
|
289
|
+
rg = @rg.to_hsh
|
290
|
+
tmp["rg"] = rg unless rg.empty?
|
291
|
+
if @targets[0]
|
292
|
+
sinks = active_sinks
|
293
|
+
tmp["sinks"] = sinks.map! do |sink|
|
294
|
+
h = sink.to_hsh
|
295
|
+
h["pid"] = sink.pid
|
296
|
+
h
|
297
|
+
end
|
298
|
+
end
|
299
|
+
io.emit(tmp.to_yaml)
|
300
|
+
end
|
301
|
+
|
302
|
+
def __buf_reset(buf)
|
303
|
+
@srv.wait_ctl(buf, :ignore)
|
304
|
+
buf.buf_reset
|
305
|
+
@srv.wait_ctl(buf, :wait_readable)
|
306
|
+
end
|
307
|
+
|
308
|
+
def skip_handler(io, msg)
|
309
|
+
__current_drop
|
310
|
+
echo("skip")
|
311
|
+
io.emit("OK")
|
312
|
+
end
|
313
|
+
|
314
|
+
def play_pause_handler(io, command)
|
315
|
+
prev = @paused
|
316
|
+
__send__("do_#{command}")
|
317
|
+
io.emit({
|
318
|
+
"paused" => {
|
319
|
+
"before" => prev,
|
320
|
+
"after" => @paused,
|
321
|
+
}
|
322
|
+
}.to_yaml)
|
323
|
+
end
|
324
|
+
|
325
|
+
def do_pause
|
326
|
+
return if @paused
|
327
|
+
echo("pause")
|
328
|
+
@paused = true
|
329
|
+
__current_requeue
|
330
|
+
end
|
331
|
+
|
332
|
+
def do_play
|
333
|
+
# no echo, next_source will echo on new track
|
334
|
+
@paused = false
|
335
|
+
return if @current && @current.pid
|
336
|
+
next_source(@queue.shift)
|
337
|
+
end
|
338
|
+
|
339
|
+
def do_play_pause
|
340
|
+
@paused ? do_play : do_pause
|
341
|
+
end
|
342
|
+
|
343
|
+
def do_seek(io, offset)
|
344
|
+
if @current && @current.pid
|
345
|
+
if @current.respond_to?(:infile)
|
346
|
+
begin
|
347
|
+
if offset.sub!(/\A\+/, '')
|
348
|
+
offset = __seek_offset_adj(1, offset)
|
349
|
+
elsif offset.sub!(/\A-/, '')
|
350
|
+
offset = __seek_offset_adj(-1, offset)
|
351
|
+
# else: pass to sox directly
|
352
|
+
end
|
353
|
+
rescue ArgumentError
|
354
|
+
return io.emit("ERR bad time format")
|
355
|
+
end
|
356
|
+
@queue.unshift([ @current.infile, offset ])
|
357
|
+
__buf_reset(@current.dst) # trigger EPIPE
|
358
|
+
else
|
359
|
+
return io.emit("ERR unseekable")
|
360
|
+
end
|
361
|
+
elsif @paused
|
362
|
+
case file = @queue[0]
|
363
|
+
when String
|
364
|
+
@queue[0] = [ file, offset ]
|
365
|
+
when Array
|
366
|
+
file[1] = offset
|
367
|
+
else
|
368
|
+
return io.emit("ERR unseekable")
|
369
|
+
end
|
370
|
+
# unpaused case... what do we do?
|
371
|
+
end
|
372
|
+
io.emit("OK")
|
373
|
+
end
|
374
|
+
|
375
|
+
def restart_pipeline
|
376
|
+
return if @paused
|
377
|
+
__current_requeue
|
378
|
+
@sinks.each_value { |sink| sink.respawn = sink.active }
|
379
|
+
@targets.each { |t| drop_target(t) }.clear
|
380
|
+
end
|
381
|
+
|
382
|
+
def format_handler(io, msg)
|
383
|
+
new_fmt = @format.dup
|
384
|
+
msg.each do |kv|
|
385
|
+
k, v = kv.split(/=/, 2)
|
386
|
+
case k
|
387
|
+
when "type"
|
388
|
+
new_fmt.valid_type?(v) or return io.emit("ERR invalid file type")
|
389
|
+
new_fmt.type = v
|
390
|
+
when "channels", "bits", "rate"
|
391
|
+
rv = set_uint(io, kv, v, false) { |u| new_fmt.__send__("#{k}=", u) }
|
392
|
+
rv == true or return rv
|
393
|
+
when "endian"
|
394
|
+
new_fmt.valid_endian?(v) or return io.emit("ERR invalid endian")
|
395
|
+
new_fmt.endian = v
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
if new_fmt != @format
|
400
|
+
restart_pipeline # calls __current_requeue
|
401
|
+
|
402
|
+
# we must assign this after __current_requeue since __current_requeue
|
403
|
+
# relies on the old @format for calculation
|
404
|
+
@format = new_fmt
|
405
|
+
end
|
406
|
+
io.emit("OK")
|
407
|
+
end
|
408
|
+
|
409
|
+
def env_handler(io, msg)
|
410
|
+
msg.each do |kv|
|
411
|
+
case kv
|
412
|
+
when %r{\A([^=]+)=(.*)\z}
|
413
|
+
ENV[$1] = $2
|
414
|
+
when %r{\A([^=]+)#}
|
415
|
+
ENV.delete($1)
|
416
|
+
else
|
417
|
+
return io.emit("ERR bad env")
|
418
|
+
end
|
419
|
+
end
|
420
|
+
io.emit("OK")
|
421
|
+
end
|
422
|
+
|
423
|
+
def source_handler(io, msg)
|
424
|
+
case msg.shift
|
425
|
+
when "cat"
|
426
|
+
io.emit({
|
427
|
+
"command" => @srccmd || DTAS::Source::SOURCE_DEFAULTS["command"],
|
428
|
+
"env" => @srcenv,
|
429
|
+
}.to_yaml)
|
430
|
+
when "ed"
|
431
|
+
before = [ @srccmd, @srcenv ].inspect
|
432
|
+
msg.each do |kv|
|
433
|
+
k, v = kv.split(/=/, 2)
|
434
|
+
case k
|
435
|
+
when "command"
|
436
|
+
@srccmd = v.empty? ? nil : v
|
437
|
+
when %r{\Aenv\.([^=]+)\z}
|
438
|
+
@srcenv[$1] = v
|
439
|
+
when %r{\Aenv#([^=]+)\z}
|
440
|
+
v == nil or return io.emit("ERR unset env has no value")
|
441
|
+
@srcenv.delete($1)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
after = [ @srccmd, @srcenv ].inspect
|
445
|
+
__current_requeue if before != after
|
446
|
+
io.emit("OK")
|
447
|
+
else
|
448
|
+
io.emit("ERR unknown source op")
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
def chdir_handler(io, msg)
|
453
|
+
msg.size == 1 or return io.emit("ERR usage: cd DIRNAME")
|
454
|
+
begin
|
455
|
+
Dir.chdir(msg[0])
|
456
|
+
rescue => e
|
457
|
+
return io.emit("ERR chdir: #{e.message}")
|
458
|
+
end
|
459
|
+
# echo(%W(cd msg[0])) # should we broadcast this?
|
460
|
+
io.emit("OK")
|
461
|
+
end
|
462
|
+
end
|
463
|
+
# :startdoc:
|