continuent-tools-core 0.0.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.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +44 -0
- data/lib/continuent-tools-core.rb +20 -0
- data/lib/tungsten.rb +46 -0
- data/lib/tungsten/README +297 -0
- data/lib/tungsten/api.rb +553 -0
- data/lib/tungsten/common.rb +190 -0
- data/lib/tungsten/exec.rb +570 -0
- data/lib/tungsten/install.rb +332 -0
- data/lib/tungsten/properties.rb +476 -0
- data/lib/tungsten/script.rb +952 -0
- data/lib/tungsten/status.rb +177 -0
- data/lib/tungsten/util.rb +523 -0
- metadata +154 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
class TungstenStatus
|
2
|
+
include TungstenAPI
|
3
|
+
|
4
|
+
DATASERVICE = "dataservice"
|
5
|
+
COORDINATOR = "coordinator"
|
6
|
+
ROUTERS = "routers"
|
7
|
+
DATASOURCES = "datasources"
|
8
|
+
REPLICATORS = "replicators"
|
9
|
+
MANAGER = "manager"
|
10
|
+
REPLICATOR = "replicator"
|
11
|
+
MASTER = "master"
|
12
|
+
DATASERVER = "dataserver"
|
13
|
+
STATUS = "status"
|
14
|
+
HOSTNAME = "hostname"
|
15
|
+
ROLE = "role"
|
16
|
+
SEQNO = "seqno"
|
17
|
+
LATENCY = "latency"
|
18
|
+
|
19
|
+
SERVICE_TYPE = "service_type"
|
20
|
+
REPLICATION_SERVICE = :replication
|
21
|
+
PHYSICAL_SERVICE = :physical
|
22
|
+
COMPOSITE_SERVICE = :composite
|
23
|
+
|
24
|
+
def initialize(install, dataservice = nil)
|
25
|
+
@install = install
|
26
|
+
@dataservice = dataservice
|
27
|
+
@service_type = nil
|
28
|
+
@props = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse
|
32
|
+
if @props != nil
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
if @dataservice == nil
|
37
|
+
if @install.dataservices().size > 1
|
38
|
+
raise "Unable to parse trepctl because there are multiple dataservices defined for this replicator. Try specifying a specific replication service."
|
39
|
+
end
|
40
|
+
@dataservice = @install.dataservices()[0]
|
41
|
+
end
|
42
|
+
|
43
|
+
if @install.is_manager?()
|
44
|
+
unless @install.is_running?("manager")
|
45
|
+
raise "Unable to provide status for #{@dataservice} from #{@install.hostname()}:#{@install.root()} because the manager is not running"
|
46
|
+
end
|
47
|
+
parse_manager()
|
48
|
+
elsif @install.is_replicator?()
|
49
|
+
unless @install.is_running?("replicator")
|
50
|
+
raise "Unable to provide status for #{@dataservice} from #{@install.hostname()}:#{@install.root()} because the replicator is not running"
|
51
|
+
end
|
52
|
+
parse_replicator()
|
53
|
+
else
|
54
|
+
raise "Unable to provide status for #{@dataservice} from #{@install.hostname()}:#{@install.root()} because it is not a database server"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_manager
|
59
|
+
@props = Properties.new()
|
60
|
+
|
61
|
+
mgr = TungstenDataserviceManager.new(@install.mgr_api_uri())
|
62
|
+
result = mgr.get(@dataservice, 'status')
|
63
|
+
result = result["outputPayload"]["dataServiceState"]
|
64
|
+
|
65
|
+
@props.setProperty(DATASERVICE, @dataservice)
|
66
|
+
if result["composite"] == true
|
67
|
+
@props.setProperty(SERVICE_TYPE, COMPOSITE_SERVICE)
|
68
|
+
else
|
69
|
+
@props.setProperty(SERVICE_TYPE, PHYSICAL_SERVICE)
|
70
|
+
end
|
71
|
+
@props.setProperty(COORDINATOR, {
|
72
|
+
"host" => result["coordinator"],
|
73
|
+
"mode" => result["policyManagerMode"]
|
74
|
+
})
|
75
|
+
@props.setProperty(DATASOURCES, result["dataSources"])
|
76
|
+
@props.setProperty(REPLICATORS, result["replicators"])
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse_replicator
|
80
|
+
@props = Properties.new()
|
81
|
+
|
82
|
+
@props.setProperty(DATASERVICE, @dataservice)
|
83
|
+
@props.setProperty(SERVICE_TYPE, REPLICATION_SERVICE)
|
84
|
+
r_props = JSON.parse(TU.cmd_result("#{@install.trepctl(@dataservice)} status -json"))
|
85
|
+
@props.setProperty([REPLICATORS, @install.hostname()], r_props)
|
86
|
+
end
|
87
|
+
|
88
|
+
def name
|
89
|
+
self.parse()
|
90
|
+
return @props.getProperty(DATASERVICE)
|
91
|
+
end
|
92
|
+
|
93
|
+
def coordinator
|
94
|
+
self.parse()
|
95
|
+
return @props.getProperty(['coordinator','host'])
|
96
|
+
end
|
97
|
+
|
98
|
+
def policy
|
99
|
+
self.parse()
|
100
|
+
return @props.getProperty(['coordinator','mode'])
|
101
|
+
end
|
102
|
+
|
103
|
+
def datasources
|
104
|
+
self.parse()
|
105
|
+
return @props.getPropertyOr([DATASOURCES], {}).keys()
|
106
|
+
end
|
107
|
+
|
108
|
+
def replicators
|
109
|
+
self.parse()
|
110
|
+
return @props.getPropertyOr([REPLICATORS], {}).keys()
|
111
|
+
end
|
112
|
+
|
113
|
+
def datasource_role(hostname)
|
114
|
+
datasource_value(hostname, 'role')
|
115
|
+
end
|
116
|
+
|
117
|
+
def datasource_status(hostname)
|
118
|
+
datasource_value(hostname, 'state')
|
119
|
+
end
|
120
|
+
|
121
|
+
def replicator_role(hostname)
|
122
|
+
replicator_value(hostname, 'role')
|
123
|
+
end
|
124
|
+
|
125
|
+
def replicator_status(hostname)
|
126
|
+
replicator_value(hostname, 'state')
|
127
|
+
end
|
128
|
+
|
129
|
+
def replicator_latency(hostname)
|
130
|
+
replicator_value(hostname, 'appliedLatency').to_f()
|
131
|
+
end
|
132
|
+
|
133
|
+
def datasource_value(hostname, argument)
|
134
|
+
self.parse()
|
135
|
+
return @props.getProperty([DATASOURCES, hostname, argument])
|
136
|
+
end
|
137
|
+
|
138
|
+
def replicator_value(hostname, argument)
|
139
|
+
self.parse()
|
140
|
+
return @props.getProperty([REPLICATORS, hostname, argument])
|
141
|
+
end
|
142
|
+
|
143
|
+
def is_replication?
|
144
|
+
self.parse()
|
145
|
+
return @props.getProperty(SERVICE_TYPE) == REPLICATION_SERVICE
|
146
|
+
end
|
147
|
+
|
148
|
+
def is_physical?
|
149
|
+
self.parse()
|
150
|
+
return @props.getProperty(SERVICE_TYPE) == PHYSICAL_SERVICE
|
151
|
+
end
|
152
|
+
|
153
|
+
def is_composite?
|
154
|
+
self.parse()
|
155
|
+
return @props.getProperty(SERVICE_TYPE) == COMPOSITE_SERVICE
|
156
|
+
end
|
157
|
+
|
158
|
+
def to_s
|
159
|
+
self.parse()
|
160
|
+
return @props.to_s()
|
161
|
+
end
|
162
|
+
|
163
|
+
def to_hash
|
164
|
+
self.parse()
|
165
|
+
return @props.props
|
166
|
+
end
|
167
|
+
|
168
|
+
def output()
|
169
|
+
self.parse()
|
170
|
+
TU.output(self.to_s)
|
171
|
+
end
|
172
|
+
|
173
|
+
def force_output()
|
174
|
+
self.parse()
|
175
|
+
TU.force_output(self.to_s)
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,523 @@
|
|
1
|
+
class TungstenUtil
|
2
|
+
include Singleton
|
3
|
+
attr_accessor :remaining_arguments, :extra_arguments
|
4
|
+
|
5
|
+
def initialize()
|
6
|
+
super()
|
7
|
+
|
8
|
+
@logger_threshold = Logger::NOTICE
|
9
|
+
@previous_option_arguments = {}
|
10
|
+
@ssh_options = {}
|
11
|
+
@display_help = false
|
12
|
+
@num_errors = 0
|
13
|
+
@json_interface = false
|
14
|
+
@json_message_cache = []
|
15
|
+
|
16
|
+
# Create a temporary file to hold log contents
|
17
|
+
@log = Tempfile.new("tlog")
|
18
|
+
# Unlink the file so that no other process can read this log
|
19
|
+
@log.unlink()
|
20
|
+
|
21
|
+
arguments = ARGV.dup
|
22
|
+
|
23
|
+
if arguments.size() > 0
|
24
|
+
@extra_arguments = []
|
25
|
+
|
26
|
+
# This fixes an error that was coming up reading certain characters wrong
|
27
|
+
# The root cause is unknown but this allowed Ruby to parse the
|
28
|
+
# arguments properly
|
29
|
+
send_to_extra_arguments = false
|
30
|
+
arguments = arguments.map{|arg|
|
31
|
+
newarg = ''
|
32
|
+
arg.split("").each{|b|
|
33
|
+
unless b.getbyte(0)<33 || b.getbyte(0)>127 then
|
34
|
+
newarg.concat(b)
|
35
|
+
end
|
36
|
+
}
|
37
|
+
|
38
|
+
if newarg == "--"
|
39
|
+
send_to_extra_arguments = true
|
40
|
+
nil
|
41
|
+
elsif send_to_extra_arguments == true
|
42
|
+
@extra_arguments << newarg
|
43
|
+
nil
|
44
|
+
else
|
45
|
+
newarg
|
46
|
+
end
|
47
|
+
}
|
48
|
+
|
49
|
+
opts=OptionParser.new
|
50
|
+
opts.on("-i", "--info") {@logger_threshold = Logger::INFO}
|
51
|
+
opts.on("-n", "--notice") {@logger_threshold = Logger::NOTICE}
|
52
|
+
opts.on("-q", "--quiet") {@logger_threshold = Logger::WARN}
|
53
|
+
opts.on("-v", "--verbose") {@logger_threshold = Logger::DEBUG}
|
54
|
+
opts.on("--json") { @json_interface = true }
|
55
|
+
opts.on("-h", "--help") { @display_help = true }
|
56
|
+
opts.on("--net-ssh-option String") {|val|
|
57
|
+
val_parts = val.split("=")
|
58
|
+
if val_parts.length() !=2
|
59
|
+
error "Invalid value #{val} given for '--net-ssh-option'. There should be a key/value pair joined by a single =."
|
60
|
+
end
|
61
|
+
|
62
|
+
if val_parts[0] == "timeout"
|
63
|
+
val_parts[1] = val_parts[1].to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
@ssh_options[val_parts[0].to_sym] = val_parts[1]
|
67
|
+
}
|
68
|
+
|
69
|
+
log(JSON.pretty_generate(arguments))
|
70
|
+
@remaining_arguments = run_option_parser(opts, arguments)
|
71
|
+
else
|
72
|
+
@remaining_arguments = []
|
73
|
+
@extra_arguments = []
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def exit(code = 0)
|
78
|
+
if @json_interface == true
|
79
|
+
puts JSON.pretty_generate({
|
80
|
+
"rc" => code,
|
81
|
+
"messages" => @json_message_cache
|
82
|
+
})
|
83
|
+
end
|
84
|
+
|
85
|
+
Kernel.exit(code)
|
86
|
+
end
|
87
|
+
|
88
|
+
def display_help?
|
89
|
+
(@display_help == true)
|
90
|
+
end
|
91
|
+
|
92
|
+
def display_help
|
93
|
+
write_header("Global Options", nil)
|
94
|
+
output_usage_line("--directory", "Use this installed Tungsten directory as the base for all operations")
|
95
|
+
output_usage_line("--quiet, -q")
|
96
|
+
output_usage_line("--info, -i")
|
97
|
+
output_usage_line("--notice, -n")
|
98
|
+
output_usage_line("--verbose, -v")
|
99
|
+
output_usage_line("--help, -h", "Display this message")
|
100
|
+
output_usage_line("--json", "Provide return code and logging messages as a JSON object after the script finishes")
|
101
|
+
output_usage_line("--net-ssh-option=key=value", "Set the Net::SSH option for remote system calls", nil, nil, "Valid options can be found at http://net-ssh.github.com/ssh/v2/api/classes/Net/SSH.html#M000002")
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_autocomplete_arguments
|
105
|
+
[
|
106
|
+
'--directory',
|
107
|
+
'--quiet', '-q',
|
108
|
+
'--info', '-i',
|
109
|
+
'--notice', '-n',
|
110
|
+
'--verbose', '-v',
|
111
|
+
'--help', '-h',
|
112
|
+
'--json',
|
113
|
+
'--net-ssh-option='
|
114
|
+
]
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_base_path
|
118
|
+
File.expand_path("#{File.dirname(__FILE__)}/../../../..")
|
119
|
+
end
|
120
|
+
|
121
|
+
def enable_output?
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
def output(content)
|
126
|
+
write(content, nil)
|
127
|
+
end
|
128
|
+
|
129
|
+
def force_output(content)
|
130
|
+
log(content)
|
131
|
+
|
132
|
+
puts(content)
|
133
|
+
$stdout.flush()
|
134
|
+
end
|
135
|
+
|
136
|
+
def log(content = nil)
|
137
|
+
if content == nil
|
138
|
+
@log
|
139
|
+
else
|
140
|
+
@log.puts DateTime.now.to_s + " " + content
|
141
|
+
@log.flush
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def set_log_path(path)
|
146
|
+
TU.mkdir_if_absent(File.dirname(path))
|
147
|
+
old_log = @log
|
148
|
+
old_log.rewind()
|
149
|
+
|
150
|
+
@log = File.open(path, "w")
|
151
|
+
@log.puts(old_log.read())
|
152
|
+
end
|
153
|
+
|
154
|
+
def reset_errors
|
155
|
+
@num_errors = 0
|
156
|
+
end
|
157
|
+
|
158
|
+
def is_valid?
|
159
|
+
(@num_errors == 0)
|
160
|
+
end
|
161
|
+
|
162
|
+
def write(content="", level=Logger::INFO, hostname = nil, add_prefix = true)
|
163
|
+
if content.is_a?(Array)
|
164
|
+
content.each{
|
165
|
+
|c|
|
166
|
+
write(c, level, hostname, add_prefix)
|
167
|
+
}
|
168
|
+
return
|
169
|
+
end
|
170
|
+
|
171
|
+
unless content == "" || level == nil || add_prefix == false
|
172
|
+
content = "#{get_log_level_prefix(level, hostname)}#{content}"
|
173
|
+
end
|
174
|
+
|
175
|
+
# Attempt to determine the level for this message based on it's content
|
176
|
+
# If it is forwarded from another Tungsten script it will have a prefix
|
177
|
+
# so we know to use stdout or stderr
|
178
|
+
if level == nil
|
179
|
+
level = parse_log_level(content)
|
180
|
+
end
|
181
|
+
|
182
|
+
if level == Logger::ERROR
|
183
|
+
@num_errors = @num_errors + 1
|
184
|
+
end
|
185
|
+
|
186
|
+
log(content)
|
187
|
+
|
188
|
+
if enable_log_level?(level)
|
189
|
+
if @json_interface == true
|
190
|
+
@json_message_cache << content
|
191
|
+
else
|
192
|
+
if enable_output?()
|
193
|
+
if level != nil && level > Logger::NOTICE
|
194
|
+
$stdout.puts(content)
|
195
|
+
$stdout.flush()
|
196
|
+
else
|
197
|
+
$stdout.puts(content)
|
198
|
+
$stdout.flush()
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Write a header
|
206
|
+
def write_header(content, level=Logger::INFO)
|
207
|
+
write("#####################################################################", level, nil, false)
|
208
|
+
write("# #{content}", level, nil, false)
|
209
|
+
write("#####################################################################", level, nil, false)
|
210
|
+
end
|
211
|
+
|
212
|
+
def info(message, hostname = nil)
|
213
|
+
write(message, Logger::INFO, hostname)
|
214
|
+
end
|
215
|
+
|
216
|
+
def notice(message, hostname = nil)
|
217
|
+
write(message, Logger::NOTICE, hostname)
|
218
|
+
end
|
219
|
+
|
220
|
+
def warning(message, hostname = nil)
|
221
|
+
write(message, Logger::WARN, hostname)
|
222
|
+
end
|
223
|
+
|
224
|
+
def error(message, hostname = nil)
|
225
|
+
write(message, Logger::ERROR, hostname)
|
226
|
+
end
|
227
|
+
|
228
|
+
def exception(e, hostname = nil)
|
229
|
+
error(e.to_s(), hostname)
|
230
|
+
debug(e, hostname)
|
231
|
+
end
|
232
|
+
|
233
|
+
def debug(message, hostname = nil)
|
234
|
+
if message.is_a?(StandardError)
|
235
|
+
message = message.to_s() + ":\n" + message.backtrace.join("\n")
|
236
|
+
end
|
237
|
+
write(message, Logger::DEBUG, hostname)
|
238
|
+
end
|
239
|
+
|
240
|
+
def get_log_level_prefix(level=Logger::INFO, hostname = nil)
|
241
|
+
case level
|
242
|
+
when Logger::ERROR then prefix = "ERROR"
|
243
|
+
when Logger::WARN then prefix = "WARN "
|
244
|
+
when Logger::DEBUG then prefix = "DEBUG"
|
245
|
+
when Logger::NOTICE then prefix = "NOTE "
|
246
|
+
else
|
247
|
+
prefix = "INFO "
|
248
|
+
end
|
249
|
+
|
250
|
+
if hostname == nil
|
251
|
+
"#{prefix} >> "
|
252
|
+
else
|
253
|
+
"#{prefix} >> #{hostname} >> "
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def parse_log_level(line)
|
258
|
+
prefix = line[0,5]
|
259
|
+
|
260
|
+
case prefix.strip
|
261
|
+
when "ERROR" then Logger::ERROR
|
262
|
+
when "WARN" then Logger::WARN
|
263
|
+
when "DEBUG" then Logger::DEBUG
|
264
|
+
when "NOTE" then Logger::NOTICE
|
265
|
+
when "INFO" then Logger::INFO
|
266
|
+
else
|
267
|
+
nil
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Split a log line into the log level and actual message
|
272
|
+
# This is used when forwarding log messages from a remote commmand
|
273
|
+
def split_log_content(content)
|
274
|
+
level = parse_log_level(content)
|
275
|
+
if level != nil
|
276
|
+
prefix = get_log_level_prefix(level)
|
277
|
+
content = content[prefix.length, content.length]
|
278
|
+
end
|
279
|
+
|
280
|
+
return [level, content]
|
281
|
+
end
|
282
|
+
|
283
|
+
def enable_log_level?(level=Logger::INFO)
|
284
|
+
if level == nil
|
285
|
+
true
|
286
|
+
elsif level < @logger_threshold
|
287
|
+
false
|
288
|
+
else
|
289
|
+
true
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def get_log_level
|
294
|
+
@logger_threshold
|
295
|
+
end
|
296
|
+
|
297
|
+
def set_log_level(level=Logger::INFO)
|
298
|
+
@logger_threshold = level
|
299
|
+
end
|
300
|
+
|
301
|
+
def whoami
|
302
|
+
if ENV['USER']
|
303
|
+
ENV['USER']
|
304
|
+
elsif ENV['LOGNAME']
|
305
|
+
ENV['LOGNAME']
|
306
|
+
else
|
307
|
+
`whoami 2>/dev/null`.chomp
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Run the OptionParser object against @remaining_arguments or the
|
312
|
+
# {arguments} passed in. If no {arguments} value is given, the function
|
313
|
+
# will read from @remaining_arguments and update it with any remaining
|
314
|
+
# values
|
315
|
+
def run_option_parser(opts, arguments = nil, allow_invalid_options = true, invalid_option_prefix = nil)
|
316
|
+
if arguments == nil
|
317
|
+
use_remaining_arguments = true
|
318
|
+
arguments = @remaining_arguments
|
319
|
+
else
|
320
|
+
use_remaining_arguments = false
|
321
|
+
end
|
322
|
+
|
323
|
+
# Collect the list of options and remove the first two that are
|
324
|
+
# created by default
|
325
|
+
option_lists = opts.stack()
|
326
|
+
option_lists.shift()
|
327
|
+
option_lists.shift()
|
328
|
+
|
329
|
+
option_lists.each{
|
330
|
+
|ol|
|
331
|
+
|
332
|
+
ol.short.keys().each{
|
333
|
+
|arg|
|
334
|
+
if @previous_option_arguments.has_key?(arg)
|
335
|
+
error("The -#{arg} argument has already been captured")
|
336
|
+
end
|
337
|
+
@previous_option_arguments[arg] = true
|
338
|
+
}
|
339
|
+
ol.long.keys().each{
|
340
|
+
|arg|
|
341
|
+
if @previous_option_arguments.has_key?(arg)
|
342
|
+
error("The --#{arg} argument has already been captured")
|
343
|
+
end
|
344
|
+
@previous_option_arguments[arg] = true
|
345
|
+
}
|
346
|
+
}
|
347
|
+
|
348
|
+
remainder = []
|
349
|
+
while arguments.size() > 0
|
350
|
+
begin
|
351
|
+
arguments = opts.order!(arguments)
|
352
|
+
|
353
|
+
# The next argument does not have a dash so the OptionParser
|
354
|
+
# ignores it, we will add it to the stack and continue
|
355
|
+
if arguments.size() > 0 && (arguments[0] =~ /-.*/) != 0
|
356
|
+
remainder << arguments.shift()
|
357
|
+
end
|
358
|
+
rescue OptionParser::InvalidOption => io
|
359
|
+
if allow_invalid_options
|
360
|
+
# Prepend the invalid option onto the arguments array
|
361
|
+
remainder = remainder + io.recover([])
|
362
|
+
|
363
|
+
# The next argument does not have a dash so the OptionParser
|
364
|
+
# ignores it, we will add it to the stack and continue
|
365
|
+
if arguments.size() > 0 && (arguments[0] =~ /-.*/) != 0
|
366
|
+
remainder << arguments.shift()
|
367
|
+
end
|
368
|
+
else
|
369
|
+
if invalid_option_prefix != nil
|
370
|
+
io.reason = invalid_option_prefix
|
371
|
+
end
|
372
|
+
|
373
|
+
raise io
|
374
|
+
end
|
375
|
+
rescue => e
|
376
|
+
exception(e)
|
377
|
+
raise "Argument parsing failed: #{e.to_s()}"
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
if use_remaining_arguments == true
|
382
|
+
@remaining_arguments = remainder
|
383
|
+
return nil
|
384
|
+
else
|
385
|
+
return remainder
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# Returns [width, height] of terminal when detected, nil if not detected.
|
390
|
+
# Think of this as a simpler version of Highline's Highline::SystemExtensions.terminal_size()
|
391
|
+
def detect_terminal_size
|
392
|
+
unless @terminal_size
|
393
|
+
if (ENV['COLUMNS'] =~ /^\d+$/) && (ENV['LINES'] =~ /^\d+$/)
|
394
|
+
@terminal_size = [ENV['COLUMNS'].to_i, ENV['LINES'].to_i]
|
395
|
+
elsif (RUBY_PLATFORM =~ /java/ || (!STDIN.tty? && ENV['TERM'])) && command_exists?('tput')
|
396
|
+
@terminal_size = [`tput cols`.to_i, `tput lines`.to_i]
|
397
|
+
elsif STDIN.tty? && command_exists?('stty')
|
398
|
+
@terminal_size = `stty size`.scan(/\d+/).map { |s| s.to_i }.reverse
|
399
|
+
else
|
400
|
+
@terminal_size = [80, 30]
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
return @terminal_size
|
405
|
+
rescue => e
|
406
|
+
[80, 30]
|
407
|
+
end
|
408
|
+
|
409
|
+
# Display information about the argument formatting {msg} so all lines
|
410
|
+
# appear formatted correctly
|
411
|
+
def output_usage_line(argument, msg = "", default = nil, max_line = nil, additional_help = "")
|
412
|
+
if max_line == nil
|
413
|
+
max_line = detect_terminal_size()[0]-5
|
414
|
+
end
|
415
|
+
|
416
|
+
if msg.is_a?(String)
|
417
|
+
msg = msg.split("\n").join(" ")
|
418
|
+
else
|
419
|
+
msg = msg.to_s()
|
420
|
+
end
|
421
|
+
|
422
|
+
msg = msg.gsub(/^\s+/, "").gsub(/\s+$/, $/)
|
423
|
+
|
424
|
+
if default.to_s() != ""
|
425
|
+
if msg != ""
|
426
|
+
msg += " "
|
427
|
+
end
|
428
|
+
|
429
|
+
msg += "[#{default}]"
|
430
|
+
end
|
431
|
+
|
432
|
+
if argument.length > 28 || (argument.length + msg.length > max_line)
|
433
|
+
output(argument)
|
434
|
+
|
435
|
+
wrapped_lines(msg, 29).each{
|
436
|
+
|line|
|
437
|
+
output(line)
|
438
|
+
}
|
439
|
+
else
|
440
|
+
output(format("%-29s", argument) + " " + msg)
|
441
|
+
end
|
442
|
+
|
443
|
+
if additional_help.to_s != ""
|
444
|
+
additional_help = additional_help.split("\n").map!{
|
445
|
+
|line|
|
446
|
+
line.strip()
|
447
|
+
}.join(' ')
|
448
|
+
additional_help.split("<br>").each{
|
449
|
+
|line|
|
450
|
+
output_usage_line("", line, nil, max_line)
|
451
|
+
}
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
# Break {msg} into lines that have {offset} leading spaces and do
|
456
|
+
# not exceed {max_line}
|
457
|
+
def wrapped_lines(msg, offset = 0, max_line = nil)
|
458
|
+
if max_line == nil
|
459
|
+
max_line = detect_terminal_size()[0]-5
|
460
|
+
end
|
461
|
+
if offset == 0
|
462
|
+
default_line = ""
|
463
|
+
else
|
464
|
+
line_format = "%-#{offset}s"
|
465
|
+
default_line = format(line_format, " ")
|
466
|
+
end
|
467
|
+
|
468
|
+
lines = []
|
469
|
+
words = msg.split(' ')
|
470
|
+
|
471
|
+
force_add_word = true
|
472
|
+
line = default_line.dup()
|
473
|
+
while words.length() > 0
|
474
|
+
if !force_add_word && line.length() + words[0].length() > max_line
|
475
|
+
lines << line
|
476
|
+
line = default_line.dup()
|
477
|
+
force_add_word = true
|
478
|
+
else
|
479
|
+
if line == ""
|
480
|
+
line = words.shift()
|
481
|
+
else
|
482
|
+
line += " " + words.shift()
|
483
|
+
end
|
484
|
+
force_add_word = false
|
485
|
+
end
|
486
|
+
end
|
487
|
+
lines << line
|
488
|
+
|
489
|
+
return lines
|
490
|
+
end
|
491
|
+
|
492
|
+
def to_identifier(str)
|
493
|
+
str.tr('.', '_').tr('-', '_').tr('/', '_').tr('\\', '_').downcase()
|
494
|
+
end
|
495
|
+
|
496
|
+
# Create a directory if it is absent.
|
497
|
+
def mkdir_if_absent(dirname)
|
498
|
+
if dirname == nil
|
499
|
+
return
|
500
|
+
end
|
501
|
+
|
502
|
+
if File.exists?(dirname)
|
503
|
+
if File.directory?(dirname)
|
504
|
+
debug("Found directory, no need to create: #{dirname}")
|
505
|
+
else
|
506
|
+
raise "Directory already exists as a file: #{dirname}"
|
507
|
+
end
|
508
|
+
else
|
509
|
+
debug("Creating missing directory: #{dirname}")
|
510
|
+
cmd_result("mkdir -p #{dirname}")
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def pluralize(array, singular, plural = nil)
|
515
|
+
if plural == nil
|
516
|
+
singular
|
517
|
+
elsif array.size() > 1
|
518
|
+
plural
|
519
|
+
else
|
520
|
+
singular
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|