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.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +11 -0
  3. data/.gitignore +8 -0
  4. data/.overcommit.yml +6 -0
  5. data/.rubocop.yml +67 -0
  6. data/.rubocop_todo.yml +5 -0
  7. data/.ruby-version +1 -0
  8. data/CHANGELOG.md +2 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE.txt +674 -0
  11. data/README.md +522 -0
  12. data/Rakefile +40 -0
  13. data/bin/confctl +4 -0
  14. data/confctl.gemspec +33 -0
  15. data/example/.gitignore +2 -0
  16. data/example/README.md +38 -0
  17. data/example/cluster/cluster.nix +7 -0
  18. data/example/cluster/module-list.nix +3 -0
  19. data/example/cluster/nixos-machine/config.nix +15 -0
  20. data/example/cluster/nixos-machine/hardware.nix +4 -0
  21. data/example/cluster/nixos-machine/module.nix +8 -0
  22. data/example/cluster/vpsadminos-container/config.nix +22 -0
  23. data/example/cluster/vpsadminos-container/module.nix +8 -0
  24. data/example/cluster/vpsadminos-machine/config.nix +22 -0
  25. data/example/cluster/vpsadminos-machine/hardware.nix +4 -0
  26. data/example/cluster/vpsadminos-machine/module.nix +8 -0
  27. data/example/cluster/vpsfreecz-vps/config.nix +25 -0
  28. data/example/cluster/vpsfreecz-vps/module.nix +8 -0
  29. data/example/configs/confctl.nix +10 -0
  30. data/example/configs/swpins.nix +28 -0
  31. data/example/data/default.nix +5 -0
  32. data/example/data/ssh-keys.nix +7 -0
  33. data/example/environments/base.nix +13 -0
  34. data/example/modules/module-list.nix +13 -0
  35. data/example/shell.nix +11 -0
  36. data/example/swpins/channels/nixos-unstable.json +35 -0
  37. data/example/swpins/channels/vpsadminos-staging.json +35 -0
  38. data/lib/confctl/cli/app.rb +551 -0
  39. data/lib/confctl/cli/attr_filters.rb +51 -0
  40. data/lib/confctl/cli/cluster.rb +1248 -0
  41. data/lib/confctl/cli/command.rb +206 -0
  42. data/lib/confctl/cli/configuration.rb +296 -0
  43. data/lib/confctl/cli/gen_data.rb +97 -0
  44. data/lib/confctl/cli/generation.rb +335 -0
  45. data/lib/confctl/cli/log_view.rb +267 -0
  46. data/lib/confctl/cli/output_formatter.rb +288 -0
  47. data/lib/confctl/cli/swpins/base.rb +40 -0
  48. data/lib/confctl/cli/swpins/channel.rb +73 -0
  49. data/lib/confctl/cli/swpins/cluster.rb +80 -0
  50. data/lib/confctl/cli/swpins/core.rb +86 -0
  51. data/lib/confctl/cli/swpins/utils.rb +55 -0
  52. data/lib/confctl/cli/swpins.rb +5 -0
  53. data/lib/confctl/cli/tag_filters.rb +30 -0
  54. data/lib/confctl/cli.rb +5 -0
  55. data/lib/confctl/conf_cache.rb +105 -0
  56. data/lib/confctl/conf_dir.rb +88 -0
  57. data/lib/confctl/erb_template.rb +37 -0
  58. data/lib/confctl/exceptions.rb +3 -0
  59. data/lib/confctl/gcroot.rb +30 -0
  60. data/lib/confctl/generation/build.rb +145 -0
  61. data/lib/confctl/generation/build_list.rb +106 -0
  62. data/lib/confctl/generation/host.rb +35 -0
  63. data/lib/confctl/generation/host_list.rb +81 -0
  64. data/lib/confctl/generation/unified.rb +117 -0
  65. data/lib/confctl/generation/unified_list.rb +63 -0
  66. data/lib/confctl/git_repo_mirror.rb +79 -0
  67. data/lib/confctl/health_checks/base.rb +66 -0
  68. data/lib/confctl/health_checks/run_command.rb +179 -0
  69. data/lib/confctl/health_checks/systemd/properties.rb +84 -0
  70. data/lib/confctl/health_checks/systemd/property_check.rb +31 -0
  71. data/lib/confctl/health_checks/systemd/property_list.rb +20 -0
  72. data/lib/confctl/health_checks.rb +5 -0
  73. data/lib/confctl/hook.rb +35 -0
  74. data/lib/confctl/line_buffer.rb +53 -0
  75. data/lib/confctl/logger.rb +151 -0
  76. data/lib/confctl/machine.rb +107 -0
  77. data/lib/confctl/machine_control.rb +172 -0
  78. data/lib/confctl/machine_list.rb +108 -0
  79. data/lib/confctl/machine_status.rb +135 -0
  80. data/lib/confctl/module_options.rb +95 -0
  81. data/lib/confctl/nix.rb +382 -0
  82. data/lib/confctl/nix_build.rb +108 -0
  83. data/lib/confctl/nix_collect_garbage.rb +64 -0
  84. data/lib/confctl/nix_copy.rb +49 -0
  85. data/lib/confctl/nix_format.rb +124 -0
  86. data/lib/confctl/nix_literal_expression.rb +15 -0
  87. data/lib/confctl/parallel_executor.rb +43 -0
  88. data/lib/confctl/pattern.rb +9 -0
  89. data/lib/confctl/settings.rb +50 -0
  90. data/lib/confctl/std_line_buffer.rb +40 -0
  91. data/lib/confctl/swpins/change_set.rb +151 -0
  92. data/lib/confctl/swpins/channel.rb +62 -0
  93. data/lib/confctl/swpins/channel_list.rb +47 -0
  94. data/lib/confctl/swpins/cluster_name.rb +94 -0
  95. data/lib/confctl/swpins/cluster_name_list.rb +15 -0
  96. data/lib/confctl/swpins/core.rb +137 -0
  97. data/lib/confctl/swpins/deployed_info.rb +23 -0
  98. data/lib/confctl/swpins/spec.rb +20 -0
  99. data/lib/confctl/swpins/specs/base.rb +184 -0
  100. data/lib/confctl/swpins/specs/directory.rb +51 -0
  101. data/lib/confctl/swpins/specs/git.rb +135 -0
  102. data/lib/confctl/swpins/specs/git_rev.rb +24 -0
  103. data/lib/confctl/swpins.rb +17 -0
  104. data/lib/confctl/system_command.rb +10 -0
  105. data/lib/confctl/user_script.rb +13 -0
  106. data/lib/confctl/user_scripts.rb +41 -0
  107. data/lib/confctl/utils/file.rb +21 -0
  108. data/lib/confctl/version.rb +3 -0
  109. data/lib/confctl.rb +43 -0
  110. data/man/man8/confctl-options.nix.8 +1334 -0
  111. data/man/man8/confctl-options.nix.8.md +1340 -0
  112. data/man/man8/confctl.8 +660 -0
  113. data/man/man8/confctl.8.md +654 -0
  114. data/nix/evaluator.nix +160 -0
  115. data/nix/lib/default.nix +83 -0
  116. data/nix/lib/machine/default.nix +74 -0
  117. data/nix/lib/machine/info.nix +5 -0
  118. data/nix/lib/swpins/eval.nix +71 -0
  119. data/nix/lib/swpins/options.nix +94 -0
  120. data/nix/machines.nix +31 -0
  121. data/nix/modules/cluster/default.nix +459 -0
  122. data/nix/modules/confctl/cli.nix +21 -0
  123. data/nix/modules/confctl/generations.nix +84 -0
  124. data/nix/modules/confctl/nix.nix +28 -0
  125. data/nix/modules/confctl/swpins.nix +55 -0
  126. data/nix/modules/module-list.nix +19 -0
  127. data/shell.nix +42 -0
  128. data/template/confctl-options.nix/main.erb +45 -0
  129. data/template/confctl-options.nix/options.erb +15 -0
  130. 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