db_sucker 3.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/.gitignore +16 -0
- data/CHANGELOG.md +45 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +193 -0
- data/Rakefile +1 -0
- data/VERSION +1 -0
- data/bin/db_sucker +12 -0
- data/bin/db_sucker.sh +14 -0
- data/db_sucker.gemspec +29 -0
- data/doc/config_example.rb +53 -0
- data/doc/container_example.yml +150 -0
- data/lib/db_sucker/adapters/mysql2.rb +103 -0
- data/lib/db_sucker/application/colorize.rb +28 -0
- data/lib/db_sucker/application/container/accessors.rb +60 -0
- data/lib/db_sucker/application/container/ssh.rb +225 -0
- data/lib/db_sucker/application/container/validations.rb +53 -0
- data/lib/db_sucker/application/container/variation/accessors.rb +45 -0
- data/lib/db_sucker/application/container/variation/helpers.rb +21 -0
- data/lib/db_sucker/application/container/variation/worker_api.rb +65 -0
- data/lib/db_sucker/application/container/variation.rb +60 -0
- data/lib/db_sucker/application/container.rb +70 -0
- data/lib/db_sucker/application/container_collection.rb +47 -0
- data/lib/db_sucker/application/core.rb +222 -0
- data/lib/db_sucker/application/dispatch.rb +364 -0
- data/lib/db_sucker/application/evented_resultset.rb +149 -0
- data/lib/db_sucker/application/fake_channel.rb +22 -0
- data/lib/db_sucker/application/output_helper.rb +197 -0
- data/lib/db_sucker/application/sklaven_treiber/log_spool.rb +57 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/accessors.rb +105 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/core.rb +168 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/helpers.rb +144 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/io/base.rb +240 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/io/file_copy.rb +81 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/io/file_gunzip.rb +58 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/io/file_import_sql.rb +80 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/io/file_shasum.rb +49 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/io/pv_wrapper.rb +73 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/io/sftp_download.rb +57 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/io/throughput.rb +219 -0
- data/lib/db_sucker/application/sklaven_treiber/worker/routines.rb +313 -0
- data/lib/db_sucker/application/sklaven_treiber/worker.rb +48 -0
- data/lib/db_sucker/application/sklaven_treiber.rb +281 -0
- data/lib/db_sucker/application/slot_pool.rb +137 -0
- data/lib/db_sucker/application/tie.rb +25 -0
- data/lib/db_sucker/application/window/core.rb +185 -0
- data/lib/db_sucker/application/window/dialog.rb +142 -0
- data/lib/db_sucker/application/window/keypad/core.rb +85 -0
- data/lib/db_sucker/application/window/keypad.rb +174 -0
- data/lib/db_sucker/application/window/prompt.rb +124 -0
- data/lib/db_sucker/application/window.rb +329 -0
- data/lib/db_sucker/application.rb +168 -0
- data/lib/db_sucker/patches/beta-warning.rb +374 -0
- data/lib/db_sucker/patches/developer.rb +29 -0
- data/lib/db_sucker/patches/net-sftp.rb +20 -0
- data/lib/db_sucker/patches/thread-count.rb +30 -0
- data/lib/db_sucker/version.rb +4 -0
- data/lib/db_sucker.rb +81 -0
- metadata +217 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
module DbSucker
|
2
|
+
class Application
|
3
|
+
class SklavenTreiber
|
4
|
+
class Worker
|
5
|
+
SlotPoolNotInitializedError = Class.new(::RuntimeError)
|
6
|
+
ChannelFailRetryError = Class.new(::RuntimeError)
|
7
|
+
|
8
|
+
include Core
|
9
|
+
include Accessors
|
10
|
+
include Helpers
|
11
|
+
include Routines
|
12
|
+
|
13
|
+
attr_reader :exception, :ctn, :var, :table, :thread, :monitor, :step, :perform, :should_cancel, :sklaventreiber, :timings, :sshing
|
14
|
+
OutputHelper.hook(self)
|
15
|
+
|
16
|
+
def initialize sklaventreiber, ctn, var, table
|
17
|
+
@sklaventreiber = sklaventreiber
|
18
|
+
@ctn = ctn
|
19
|
+
@var = var
|
20
|
+
@table = table
|
21
|
+
@monitor = Monitor.new
|
22
|
+
@timings = {}
|
23
|
+
@deferred = false
|
24
|
+
@spinner_frames = sklaventreiber.window.try(:spinner_frames).try(:dup) || []
|
25
|
+
@current_perform = :unknown
|
26
|
+
@perform = %w[].tap do |perform|
|
27
|
+
perform << "r_dump_file"
|
28
|
+
perform << "r_calculate_raw_hash" if ctn.integrity?
|
29
|
+
perform << "r_compress_file"
|
30
|
+
perform << "r_calculate_compressed_hash" if ctn.integrity?
|
31
|
+
perform << "l_download_file"
|
32
|
+
perform << "l_verify_compressed_hash" if ctn.integrity?
|
33
|
+
perform << "l_copy_file" if var.copies_file? && var.copies_file_compressed?
|
34
|
+
if var.requires_uncompression?
|
35
|
+
perform << "l_decompress_file"
|
36
|
+
perform << "l_verify_raw_hash" if ctn.integrity?
|
37
|
+
perform << "l_copy_file" if var.copies_file? && !var.copies_file_compressed?
|
38
|
+
perform << "l_import_file" if var.data["database"]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
@state = :pending
|
43
|
+
@status = ["waiting...", "gray"]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
module DbSucker
|
2
|
+
class Application
|
3
|
+
class SklavenTreiber
|
4
|
+
attr_reader :app, :trxid, :window, :data, :status, :monitor, :workers, :poll, :throughput, :slot_pools
|
5
|
+
|
6
|
+
def initialize app, trxid
|
7
|
+
@app = app
|
8
|
+
@trxid = trxid
|
9
|
+
@status = ["initializing", "gray"]
|
10
|
+
@monitor = Monitor.new
|
11
|
+
@workers = []
|
12
|
+
@threads = []
|
13
|
+
@slot_pools = {}
|
14
|
+
@sleep_before_exit = 0
|
15
|
+
@throughput = Worker::IO::Throughput.new(self)
|
16
|
+
|
17
|
+
@data = {
|
18
|
+
database: nil,
|
19
|
+
tables_transfer: nil,
|
20
|
+
tables_transfer_list: [],
|
21
|
+
tables_total: nil,
|
22
|
+
tables_done: 0,
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def sync
|
27
|
+
@monitor.synchronize { yield }
|
28
|
+
end
|
29
|
+
|
30
|
+
def pause_worker worker
|
31
|
+
sync { worker.pause }
|
32
|
+
end
|
33
|
+
|
34
|
+
def unpause_worker worker
|
35
|
+
sync { worker.unpause }
|
36
|
+
end
|
37
|
+
|
38
|
+
def pause_all_workers
|
39
|
+
sync { @workers.each {|wrk| pause_worker(wrk) } }
|
40
|
+
end
|
41
|
+
|
42
|
+
def unpause_all_workers
|
43
|
+
sync { @workers.each {|wrk| unpause_worker(wrk) } }
|
44
|
+
end
|
45
|
+
|
46
|
+
def spooled
|
47
|
+
stdout_was = app.opts[:stdout]
|
48
|
+
app.opts[:stdout] = SklavenTreiber::LogSpool.new(stdout_was) if app.opts[:window_enabled]
|
49
|
+
yield if block_given?
|
50
|
+
ensure
|
51
|
+
app.opts[:stdout].spooldown do |meth, args, time|
|
52
|
+
stdout_was.send(meth, *args)
|
53
|
+
end if app.opts[:stdout].respond_to?(:spooldown)
|
54
|
+
app.opts[:stdout] = stdout_was
|
55
|
+
end
|
56
|
+
|
57
|
+
def whip_it! ctn, var
|
58
|
+
@ctn, @var = ctn, var
|
59
|
+
|
60
|
+
_start_ssh_poll
|
61
|
+
_init_window
|
62
|
+
_check_remote_tmp_directory
|
63
|
+
_select_tables
|
64
|
+
_initialize_slot_pools
|
65
|
+
_initialize_workers
|
66
|
+
@ctn.pv_utility # lazy load
|
67
|
+
@poll[:force] = false
|
68
|
+
@throughput.start_loop
|
69
|
+
|
70
|
+
@sleep_before_exit = 3 if @window
|
71
|
+
_run_consumers
|
72
|
+
ensure
|
73
|
+
app.sandboxed do
|
74
|
+
@status = ["terminating (canceling workers)", "red"]
|
75
|
+
@workers.each {|w| catch(:abort_execution) { w.cancel! } }
|
76
|
+
end
|
77
|
+
app.sandboxed do
|
78
|
+
@status = ["terminating (SSH poll)", "red"]
|
79
|
+
if @poll
|
80
|
+
@poll[:force] = false
|
81
|
+
@poll.join
|
82
|
+
end
|
83
|
+
end
|
84
|
+
@status = ["terminated", "red"]
|
85
|
+
sleep @sleep_before_exit
|
86
|
+
app.sandboxed { @window.try(:stop) }
|
87
|
+
app.sandboxed { @ctn.try(:sftp_end) }
|
88
|
+
app.sandboxed { @throughput.try(:stop_loop) }
|
89
|
+
app.sandboxed { @slot_pools.each{|n, p| p.close! } }
|
90
|
+
app.sandboxed do
|
91
|
+
app.puts @window.try(:_render_final_results)
|
92
|
+
end
|
93
|
+
@ctn, @var = nil, nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def _init_window
|
97
|
+
return unless app.opts[:window_enabled]
|
98
|
+
@window = Window.new(app, self)
|
99
|
+
@window.init!
|
100
|
+
@window.start
|
101
|
+
end
|
102
|
+
|
103
|
+
def _check_remote_tmp_directory
|
104
|
+
@status = ["checking remote temp directory", "blue"]
|
105
|
+
@ctn.sftp_begin
|
106
|
+
@ctn.sftp_start do |sftp|
|
107
|
+
# check tmp directory
|
108
|
+
app.debug "Checking remote temp directory #{app.c @ctn.tmp_path, :magenta}"
|
109
|
+
begin
|
110
|
+
sftp.dir.glob("#{@ctn.tmp_path}", "**/*")
|
111
|
+
rescue Net::SFTP::StatusException => ex
|
112
|
+
if ex.message["no such file"]
|
113
|
+
app.abort "Temp directory `#{@ctn.tmp_path}' does not exist on the remote side!", 2
|
114
|
+
else
|
115
|
+
raise
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def _select_tables
|
122
|
+
@status = ["selecting tables for transfer", "blue"]
|
123
|
+
ttt, at = @var.tables_to_transfer
|
124
|
+
|
125
|
+
# apply only/except filters provided via command line
|
126
|
+
if @app.opts[:suck_only].any? && @app.opts[:suck_except].any?
|
127
|
+
raise OptionParser::InvalidArgument, "only one of `--only' or `--except' option can be provided at the same time"
|
128
|
+
elsif @app.opts[:suck_only].any?
|
129
|
+
unless (r = @app.opts[:suck_only] - at).empty?
|
130
|
+
raise Container::TableNotFoundError, "table(s) `#{r * ", "}' for the database `#{@ctn.source["database"]}' could not be found (provided via --only, variation `#{@ctn.name}/#{@var.name}' in `#{@ctn.src}')"
|
131
|
+
end
|
132
|
+
ttt = @app.opts[:suck_only]
|
133
|
+
elsif @app.opts[:suck_except].any?
|
134
|
+
unless (r = @app.opts[:suck_except] - at).empty?
|
135
|
+
raise Container::TableNotFoundError, "table(s) `#{r * ", "}' for the database `#{@ctn.source["database"]}' could not be found (provided via --except, variation `#{@ctn.name}/#{@var.name}' in `#{@ctn.src}')"
|
136
|
+
end
|
137
|
+
ttt = ttt - @app.opts[:suck_except]
|
138
|
+
end
|
139
|
+
|
140
|
+
@data[:database] = @ctn.source["database"]
|
141
|
+
@data[:tables_transfer] = ttt.length
|
142
|
+
@data[:tables_transfer_list] = ttt
|
143
|
+
@data[:window_col1] = ttt.map(&:length).max
|
144
|
+
@data[:tables_total] = at.length
|
145
|
+
end
|
146
|
+
|
147
|
+
def _initialize_slot_pools
|
148
|
+
app.opts[:slot_pools].each do |name, slots|
|
149
|
+
@slot_pools[name] = SlotPool.new(slots, name)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def _initialize_workers
|
154
|
+
@status = ["initializing workers 0/#{@data[:tables_transfer]}", "blue"]
|
155
|
+
|
156
|
+
@data[:tables_transfer_list].each_with_index do |table, index|
|
157
|
+
@status = ["initializing workers #{index+1}/#{@data[:tables_transfer]}", "blue"]
|
158
|
+
@workers << Worker.new(self, @ctn, @var, table)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def _start_ssh_poll
|
163
|
+
wait_lock = Queue.new
|
164
|
+
@poll = app.spawn_thread(:sklaventreiber_ssh_poll) do |thr|
|
165
|
+
thr[:force] = true
|
166
|
+
thr[:iteration] = 0
|
167
|
+
thr[:errors] = 0
|
168
|
+
wait_lock << true
|
169
|
+
begin
|
170
|
+
@ctn.loop_ssh(0.1) {
|
171
|
+
thr[:iteration] += 1
|
172
|
+
thr[:last_iteration] = Time.current
|
173
|
+
thr[:force] || @workers.select{|w| !w.done? || w.sshing }.any?
|
174
|
+
}
|
175
|
+
rescue Container::SSH::ChannelOpenFailedError
|
176
|
+
thr[:errors] += 1
|
177
|
+
sleep 0.5
|
178
|
+
retry
|
179
|
+
end
|
180
|
+
|
181
|
+
if thr[:errors].zero?
|
182
|
+
app.debug "SSH error count (#{thr[:errors]})"
|
183
|
+
elsif thr[:errors] > 25
|
184
|
+
app.warning "SSH error count (#{thr[:errors]}) is high! Verify remote MaxSessions setting or lower concurrent worker count."
|
185
|
+
else
|
186
|
+
app.warning "SSH errors occured (#{thr[:errors]})! Verify remote MaxSessions setting or lower concurrent worker count."
|
187
|
+
end
|
188
|
+
end
|
189
|
+
wait_lock.pop
|
190
|
+
sleep 0.01 until @poll[:iteration] && @poll[:iteration] > 0
|
191
|
+
end
|
192
|
+
|
193
|
+
def _run_consumers
|
194
|
+
cnum = [app.opts[:consumers], @data[:tables_transfer]].min
|
195
|
+
@data[:window_col2] = cnum.to_s.length
|
196
|
+
if cnum <= 1
|
197
|
+
_run_in_main_thread
|
198
|
+
else
|
199
|
+
_run_in_threads(cnum)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def _run_in_main_thread
|
204
|
+
@status = ["running in main thread...", "green"]
|
205
|
+
|
206
|
+
# control thread
|
207
|
+
ctrlthr = app.spawn_thread(:sklaventreiber_worker_ctrl) do |thr|
|
208
|
+
loop do
|
209
|
+
_control_thread
|
210
|
+
break if thr[:stop]
|
211
|
+
thr.wait(0.1)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
begin
|
216
|
+
Thread.current[:managed_worker] = :main
|
217
|
+
_queueoff
|
218
|
+
ensure
|
219
|
+
ctrlthr[:stop] = true
|
220
|
+
ctrlthr.signal.join
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def _run_in_threads(cnum)
|
225
|
+
@status = ["starting consumer 0/#{cnum}", "blue"]
|
226
|
+
|
227
|
+
# initializing consumer threads
|
228
|
+
cnum.times do |wi|
|
229
|
+
@status = ["starting consumer #{wi+1}/#{cnum}", "blue"]
|
230
|
+
@threads << app.spawn_thread(:sklaventreiber_worker) {|thr|
|
231
|
+
begin
|
232
|
+
thr[:managed_worker] = wi
|
233
|
+
thr.wait(0.1) until thr[:start] || $core_runtime_exiting
|
234
|
+
_queueoff
|
235
|
+
rescue Interrupt
|
236
|
+
end
|
237
|
+
}
|
238
|
+
end
|
239
|
+
|
240
|
+
# start consumer threads
|
241
|
+
@status = ["running", "green"]
|
242
|
+
@threads.each{|t| t[:start] = true; t.signal }
|
243
|
+
|
244
|
+
# master thread (control)
|
245
|
+
while @threads.any?(&:alive?)
|
246
|
+
_control_thread
|
247
|
+
Thread.current.wait(0.1)
|
248
|
+
end
|
249
|
+
@threads.each(&:join)
|
250
|
+
end
|
251
|
+
|
252
|
+
def _queueoff
|
253
|
+
loop do
|
254
|
+
return if $core_runtime_exiting
|
255
|
+
worker = false
|
256
|
+
sync do
|
257
|
+
pending = @workers.select(&:pending?)
|
258
|
+
return unless pending.any?
|
259
|
+
worker = pending.first.aquire(Thread.current)
|
260
|
+
end
|
261
|
+
if worker
|
262
|
+
begin
|
263
|
+
worker.run
|
264
|
+
ensure
|
265
|
+
sync { @data[:tables_done] += 1 }
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def _control_thread
|
272
|
+
if $core_runtime_exiting && $core_runtime_exiting < 100
|
273
|
+
$core_runtime_exiting += 100
|
274
|
+
app.sandboxed { @workers.each {|w| catch(:abort_execution) { w.cancel! } } }
|
275
|
+
app.sandboxed { @slot_pools.each{|n, p| p.softclose! } }
|
276
|
+
app.wakeup_handlers
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module DbSucker
|
2
|
+
class Application
|
3
|
+
class SlotPool
|
4
|
+
SlotAllocationError = Class.new(::RuntimeError)
|
5
|
+
PoolAlreadyClosedError = Class.new(SlotAllocationError)
|
6
|
+
|
7
|
+
def initialize slots = 1, name = nil
|
8
|
+
@name = name
|
9
|
+
@slots = slots
|
10
|
+
@monitor = Monitor.new
|
11
|
+
@closed = false
|
12
|
+
@softclosed = false
|
13
|
+
@waiting = []
|
14
|
+
@active = []
|
15
|
+
@signal = @monitor.new_cond
|
16
|
+
@closed_signal = @monitor.new_cond
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.expose what, &how
|
20
|
+
define_method(what) do |*args, &block|
|
21
|
+
sync { instance_exec(*args, &how) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def sync &block
|
26
|
+
@monitor.synchronize(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
expose(:name) { @name }
|
30
|
+
expose(:active) { @active }
|
31
|
+
expose(:active?) { @active.any? }
|
32
|
+
expose(:waiting) { @waiting }
|
33
|
+
expose(:waiting?) { @waiting.any? }
|
34
|
+
expose(:closed?) { @closed }
|
35
|
+
expose(:softclosed?) { @softclosed }
|
36
|
+
expose(:slots) { @slots }
|
37
|
+
expose(:available_slots) { @slots ? @slots - @active.length : 1.0/0 }
|
38
|
+
expose(:slots?) { @slots ? available_slots > 0 : true }
|
39
|
+
|
40
|
+
def close
|
41
|
+
sync do
|
42
|
+
@closed = true
|
43
|
+
@signal.broadcast
|
44
|
+
end
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def close!
|
49
|
+
sync do
|
50
|
+
close
|
51
|
+
@closed_signal.wait if active?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def softclose!
|
56
|
+
@softclosed = true
|
57
|
+
dequeue_waiting!
|
58
|
+
end
|
59
|
+
|
60
|
+
def dequeue_waiting!
|
61
|
+
sync do
|
62
|
+
while @waiting.any?
|
63
|
+
_wthr, _tthr = @waiting.shift
|
64
|
+
_wthr.signal
|
65
|
+
end
|
66
|
+
@signal.broadcast
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def qindex thr = nil
|
71
|
+
thr ||= Thread.current
|
72
|
+
sync do
|
73
|
+
index = @waiting.find_index {|wthr, tthr| tthr == thr }
|
74
|
+
index ? index + 1 : false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def puts *a
|
79
|
+
Thread.main[:app].puts(*a)
|
80
|
+
end
|
81
|
+
|
82
|
+
def aquired? tthr = nil
|
83
|
+
@active.include?(tthr || Thread.current)
|
84
|
+
end
|
85
|
+
|
86
|
+
def wait_aquired tthr = nil
|
87
|
+
tthr ||= Thread.current
|
88
|
+
#tthr.wait(0.1) until qindex(tthr)
|
89
|
+
loop do
|
90
|
+
sync do
|
91
|
+
#puts "<#{Time.current.to_f}-#{tthr[:current_task]}> wait for index"
|
92
|
+
#puts "<#{Time.current.to_f}-#{tthr[:current_task]}> has #{available_slots} slots"
|
93
|
+
while slots? && @waiting.any?
|
94
|
+
_wthr, _tthr = @waiting.shift
|
95
|
+
#puts "<#{Time.current.to_f}-#{_tthr[:current_task]}> running now"
|
96
|
+
@active.push(_tthr) unless @softclosed
|
97
|
+
_tthr.signal
|
98
|
+
_wthr.signal
|
99
|
+
end
|
100
|
+
unless qindex(tthr)
|
101
|
+
#puts "<#{Time.current.to_f}-#{tthr[:current_task]}> return"
|
102
|
+
return
|
103
|
+
end
|
104
|
+
#puts "<#{Time.current.to_f}-#{tthr[:current_task]}> wait"
|
105
|
+
@signal.wait #(1)
|
106
|
+
#puts "<#{Time.current.to_f}-#{tthr[:current_task]}> wait DONE"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def aquire tthr = nil
|
112
|
+
wthr = Thread.current
|
113
|
+
tthr ||= Thread.current
|
114
|
+
sync do
|
115
|
+
raise PoolAlreadyClosedError, "slot pool has already been closed, cannot aquire slot" if closed?
|
116
|
+
@waiting << [wthr, tthr]
|
117
|
+
#puts "<#{Time.current.to_f}-#{tthr[:current_task]}> broadcasting signal after adding new waiter"
|
118
|
+
@signal.broadcast # signal polling threads
|
119
|
+
end
|
120
|
+
tthr.signal # signal target thread to continue and poll
|
121
|
+
wthr.wait # suspend thread until we aquired it
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
def release tthr = nil
|
126
|
+
sync do
|
127
|
+
tthr ||= Thread.current
|
128
|
+
ai = @active.delete(tthr)
|
129
|
+
return unless ai
|
130
|
+
#puts "<#{Time.current.to_f}-#{tthr[:current_task]}> broadcasting signal"
|
131
|
+
@signal.broadcast
|
132
|
+
@closed_signal.broadcast if @active.empty? && closed?
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module DbSucker
|
2
|
+
class Application
|
3
|
+
class Tie
|
4
|
+
def self.descendants
|
5
|
+
@descendants ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
# Descendant tracking for inherited classes.
|
9
|
+
def self.inherited(descendant)
|
10
|
+
descendants << descendant
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.hook_all! app
|
14
|
+
descendants.uniq.each do |klass|
|
15
|
+
app.debug "[AppTie] Loading apptie `#{klass.name}'"
|
16
|
+
klass.hook!(app)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.hook! app
|
21
|
+
raise NotImplementedError, "AppTies must implement class method `.hook!(app)'!"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module DbSucker
|
2
|
+
class Application
|
3
|
+
class Window
|
4
|
+
module Core
|
5
|
+
UnknownSpinnerError = Class.new(::RuntimeError)
|
6
|
+
SPINNERS = {
|
7
|
+
arrows: "←↖↑↗→↘↓↙",
|
8
|
+
blocks: "▁▃▄▅▆▇█▇▆▅▄▃",
|
9
|
+
blocks2: "▉▊▋▌▍▎▏▎▍▌▋▊▉",
|
10
|
+
blocks3: "▖▘▝▗",
|
11
|
+
blocks4: "▌▀▐▄",
|
12
|
+
forks: "┤┘┴└├┌┬┐",
|
13
|
+
triangles: "◢◣◤◥",
|
14
|
+
trbl_square: "◰◳◲◱",
|
15
|
+
trbl_circle: "◴◷◶◵",
|
16
|
+
circle_half: "◐◓◑◒",
|
17
|
+
circle_quarter: "◜◝◞◟",
|
18
|
+
circle_quarter2: "╮╯╰╭",
|
19
|
+
unix: "|/-\\",
|
20
|
+
bomb: ".oO@*",
|
21
|
+
eye: "◡⊙◠",
|
22
|
+
diamond: "◇◈◆",
|
23
|
+
}
|
24
|
+
|
25
|
+
def start
|
26
|
+
@keypad.start_loop
|
27
|
+
start_window_loop
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
stop_window_loop
|
32
|
+
@keypad.stop_loop
|
33
|
+
close_screen
|
34
|
+
app.debug "Leaving curses screen mode"
|
35
|
+
end
|
36
|
+
|
37
|
+
def start_window_loop
|
38
|
+
@loop = app.spawn_thread(:window_draw_loop) do |thr|
|
39
|
+
loop do
|
40
|
+
break if thr[:stop] && (@view == :status || @force_kill)
|
41
|
+
refresh_screen if app.opts[:window_draw]
|
42
|
+
thr.wait(app.opts[:window_refresh_delay])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def stop_window_loop
|
48
|
+
return unless @loop
|
49
|
+
@loop[:stop] = true
|
50
|
+
@loop.signal.join
|
51
|
+
end
|
52
|
+
|
53
|
+
def choose_spinner
|
54
|
+
spinner = app.opts[:window_spinner]
|
55
|
+
spinner = SPINNERS.keys.sample if spinner == :random
|
56
|
+
if s = SPINNERS[spinner]
|
57
|
+
@spinner_frames = s.split("").reverse.freeze
|
58
|
+
else
|
59
|
+
raise UnknownSpinnerError, "The spinner `#{spinner}' does not exist, use :random or one of: #{SPINNERS.keys * ", "}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def init!
|
64
|
+
app.debug "Entering curses screen mode"
|
65
|
+
init_screen
|
66
|
+
nl
|
67
|
+
if @app.opts[:window_keypad]
|
68
|
+
raw
|
69
|
+
nonl
|
70
|
+
noecho
|
71
|
+
cbreak
|
72
|
+
stdscr.keypad = true
|
73
|
+
set_cursor 0
|
74
|
+
end
|
75
|
+
|
76
|
+
# colors
|
77
|
+
start_color
|
78
|
+
use_default_colors
|
79
|
+
[:COLOR_BLACK, :COLOR_RED, :COLOR_GREEN, :COLOR_YELLOW, :COLOR_BLUE, :COLOR_MAGENTA, :COLOR_CYAN, :COLOR_WHITE].each do |cl|
|
80
|
+
c = Window.const_get(cl)
|
81
|
+
init_pair(c, c, -1)
|
82
|
+
end
|
83
|
+
init_pair(Window::COLOR_GRAY, 0, -1)
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_cursor visibility
|
87
|
+
curs_set(visibility)
|
88
|
+
end
|
89
|
+
|
90
|
+
def force_cursor line, col = 0
|
91
|
+
if line.nil?
|
92
|
+
@force_cursor = nil
|
93
|
+
else
|
94
|
+
@force_cursor = [line, col]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def update
|
99
|
+
clear
|
100
|
+
@x_offset = 0
|
101
|
+
@line = -1
|
102
|
+
yield if block_given?
|
103
|
+
next_line
|
104
|
+
setpos(*@force_cursor) if @force_cursor
|
105
|
+
refresh
|
106
|
+
end
|
107
|
+
|
108
|
+
def line l = 1
|
109
|
+
setpos(l - 1, @x_offset)
|
110
|
+
end
|
111
|
+
|
112
|
+
def next_line
|
113
|
+
@line += 1
|
114
|
+
setpos(@line, @x_offset)
|
115
|
+
end
|
116
|
+
|
117
|
+
def change_view new_view
|
118
|
+
view_was = @view
|
119
|
+
if block_given?
|
120
|
+
begin
|
121
|
+
@view = new_view
|
122
|
+
yield
|
123
|
+
ensure
|
124
|
+
@view = view_was
|
125
|
+
end
|
126
|
+
else
|
127
|
+
@view = new_view
|
128
|
+
view_was
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def progress_bar perc, opts = {}
|
133
|
+
opts = opts.reverse_merge({
|
134
|
+
width: cols - stdscr.curx - 3,
|
135
|
+
prog_open: "[",
|
136
|
+
prog_open_color: "yellow",
|
137
|
+
prog_done: "=",
|
138
|
+
prog_done_color: "green",
|
139
|
+
prog_current: ">",
|
140
|
+
prog_current_color: "yellow",
|
141
|
+
prog_remain: ".",
|
142
|
+
prog_remain_color: "gray",
|
143
|
+
prog_close: "]",
|
144
|
+
prog_close_color: "yellow",
|
145
|
+
})
|
146
|
+
pdone = (opts[:width].to_d * (perc.to_d / 100.to_d)).ceil.to_i
|
147
|
+
prem = opts[:width] - pdone
|
148
|
+
pcur = 0
|
149
|
+
if perc < 100
|
150
|
+
pdone.zero? ? (prem -= 1) : (pdone -= 1)
|
151
|
+
pcur += 1
|
152
|
+
end
|
153
|
+
|
154
|
+
send(opts[:prog_open_color], " #{opts[:prog_open]}")
|
155
|
+
send(opts[:prog_done_color], "".ljust(pdone, opts[:prog_done])) unless pdone.zero?
|
156
|
+
send(opts[:prog_current_color], "".ljust(pcur, opts[:prog_current])) unless pcur.zero?
|
157
|
+
send(opts[:prog_remain_color], "".ljust(prem, opts[:prog_remain])) unless prem.zero?
|
158
|
+
send(opts[:prog_close_color], "#{opts[:prog_close]}")
|
159
|
+
end
|
160
|
+
|
161
|
+
def dialog &block
|
162
|
+
Dialog.new(self, &block)
|
163
|
+
end
|
164
|
+
|
165
|
+
def dialog! &block
|
166
|
+
dialog(&block).render!
|
167
|
+
end
|
168
|
+
|
169
|
+
# colors
|
170
|
+
[:red, :blue, :yellow, :cyan, :magenta, :gray, :green, :white].each do |c|
|
171
|
+
define_method(c) do |*args, &block|
|
172
|
+
color = Window.const_get "COLOR_#{c.to_s.upcase}"
|
173
|
+
attron(color_pair(color)|Window::A_NORMAL) do
|
174
|
+
if block
|
175
|
+
block.call
|
176
|
+
else
|
177
|
+
args.each {|a| addstr(a) }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|