confctl 1.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/.editorconfig +11 -0
- data/.gitignore +8 -0
- data/.overcommit.yml +6 -0
- data/.rubocop.yml +67 -0
- data/.rubocop_todo.yml +5 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +674 -0
- data/README.md +522 -0
- data/Rakefile +40 -0
- data/bin/confctl +4 -0
- data/confctl.gemspec +33 -0
- data/example/.gitignore +2 -0
- data/example/README.md +38 -0
- data/example/cluster/cluster.nix +7 -0
- data/example/cluster/module-list.nix +3 -0
- data/example/cluster/nixos-machine/config.nix +15 -0
- data/example/cluster/nixos-machine/hardware.nix +4 -0
- data/example/cluster/nixos-machine/module.nix +8 -0
- data/example/cluster/vpsadminos-container/config.nix +22 -0
- data/example/cluster/vpsadminos-container/module.nix +8 -0
- data/example/cluster/vpsadminos-machine/config.nix +22 -0
- data/example/cluster/vpsadminos-machine/hardware.nix +4 -0
- data/example/cluster/vpsadminos-machine/module.nix +8 -0
- data/example/cluster/vpsfreecz-vps/config.nix +25 -0
- data/example/cluster/vpsfreecz-vps/module.nix +8 -0
- data/example/configs/confctl.nix +10 -0
- data/example/configs/swpins.nix +28 -0
- data/example/data/default.nix +5 -0
- data/example/data/ssh-keys.nix +7 -0
- data/example/environments/base.nix +13 -0
- data/example/modules/module-list.nix +13 -0
- data/example/shell.nix +11 -0
- data/example/swpins/channels/nixos-unstable.json +35 -0
- data/example/swpins/channels/vpsadminos-staging.json +35 -0
- data/lib/confctl/cli/app.rb +551 -0
- data/lib/confctl/cli/attr_filters.rb +51 -0
- data/lib/confctl/cli/cluster.rb +1248 -0
- data/lib/confctl/cli/command.rb +206 -0
- data/lib/confctl/cli/configuration.rb +296 -0
- data/lib/confctl/cli/gen_data.rb +97 -0
- data/lib/confctl/cli/generation.rb +335 -0
- data/lib/confctl/cli/log_view.rb +267 -0
- data/lib/confctl/cli/output_formatter.rb +288 -0
- data/lib/confctl/cli/swpins/base.rb +40 -0
- data/lib/confctl/cli/swpins/channel.rb +73 -0
- data/lib/confctl/cli/swpins/cluster.rb +80 -0
- data/lib/confctl/cli/swpins/core.rb +86 -0
- data/lib/confctl/cli/swpins/utils.rb +55 -0
- data/lib/confctl/cli/swpins.rb +5 -0
- data/lib/confctl/cli/tag_filters.rb +30 -0
- data/lib/confctl/cli.rb +5 -0
- data/lib/confctl/conf_cache.rb +105 -0
- data/lib/confctl/conf_dir.rb +88 -0
- data/lib/confctl/erb_template.rb +37 -0
- data/lib/confctl/exceptions.rb +3 -0
- data/lib/confctl/gcroot.rb +30 -0
- data/lib/confctl/generation/build.rb +145 -0
- data/lib/confctl/generation/build_list.rb +106 -0
- data/lib/confctl/generation/host.rb +35 -0
- data/lib/confctl/generation/host_list.rb +81 -0
- data/lib/confctl/generation/unified.rb +117 -0
- data/lib/confctl/generation/unified_list.rb +63 -0
- data/lib/confctl/git_repo_mirror.rb +79 -0
- data/lib/confctl/health_checks/base.rb +66 -0
- data/lib/confctl/health_checks/run_command.rb +179 -0
- data/lib/confctl/health_checks/systemd/properties.rb +84 -0
- data/lib/confctl/health_checks/systemd/property_check.rb +31 -0
- data/lib/confctl/health_checks/systemd/property_list.rb +20 -0
- data/lib/confctl/health_checks.rb +5 -0
- data/lib/confctl/hook.rb +35 -0
- data/lib/confctl/line_buffer.rb +53 -0
- data/lib/confctl/logger.rb +151 -0
- data/lib/confctl/machine.rb +107 -0
- data/lib/confctl/machine_control.rb +172 -0
- data/lib/confctl/machine_list.rb +108 -0
- data/lib/confctl/machine_status.rb +135 -0
- data/lib/confctl/module_options.rb +95 -0
- data/lib/confctl/nix.rb +382 -0
- data/lib/confctl/nix_build.rb +108 -0
- data/lib/confctl/nix_collect_garbage.rb +64 -0
- data/lib/confctl/nix_copy.rb +49 -0
- data/lib/confctl/nix_format.rb +124 -0
- data/lib/confctl/nix_literal_expression.rb +15 -0
- data/lib/confctl/parallel_executor.rb +43 -0
- data/lib/confctl/pattern.rb +9 -0
- data/lib/confctl/settings.rb +50 -0
- data/lib/confctl/std_line_buffer.rb +40 -0
- data/lib/confctl/swpins/change_set.rb +151 -0
- data/lib/confctl/swpins/channel.rb +62 -0
- data/lib/confctl/swpins/channel_list.rb +47 -0
- data/lib/confctl/swpins/cluster_name.rb +94 -0
- data/lib/confctl/swpins/cluster_name_list.rb +15 -0
- data/lib/confctl/swpins/core.rb +137 -0
- data/lib/confctl/swpins/deployed_info.rb +23 -0
- data/lib/confctl/swpins/spec.rb +20 -0
- data/lib/confctl/swpins/specs/base.rb +184 -0
- data/lib/confctl/swpins/specs/directory.rb +51 -0
- data/lib/confctl/swpins/specs/git.rb +135 -0
- data/lib/confctl/swpins/specs/git_rev.rb +24 -0
- data/lib/confctl/swpins.rb +17 -0
- data/lib/confctl/system_command.rb +10 -0
- data/lib/confctl/user_script.rb +13 -0
- data/lib/confctl/user_scripts.rb +41 -0
- data/lib/confctl/utils/file.rb +21 -0
- data/lib/confctl/version.rb +3 -0
- data/lib/confctl.rb +43 -0
- data/man/man8/confctl-options.nix.8 +1334 -0
- data/man/man8/confctl-options.nix.8.md +1340 -0
- data/man/man8/confctl.8 +660 -0
- data/man/man8/confctl.8.md +654 -0
- data/nix/evaluator.nix +160 -0
- data/nix/lib/default.nix +83 -0
- data/nix/lib/machine/default.nix +74 -0
- data/nix/lib/machine/info.nix +5 -0
- data/nix/lib/swpins/eval.nix +71 -0
- data/nix/lib/swpins/options.nix +94 -0
- data/nix/machines.nix +31 -0
- data/nix/modules/cluster/default.nix +459 -0
- data/nix/modules/confctl/cli.nix +21 -0
- data/nix/modules/confctl/generations.nix +84 -0
- data/nix/modules/confctl/nix.nix +28 -0
- data/nix/modules/confctl/swpins.nix +55 -0
- data/nix/modules/module-list.nix +19 -0
- data/shell.nix +42 -0
- data/template/confctl-options.nix/main.erb +45 -0
- data/template/confctl-options.nix/options.erb +15 -0
- metadata +353 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
3
|
+
module ConfCtl::Cli
|
|
4
|
+
class Generation < Command
|
|
5
|
+
def list
|
|
6
|
+
machines = select_machines(args[0])
|
|
7
|
+
gens = select_generations(machines, args[1])
|
|
8
|
+
list_generations(gens)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def remove
|
|
12
|
+
machines = select_machines(args[0])
|
|
13
|
+
gens = select_generations(machines, args[1])
|
|
14
|
+
|
|
15
|
+
if gens.empty?
|
|
16
|
+
puts 'No generations found'
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
ask_confirmation! do
|
|
21
|
+
puts 'The following generations will be removed:'
|
|
22
|
+
list_generations(gens)
|
|
23
|
+
puts
|
|
24
|
+
puts "Garbage collection: #{opts[:remote] && opts[:gc] ? 'yes' : 'no'}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
gens.each do |gen|
|
|
28
|
+
puts "Removing #{gen.presence_str} generation #{gen.host}@#{gen.name}"
|
|
29
|
+
gen.destroy
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
return unless opts[:remote] && opts[:gc]
|
|
33
|
+
|
|
34
|
+
machines_gc = machines.select do |host, _machine|
|
|
35
|
+
gens.detect { |gen| gen.host == host }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
run_gc(machines_gc)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rotate
|
|
42
|
+
machines = select_machines(args[0])
|
|
43
|
+
|
|
44
|
+
to_delete = []
|
|
45
|
+
|
|
46
|
+
to_delete.concat(host_generations_rotate(machines)) if opts[:remote]
|
|
47
|
+
|
|
48
|
+
to_delete.concat(build_generations_rotate(machines)) if opts[:local] || (!opts[:local] && !opts[:remote])
|
|
49
|
+
|
|
50
|
+
if to_delete.empty?
|
|
51
|
+
puts 'No generations to delete'
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
ask_confirmation! do
|
|
56
|
+
puts 'The following generations will be removed:'
|
|
57
|
+
OutputFormatter.print(to_delete, %i[host name type id], layout: :columns)
|
|
58
|
+
puts
|
|
59
|
+
puts "Garbage collection: #{opts[:remote] ? 'when enabled in configuration' : 'no'}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
to_delete.each do |gen|
|
|
63
|
+
puts "Removing #{gen[:type]} generation #{gen[:host]}@#{gen[:name]}"
|
|
64
|
+
gen[:generation].destroy
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
return unless opts[:remote]
|
|
68
|
+
|
|
69
|
+
global = ConfCtl::Settings.instance.host_generations
|
|
70
|
+
|
|
71
|
+
machines_gc = machines.select do |_host, machine|
|
|
72
|
+
gc = machine['buildGenerations']['collectGarbage']
|
|
73
|
+
|
|
74
|
+
if gc.nil?
|
|
75
|
+
global['collectGarbage']
|
|
76
|
+
else
|
|
77
|
+
gc
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
run_gc(machines_gc) if machines_gc.any?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def collect_garbage
|
|
85
|
+
machines = select_machines(args[0])
|
|
86
|
+
|
|
87
|
+
raise 'No machines to collect garbage on' if machines.empty?
|
|
88
|
+
|
|
89
|
+
ask_confirmation! do
|
|
90
|
+
puts 'Collect garbage on the following machines:'
|
|
91
|
+
list_machines(machines)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
run_gc(machines)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
protected
|
|
98
|
+
|
|
99
|
+
def select_generations(machines, pattern)
|
|
100
|
+
gens = ConfCtl::Generation::UnifiedList.new
|
|
101
|
+
include_remote = opts[:remote]
|
|
102
|
+
include_local = opts[:local] || (!opts[:remote] && !opts[:local])
|
|
103
|
+
|
|
104
|
+
if include_remote
|
|
105
|
+
tw = ConfCtl::ParallelExecutor.new(machines.length)
|
|
106
|
+
statuses = {}
|
|
107
|
+
|
|
108
|
+
machines.each do |host, machine|
|
|
109
|
+
st = ConfCtl::MachineStatus.new(machine)
|
|
110
|
+
statuses[host] = st
|
|
111
|
+
|
|
112
|
+
tw.add do
|
|
113
|
+
st.query(toplevel: false)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
tw.run
|
|
118
|
+
|
|
119
|
+
statuses.each_value do |st|
|
|
120
|
+
gens.add_host_generations(st.generations) if st.generations
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if include_local
|
|
125
|
+
machines.each_key do |host|
|
|
126
|
+
gens.add_build_generations(ConfCtl::Generation::BuildList.new(host))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
select_old = pattern == 'old'
|
|
131
|
+
select_older_than =
|
|
132
|
+
(Time.now - (::Regexp.last_match(1).to_i * 24 * 60 * 60) if !select_old && /\A(\d+)d\Z/ =~ pattern)
|
|
133
|
+
|
|
134
|
+
if pattern
|
|
135
|
+
gens.delete_if do |gen|
|
|
136
|
+
if select_old
|
|
137
|
+
gen.current
|
|
138
|
+
elsif select_older_than
|
|
139
|
+
gen.date >= select_older_than
|
|
140
|
+
else
|
|
141
|
+
!ConfCtl::Pattern.match?(pattern, gen.name)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
gens
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def build_generations_rotate(machines)
|
|
150
|
+
global = ConfCtl::Settings.instance.build_generations
|
|
151
|
+
ret = []
|
|
152
|
+
|
|
153
|
+
machines.each do |host, machine|
|
|
154
|
+
to_delete = generations_rotate(
|
|
155
|
+
ConfCtl::Generation::BuildList.new(host),
|
|
156
|
+
min: machine['buildGenerations']['min'] || global['min'],
|
|
157
|
+
max: machine['buildGenerations']['max'] || global['max'],
|
|
158
|
+
max_age: machine['buildGenerations']['maxAge'] || global['maxAge']
|
|
159
|
+
) do |gen|
|
|
160
|
+
{
|
|
161
|
+
name: gen.name,
|
|
162
|
+
type: 'build'
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
ret.concat(to_delete)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
ret
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def host_generations_rotate(machines)
|
|
173
|
+
global = ConfCtl::Settings.instance.host_generations
|
|
174
|
+
ret = []
|
|
175
|
+
|
|
176
|
+
tw = ConfCtl::ParallelExecutor.new(machines.length)
|
|
177
|
+
statuses = {}
|
|
178
|
+
|
|
179
|
+
machines.each do |host, machine|
|
|
180
|
+
st = ConfCtl::MachineStatus.new(machine)
|
|
181
|
+
statuses[host] = st
|
|
182
|
+
|
|
183
|
+
tw.add do
|
|
184
|
+
st.query(toplevel: false)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
tw.run
|
|
189
|
+
|
|
190
|
+
statuses.each_value do |st|
|
|
191
|
+
next unless st.generations
|
|
192
|
+
|
|
193
|
+
machine = st.machine
|
|
194
|
+
|
|
195
|
+
to_delete = generations_rotate(
|
|
196
|
+
st.generations,
|
|
197
|
+
min: machine['hostGenerations']['min'] || global['min'],
|
|
198
|
+
max: machine['hostGenerations']['max'] || global['max'],
|
|
199
|
+
max_age: machine['hostGenerations']['maxAge'] || global['maxAge']
|
|
200
|
+
) do |gen|
|
|
201
|
+
{
|
|
202
|
+
type: 'host',
|
|
203
|
+
name: gen.approx_name,
|
|
204
|
+
id: gen.id
|
|
205
|
+
}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
ret.concat(to_delete)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
ret
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def generations_rotate(gens, min: nil, max: nil, max_age: nil)
|
|
215
|
+
ret = []
|
|
216
|
+
|
|
217
|
+
return ret if gens.count <= min
|
|
218
|
+
|
|
219
|
+
machine_deleted = 0
|
|
220
|
+
|
|
221
|
+
gens.each do |gen|
|
|
222
|
+
next if gen.current
|
|
223
|
+
|
|
224
|
+
if (gens.count - machine_deleted) > max || (gen.date + max_age) < Time.now
|
|
225
|
+
ret << {
|
|
226
|
+
host: gen.host,
|
|
227
|
+
generation: gen
|
|
228
|
+
}.merge(yield(gen))
|
|
229
|
+
machine_deleted += 1
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
break if gens.count - machine_deleted <= min
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
ret
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def run_gc(machines)
|
|
239
|
+
nix = ConfCtl::Nix.new
|
|
240
|
+
|
|
241
|
+
header =
|
|
242
|
+
if machines.length > 1
|
|
243
|
+
Rainbow("Collecting garbage on #{machines.length} machines").bright
|
|
244
|
+
else
|
|
245
|
+
Rainbow('Collecting garbage on ').bright + Rainbow(machines.first.to_s).yellow
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
LogView.open(
|
|
249
|
+
header: "#{header}\n",
|
|
250
|
+
title: Rainbow('Live view').bright,
|
|
251
|
+
size: :auto,
|
|
252
|
+
reserved_lines: machines.length + 8
|
|
253
|
+
) do |lw|
|
|
254
|
+
multibar = TTY::ProgressBar::Multi.new(
|
|
255
|
+
'Collecting garbage [:bar] :current',
|
|
256
|
+
width: 80
|
|
257
|
+
)
|
|
258
|
+
executor = ConfCtl::ParallelExecutor.new(opts['max-concurrent-gc'])
|
|
259
|
+
|
|
260
|
+
machines.each do |host, machine|
|
|
261
|
+
pb = multibar.register(
|
|
262
|
+
"#{host} [:bar] :current"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
executor.add do
|
|
266
|
+
end_stats = nil
|
|
267
|
+
|
|
268
|
+
ret = nix.collect_garbage(machine) do |progress|
|
|
269
|
+
lw << "#{host}> #{progress}"
|
|
270
|
+
|
|
271
|
+
if progress.path?
|
|
272
|
+
lw.sync_console do
|
|
273
|
+
pb.advance
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
elsif /^\d+ store paths deleted/ =~ progress.line
|
|
277
|
+
end_stats = progress.line
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
lw.sync_console do
|
|
282
|
+
pb.format = if ret
|
|
283
|
+
"#{host}: #{end_stats || 'done'}"
|
|
284
|
+
else
|
|
285
|
+
"#{host}: error occurred"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
pb.finish
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
ret ? nil : host
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
retvals = executor.run
|
|
296
|
+
failed = retvals.compact
|
|
297
|
+
|
|
298
|
+
raise "Gargabe collection failed on: #{failed.join(', ')}" if failed.any?
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def list_generations(gens)
|
|
303
|
+
swpin_names = []
|
|
304
|
+
|
|
305
|
+
gens.each do |gen|
|
|
306
|
+
gen.swpin_names.each do |name|
|
|
307
|
+
swpin_names << name unless swpin_names.include?(name)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
rows = gens.map do |gen|
|
|
312
|
+
row = {
|
|
313
|
+
'host' => gen.host,
|
|
314
|
+
'name' => gen.name,
|
|
315
|
+
'id' => gen.id,
|
|
316
|
+
'presence' => gen.presence_str,
|
|
317
|
+
'current' => gen.current_str
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
gen.swpin_specs.each do |name, spec|
|
|
321
|
+
row[name] = spec.version
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
row
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
OutputFormatter.print(
|
|
328
|
+
rows,
|
|
329
|
+
%w[host name id presence current] + swpin_names,
|
|
330
|
+
layout: :columns,
|
|
331
|
+
sort: %w[name host]
|
|
332
|
+
)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
require 'io/console'
|
|
2
|
+
require 'rainbow'
|
|
3
|
+
require 'tty-cursor'
|
|
4
|
+
|
|
5
|
+
module ConfCtl::Cli
|
|
6
|
+
# Create a fixed-size box showing the last `n` lines from streamed data
|
|
7
|
+
class LogView
|
|
8
|
+
# All writes to the console must go through this lock
|
|
9
|
+
CONSOLE_LOCK = Monitor.new
|
|
10
|
+
|
|
11
|
+
def self.sync_console(&)
|
|
12
|
+
CONSOLE_LOCK.synchronize(&)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Instantiate {LogView}, yield and then cleanup
|
|
16
|
+
# @yieldparam log_view [LogView]
|
|
17
|
+
def self.open(**kwargs)
|
|
18
|
+
lw = new(**kwargs)
|
|
19
|
+
lw.start
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
yield(lw)
|
|
23
|
+
ensure
|
|
24
|
+
lw.stop
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Instantiate {LogView} with feed from {ConfCtl::Logger}, yield
|
|
29
|
+
# and then cleanup
|
|
30
|
+
# @yieldparam log_view [LogView]
|
|
31
|
+
def self.open_with_logger(**kwargs)
|
|
32
|
+
lw = new(**kwargs)
|
|
33
|
+
lw.start
|
|
34
|
+
|
|
35
|
+
lb = ConfCtl::LineBuffer.new { |line| lw << line }
|
|
36
|
+
ConfCtl::Logger.instance.add_reader(lb)
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
yield(lw)
|
|
40
|
+
ensure
|
|
41
|
+
ConfCtl::Logger.instance.remove_reader(lb)
|
|
42
|
+
lw.stop
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param header [String]
|
|
47
|
+
# optional string outputted above the box, must have new lines
|
|
48
|
+
# @param title [String]
|
|
49
|
+
# optional box title
|
|
50
|
+
# @param size [Integer, :auto]
|
|
51
|
+
# number of lines to show
|
|
52
|
+
# @param reserved_lines [Integer]
|
|
53
|
+
# number of reserved lines below the box when `size` is `:auto`
|
|
54
|
+
# @param output [IO]
|
|
55
|
+
def initialize(header: nil, title: nil, size: 10, reserved_lines: 0, output: $stdout)
|
|
56
|
+
@cursor = TTY::Cursor
|
|
57
|
+
@outmutex = Mutex.new
|
|
58
|
+
@inlines = Queue.new
|
|
59
|
+
@outlines = []
|
|
60
|
+
@header = header
|
|
61
|
+
@title = title || 'Log'
|
|
62
|
+
@size = size
|
|
63
|
+
@current_size = size if size != :auto
|
|
64
|
+
@reserved_lines = reserved_lines
|
|
65
|
+
@output = output
|
|
66
|
+
@enabled = output.respond_to?(:tty?) && output.tty?
|
|
67
|
+
@resized = false
|
|
68
|
+
@stop = false
|
|
69
|
+
@generation = 0
|
|
70
|
+
@rendered = 0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def start
|
|
74
|
+
return unless enabled?
|
|
75
|
+
|
|
76
|
+
@stop = false
|
|
77
|
+
fetch_size
|
|
78
|
+
init
|
|
79
|
+
render_inplace(outlines)
|
|
80
|
+
@feeder = Thread.new { feeder_loop }
|
|
81
|
+
@renderer = Thread.new { renderer_loop }
|
|
82
|
+
|
|
83
|
+
Signal.trap('WINCH') do
|
|
84
|
+
fetch_size
|
|
85
|
+
@resized = true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def stop
|
|
90
|
+
return if @stop || !enabled?
|
|
91
|
+
|
|
92
|
+
@stop = true
|
|
93
|
+
inlines.clear
|
|
94
|
+
inlines << :stop
|
|
95
|
+
feeder.join
|
|
96
|
+
renderer.join
|
|
97
|
+
Signal.trap('WINCH', 'DEFAULT')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def flush
|
|
101
|
+
sleep(1)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def <<(line)
|
|
105
|
+
inlines << line.strip
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def sync_console(&)
|
|
109
|
+
self.class.sync_console(&)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def enabled?
|
|
113
|
+
@enabled
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
protected
|
|
117
|
+
|
|
118
|
+
attr_reader :output, :cursor, :outmutex, :inlines, :outlines, :size,
|
|
119
|
+
:current_size, :reserved_lines, :feeder, :renderer, :rows, :cols, :header, :title,
|
|
120
|
+
:generation, :rendered
|
|
121
|
+
|
|
122
|
+
def feeder_loop
|
|
123
|
+
loop do
|
|
124
|
+
line = inlines.pop
|
|
125
|
+
break if stop?
|
|
126
|
+
|
|
127
|
+
sync_outlines do
|
|
128
|
+
# TABs have variable width, there's no way to correctly determine
|
|
129
|
+
# their size, so we replace them with spaces.
|
|
130
|
+
outlines << line.gsub("\t", ' ')
|
|
131
|
+
outlines.shift while outlines.length > current_size
|
|
132
|
+
@generation += 1
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def renderer_loop
|
|
138
|
+
loop do
|
|
139
|
+
return if stop?
|
|
140
|
+
|
|
141
|
+
lines = nil
|
|
142
|
+
do_render = true
|
|
143
|
+
|
|
144
|
+
sync_outlines do
|
|
145
|
+
if generation == rendered && !resized?
|
|
146
|
+
do_render = false
|
|
147
|
+
next
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
lines = outlines.clone
|
|
151
|
+
@rendered = generation
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if do_render
|
|
155
|
+
sync_console do
|
|
156
|
+
if resized?
|
|
157
|
+
output.print(cursor.clear_screen)
|
|
158
|
+
@resized = false
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
render_scoped(lines)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
return if stop?
|
|
166
|
+
|
|
167
|
+
sleep(0.1)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def init
|
|
172
|
+
sync_console do
|
|
173
|
+
rows.times { output.puts }
|
|
174
|
+
output.print(cursor.clear_screen)
|
|
175
|
+
output.print(cursor.move_to)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def render_scoped(lines)
|
|
180
|
+
sync_console do
|
|
181
|
+
output.print(cursor.save)
|
|
182
|
+
output.print(cursor.move_to)
|
|
183
|
+
render_inplace(lines)
|
|
184
|
+
output.print(cursor.restore)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def render_inplace(lines)
|
|
189
|
+
sync_console do
|
|
190
|
+
if header
|
|
191
|
+
header.each_line do |line|
|
|
192
|
+
output.print(cursor.clear_line)
|
|
193
|
+
output.print(line)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
output.print(cursor.clear_line)
|
|
198
|
+
output.puts(title_bar(title))
|
|
199
|
+
|
|
200
|
+
current_size.times do |i|
|
|
201
|
+
output.print(cursor.clear_line)
|
|
202
|
+
|
|
203
|
+
if lines[i].nil?
|
|
204
|
+
output.puts
|
|
205
|
+
next
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
output.puts(fit_line(lines[i]))
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
output.print(cursor.clear_line)
|
|
212
|
+
output.puts("<#{'-' * (cols - 1)}")
|
|
213
|
+
output.puts
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def title_bar(s)
|
|
218
|
+
uncolored = Rainbow::StringUtils.uncolor(s)
|
|
219
|
+
|
|
220
|
+
ret = ''
|
|
221
|
+
ret << s
|
|
222
|
+
ret << ' '
|
|
223
|
+
ret << ('-' * (cols - uncolored.length - 2))
|
|
224
|
+
ret << '>'
|
|
225
|
+
ret
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def fit_line(line)
|
|
229
|
+
if line.length >= (cols - 4)
|
|
230
|
+
"#{line[0..(cols - 4)]}..."
|
|
231
|
+
else
|
|
232
|
+
line
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def fetch_size
|
|
237
|
+
@rows, @cols = IO.console.winsize
|
|
238
|
+
|
|
239
|
+
return unless size == :auto
|
|
240
|
+
|
|
241
|
+
new_size = rows
|
|
242
|
+
new_size -= header.lines.count if header
|
|
243
|
+
new_size -= reserved_lines
|
|
244
|
+
@current_size = [new_size, 10].max
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def sync_outlines(&)
|
|
248
|
+
sync_mutex(outmutex, &)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def sync_mutex(mutex, &block)
|
|
252
|
+
if mutex.owned?
|
|
253
|
+
block.call
|
|
254
|
+
else
|
|
255
|
+
mutex.synchronize(&block)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def resized?
|
|
260
|
+
@resized
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def stop?
|
|
264
|
+
@stop
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|