kamal-backup 0.3.0 → 0.3.1

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.
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'open3'
4
4
  require 'pty'
5
- require 'securerandom'
6
5
  require 'shellwords'
7
6
  require_relative 'errors'
8
7
 
@@ -30,190 +29,6 @@ module KamalBackup
30
29
 
31
30
  CommandResult = Struct.new(:stdout, :stderr, :status, :streamed, keyword_init: true)
32
31
 
33
- class CommandOutput
34
- LEVELS = {
35
- 'DEBUG' => 0,
36
- 'INFO' => 1,
37
- 'WARN' => 2,
38
- 'ERROR' => 3,
39
- 'FATAL' => 4
40
- }.freeze
41
- LEVEL_COLORS = {
42
- 'DEBUG' => :black,
43
- 'INFO' => :blue,
44
- 'WARN' => :yellow,
45
- 'ERROR' => :red,
46
- 'FATAL' => :red
47
- }.freeze
48
- COLOR_CODES = {
49
- black: 30,
50
- red: 31,
51
- green: 32,
52
- yellow: 33,
53
- blue: 34,
54
- magenta: 35,
55
- cyan: 36,
56
- white: 37,
57
- light_black: 90,
58
- light_red: 91,
59
- light_green: 92,
60
- light_yellow: 93,
61
- light_blue: 94,
62
- light_magenta: 95,
63
- light_cyan: 96,
64
- light_white: 97
65
- }.freeze
66
-
67
- def initialize(io: $stdout, env: ENV, verbosity: :info)
68
- @io = io
69
- @env = env
70
- @verbosity = LEVELS.fetch(verbosity.to_s.upcase)
71
- @mutex = Mutex.new
72
- @buffers = {}
73
- end
74
-
75
- def info(message, redactor:)
76
- write_message('INFO', redactor.redact_string(message))
77
- end
78
-
79
- def error(message, redactor:)
80
- write_message('ERROR', colorize(redactor.redact_string(message), :red, :bold))
81
- end
82
-
83
- def decorate(value, color, mode = nil)
84
- colorize(value, color, mode)
85
- end
86
-
87
- def command_start(spec, redactor:)
88
- id = SecureRandom.hex(4)
89
- started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
90
- display = spec.display(redactor)
91
-
92
- write_message('INFO', "Running #{colorize(display, :yellow, :bold)} #{target_for(spec)}", id)
93
- write_message('DEBUG', "Command: #{colorize(display, :blue)}", id)
94
-
95
- { id: id, started_at: started_at, redactor: redactor }
96
- end
97
-
98
- def command_output(context, stream, data, redactor:)
99
- raw = data.to_s
100
- return if raw.empty?
101
- return unless log_level?('DEBUG')
102
-
103
- synchronize do
104
- key = [context.fetch(:id), stream]
105
- @buffers[key] = "#{@buffers[key]}#{raw}"
106
- flush_complete_output_lines(context, key, redactor: redactor)
107
- end
108
- end
109
-
110
- def command_exit(context, status)
111
- runtime = Process.clock_gettime(Process::CLOCK_MONOTONIC) - context.fetch(:started_at)
112
- result = status.to_i.zero? ? 'successful' : 'failed'
113
- result_color = status.to_i.zero? ? :green : :red
114
-
115
- synchronize do
116
- flush_output_buffers(context)
117
- message = "Finished in #{format('%.3f seconds', runtime)} with exit status #{status} " \
118
- "(#{colorize(result, result_color, :bold)})."
119
- write_message_unlocked('INFO', message, context.fetch(:id))
120
- end
121
- end
122
-
123
- private
124
-
125
- def write_message(level, message, id = nil)
126
- return unless log_level?(level)
127
-
128
- synchronize { write_message_unlocked(level, message, id) }
129
- end
130
-
131
- def write_message_unlocked(level, message, id = nil)
132
- @io.puts(format_message(level, message, id)) if log_level?(level)
133
- end
134
-
135
- def synchronize(&block)
136
- @mutex.synchronize(&block)
137
- end
138
-
139
- def flush_complete_output_lines(context, key, redactor:)
140
- buffer = @buffers.fetch(key)
141
- output = +''
142
-
143
- while (index = buffer.index("\n"))
144
- output << buffer.slice!(0..index)
145
- end
146
-
147
- @buffers[key] = buffer
148
- write_output(context, output, redactor: redactor, stream: key.last) unless output.empty?
149
- end
150
-
151
- def flush_output_buffers(context)
152
- id = context.fetch(:id)
153
- keys = @buffers.keys.select { |key_id, _stream| key_id == id }
154
-
155
- keys.each do |key|
156
- output = @buffers.delete(key)
157
- next if output.to_s.empty?
158
-
159
- write_output(context, output, redactor: context.fetch(:redactor), stream: key.last)
160
- end
161
- end
162
-
163
- def write_output(context, output, redactor:, stream: nil)
164
- color = stream == :stderr ? :red : :green
165
-
166
- redactor.redact_string(output).each_line do |line|
167
- write_message_unlocked('DEBUG', colorize("\t#{line}".chomp, color), context.fetch(:id))
168
- end
169
- @io.flush if @io.respond_to?(:flush)
170
- end
171
-
172
- def format_message(level, message, id = nil)
173
- message = "[#{colorize(id, :green)}] #{message}" if id
174
- "#{colorize(level.rjust(6), LEVEL_COLORS.fetch(level))} #{message}"
175
- end
176
-
177
- def local_target
178
- if (remote_host = @env['KAMAL_HOST'].to_s) && !remote_host.empty?
179
- return "on #{colorize(remote_host, :blue)}"
180
- end
181
-
182
- user = @env['USER'].to_s.empty? ? @env['USERNAME'].to_s : @env['USER'].to_s
183
-
184
- if user.empty?
185
- "on #{colorize('localhost', :blue)}"
186
- else
187
- "as #{colorize(user, :blue)}@#{colorize('localhost', :blue)}"
188
- end
189
- end
190
-
191
- def target_for(spec)
192
- if spec.host
193
- "on #{colorize(spec.host, :blue)}"
194
- else
195
- local_target
196
- end
197
- end
198
-
199
- def log_level?(level)
200
- LEVELS.fetch(level) >= @verbosity
201
- end
202
-
203
- def colorize(value, color, mode = nil)
204
- string = value.to_s
205
- return string unless colorize?
206
- return string unless COLOR_CODES.key?(color)
207
-
208
- prefix = mode == :bold ? "\e[1;" : "\e[0;"
209
- "#{prefix}#{COLOR_CODES.fetch(color)};49m#{string}\e[0m"
210
- end
211
-
212
- def colorize?
213
- @env['SSHKIT_COLOR'] || (@io.respond_to?(:tty?) && @io.tty?)
214
- end
215
- end
216
-
217
32
  class Command
218
33
  class << self
219
34
  attr_accessor :output
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module KamalBackup
6
+ class CommandOutput
7
+ LEVELS = {
8
+ 'DEBUG' => 0,
9
+ 'INFO' => 1,
10
+ 'WARN' => 2,
11
+ 'ERROR' => 3,
12
+ 'FATAL' => 4
13
+ }.freeze
14
+ LEVEL_COLORS = {
15
+ 'DEBUG' => :black,
16
+ 'INFO' => :blue,
17
+ 'WARN' => :yellow,
18
+ 'ERROR' => :red,
19
+ 'FATAL' => :red
20
+ }.freeze
21
+ COLOR_CODES = {
22
+ black: 30,
23
+ red: 31,
24
+ green: 32,
25
+ yellow: 33,
26
+ blue: 34,
27
+ magenta: 35,
28
+ cyan: 36,
29
+ white: 37,
30
+ light_black: 90,
31
+ light_red: 91,
32
+ light_green: 92,
33
+ light_yellow: 93,
34
+ light_blue: 94,
35
+ light_magenta: 95,
36
+ light_cyan: 96,
37
+ light_white: 97
38
+ }.freeze
39
+
40
+ def initialize(io: $stdout, env: ENV, verbosity: :info)
41
+ @io = io
42
+ @env = env
43
+ @verbosity = LEVELS.fetch(verbosity.to_s.upcase)
44
+ @mutex = Mutex.new
45
+ @buffers = {}
46
+ end
47
+
48
+ def info(message, redactor:)
49
+ write_message('INFO', redactor.redact_string(message))
50
+ end
51
+
52
+ def error(message, redactor:)
53
+ write_message('ERROR', colorize(redactor.redact_string(message), :red, :bold))
54
+ end
55
+
56
+ def decorate(value, color, mode = nil)
57
+ colorize(value, color, mode)
58
+ end
59
+
60
+ def command_start(spec, redactor:)
61
+ id = SecureRandom.hex(4)
62
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
63
+ display = spec.display(redactor)
64
+
65
+ write_message('INFO', "Running #{colorize(display, :yellow, :bold)} #{target_for(spec)}", id)
66
+ write_message('DEBUG', "Command: #{colorize(display, :blue)}", id)
67
+
68
+ { id: id, started_at: started_at, redactor: redactor }
69
+ end
70
+
71
+ def command_output(context, stream, data, redactor:)
72
+ raw = data.to_s
73
+ return if raw.empty?
74
+ return unless log_level?('DEBUG')
75
+
76
+ synchronize do
77
+ key = [context.fetch(:id), stream]
78
+ @buffers[key] = "#{@buffers[key]}#{raw}"
79
+ flush_complete_output_lines(context, key, redactor: redactor)
80
+ end
81
+ end
82
+
83
+ def command_exit(context, status)
84
+ runtime = Process.clock_gettime(Process::CLOCK_MONOTONIC) - context.fetch(:started_at)
85
+ result = status.to_i.zero? ? 'successful' : 'failed'
86
+ result_color = status.to_i.zero? ? :green : :red
87
+
88
+ synchronize do
89
+ flush_output_buffers(context)
90
+ message = "Finished in #{format('%.3f seconds', runtime)} with exit status #{status} " \
91
+ "(#{colorize(result, result_color, :bold)})."
92
+ write_message_unlocked('INFO', message, context.fetch(:id))
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def write_message(level, message, id = nil)
99
+ return unless log_level?(level)
100
+
101
+ synchronize { write_message_unlocked(level, message, id) }
102
+ end
103
+
104
+ def write_message_unlocked(level, message, id = nil)
105
+ @io.puts(format_message(level, message, id)) if log_level?(level)
106
+ end
107
+
108
+ def synchronize(&block)
109
+ @mutex.synchronize(&block)
110
+ end
111
+
112
+ def flush_complete_output_lines(context, key, redactor:)
113
+ buffer = @buffers.fetch(key)
114
+ output = +''
115
+
116
+ while (index = buffer.index("\n"))
117
+ output << buffer.slice!(0..index)
118
+ end
119
+
120
+ @buffers[key] = buffer
121
+ write_output(context, output, redactor: redactor, stream: key.last) unless output.empty?
122
+ end
123
+
124
+ def flush_output_buffers(context)
125
+ id = context.fetch(:id)
126
+ keys = @buffers.keys.select { |key_id, _stream| key_id == id }
127
+
128
+ keys.each do |key|
129
+ output = @buffers.delete(key)
130
+ next if output.to_s.empty?
131
+
132
+ write_output(context, output, redactor: context.fetch(:redactor), stream: key.last)
133
+ end
134
+ end
135
+
136
+ def write_output(context, output, redactor:, stream: nil)
137
+ color = stream == :stderr ? :red : :green
138
+
139
+ redactor.redact_string(output).each_line do |line|
140
+ write_message_unlocked('DEBUG', colorize("\t#{line}".chomp, color), context.fetch(:id))
141
+ end
142
+ @io.flush if @io.respond_to?(:flush)
143
+ end
144
+
145
+ def format_message(level, message, id = nil)
146
+ message = "[#{colorize(id, :green)}] #{message}" if id
147
+ "#{colorize(level.rjust(6), LEVEL_COLORS.fetch(level))} #{message}"
148
+ end
149
+
150
+ def local_target
151
+ if (remote_host = @env['KAMAL_HOST'].to_s) && !remote_host.empty?
152
+ return "on #{colorize(remote_host, :blue)}"
153
+ end
154
+
155
+ user = @env['USER'].to_s.empty? ? @env['USERNAME'].to_s : @env['USER'].to_s
156
+
157
+ if user.empty?
158
+ "on #{colorize('localhost', :blue)}"
159
+ else
160
+ "as #{colorize(user, :blue)}@#{colorize('localhost', :blue)}"
161
+ end
162
+ end
163
+
164
+ def target_for(spec)
165
+ if spec.host
166
+ "on #{colorize(spec.host, :blue)}"
167
+ else
168
+ local_target
169
+ end
170
+ end
171
+
172
+ def log_level?(level)
173
+ LEVELS.fetch(level) >= @verbosity
174
+ end
175
+
176
+ def colorize(value, color, mode = nil)
177
+ string = value.to_s
178
+ return string unless colorize?
179
+ return string unless COLOR_CODES.key?(color)
180
+
181
+ prefix = mode == :bold ? "\e[1;" : "\e[0;"
182
+ "#{prefix}#{COLOR_CODES.fetch(color)};49m#{string}\e[0m"
183
+ end
184
+
185
+ def colorize?
186
+ @env['SSHKIT_COLOR'] || (@io.respond_to?(:tty?) && @io.tty?)
187
+ end
188
+ end
189
+ end