confctl 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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