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