inprovise 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +4 -0
- data/.travis.yml +28 -0
- data/Gemfile +9 -0
- data/LICENSE +8 -0
- data/README.md +197 -0
- data/Rakefile.rb +9 -0
- data/bin/rig +5 -0
- data/inprovise.gemspec +22 -0
- data/lib/inprovise/channel/ssh.rb +202 -0
- data/lib/inprovise/cli/group.rb +86 -0
- data/lib/inprovise/cli/node.rb +95 -0
- data/lib/inprovise/cli/provision.rb +84 -0
- data/lib/inprovise/cli.rb +105 -0
- data/lib/inprovise/cmd_channel.rb +100 -0
- data/lib/inprovise/cmd_helper.rb +150 -0
- data/lib/inprovise/control.rb +326 -0
- data/lib/inprovise/execution_context.rb +277 -0
- data/lib/inprovise/group.rb +67 -0
- data/lib/inprovise/helper/cygwin.rb +43 -0
- data/lib/inprovise/helper/linux.rb +181 -0
- data/lib/inprovise/helper/windows.rb +123 -0
- data/lib/inprovise/infra.rb +122 -0
- data/lib/inprovise/local_file.rb +120 -0
- data/lib/inprovise/logger.rb +79 -0
- data/lib/inprovise/node.rb +271 -0
- data/lib/inprovise/remote_file.rb +128 -0
- data/lib/inprovise/resolver.rb +36 -0
- data/lib/inprovise/script.rb +175 -0
- data/lib/inprovise/script_index.rb +46 -0
- data/lib/inprovise/script_runner.rb +110 -0
- data/lib/inprovise/sniff.rb +46 -0
- data/lib/inprovise/sniffer/linux.rb +64 -0
- data/lib/inprovise/sniffer/platform.rb +46 -0
- data/lib/inprovise/sniffer/unknown.rb +11 -0
- data/lib/inprovise/sniffer/windows.rb +32 -0
- data/lib/inprovise/template/inprovise.rb.erb +92 -0
- data/lib/inprovise/template.rb +38 -0
- data/lib/inprovise/trigger_runner.rb +36 -0
- data/lib/inprovise/version.rb +10 -0
- data/lib/inprovise.rb +145 -0
- data/test/cli_test.rb +314 -0
- data/test/cli_test_helper.rb +19 -0
- data/test/dsl_test.rb +43 -0
- data/test/fixtures/example.txt +1 -0
- data/test/fixtures/include.rb +4 -0
- data/test/fixtures/inprovise.rb +1 -0
- data/test/fixtures/myscheme.rb +1 -0
- data/test/infra_test.rb +189 -0
- data/test/local_file_test.rb +64 -0
- data/test/remote_file_test.rb +106 -0
- data/test/resolver_test.rb +66 -0
- data/test/script_index_test.rb +53 -0
- data/test/script_test.rb +56 -0
- data/test/test_helper.rb +237 -0
- metadata +182 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
# Linux Command helper for Inprovise
|
2
|
+
#
|
3
|
+
# Author:: Martin Corino
|
4
|
+
# License:: Distributes under the same license as Ruby
|
5
|
+
|
6
|
+
Inprovise::CmdHelper.define('linux') do
|
7
|
+
|
8
|
+
def initialize(channel, sudo=false)
|
9
|
+
super(channel)
|
10
|
+
@exec = sudo ? :sudo_run : :plain_run
|
11
|
+
@cwd = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# platform properties
|
15
|
+
|
16
|
+
def admin_user
|
17
|
+
'root'
|
18
|
+
end
|
19
|
+
|
20
|
+
def env_reference(varname)
|
21
|
+
"\$#{varname}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def cwd
|
25
|
+
@cwd || plain_run('pwd').chomp
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_cwd(path)
|
29
|
+
old_cwd = @cwd
|
30
|
+
@cwd = path ? File.expand_path(path, self.cwd) : path
|
31
|
+
old_cwd
|
32
|
+
end
|
33
|
+
|
34
|
+
# generic command execution
|
35
|
+
|
36
|
+
def run(cmd, forcelog=false)
|
37
|
+
cmd = "cd #{cwd}; #{cmd}" if @cwd
|
38
|
+
exec(cmd, forcelog)
|
39
|
+
end
|
40
|
+
|
41
|
+
def sudo
|
42
|
+
return self if @exec == :sudo_run
|
43
|
+
@sudo ||= self.class.new(@channel, true)
|
44
|
+
end
|
45
|
+
|
46
|
+
# file management
|
47
|
+
|
48
|
+
def upload(from, to)
|
49
|
+
@channel.upload(real_path(from), real_path(to))
|
50
|
+
end
|
51
|
+
|
52
|
+
def download(from, to)
|
53
|
+
@channel.download(real_path(from), real_path(to))
|
54
|
+
end
|
55
|
+
|
56
|
+
# basic commands
|
57
|
+
|
58
|
+
def echo(arg)
|
59
|
+
exec("echo #{arg}")
|
60
|
+
end
|
61
|
+
|
62
|
+
def cat(path)
|
63
|
+
path = real_path(path)
|
64
|
+
begin
|
65
|
+
@channel.content(path)
|
66
|
+
rescue
|
67
|
+
exec("cat #{path}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def hash_for(path)
|
72
|
+
path = real_path(path)
|
73
|
+
exec("sha1sum #{path}")[0...40]
|
74
|
+
end
|
75
|
+
|
76
|
+
def mkdir(path)
|
77
|
+
path = real_path(path)
|
78
|
+
exec("mkdir -p #{path}")
|
79
|
+
end
|
80
|
+
|
81
|
+
def exists?(path)
|
82
|
+
path = real_path(path)
|
83
|
+
begin
|
84
|
+
@channel.exists?(path)
|
85
|
+
rescue
|
86
|
+
exec(%{if [ -f #{path} ]; then echo "true"; else echo "false"; fi}).strip == 'true'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def file?(path)
|
91
|
+
path = real_path(path)
|
92
|
+
begin
|
93
|
+
@channel.file?(path)
|
94
|
+
rescue
|
95
|
+
(exec("stat --format=%f #{path}").chomp.hex & 0x8000) == 0x8000
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def directory?(path)
|
100
|
+
path = real_path(path)
|
101
|
+
begin
|
102
|
+
@channel.file?(path)
|
103
|
+
rescue
|
104
|
+
(exec("stat --format=%f #{path}").chomp.hex & 0x4000) == 0x4000
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def copy(from, to)
|
109
|
+
exec("cp #{real_path(from)} #{real_path(to)}")
|
110
|
+
end
|
111
|
+
|
112
|
+
def delete(path)
|
113
|
+
path = real_path(path)
|
114
|
+
begin
|
115
|
+
@channel.delete(path)
|
116
|
+
rescue
|
117
|
+
exec("rm #{path}")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def permissions(path)
|
122
|
+
path = real_path(path)
|
123
|
+
begin
|
124
|
+
@channel.permissions(path)
|
125
|
+
rescue
|
126
|
+
exec("stat --format=%a #{path}").strip.to_i(8)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def set_permissions(path, perm)
|
131
|
+
path = real_path(path)
|
132
|
+
begin
|
133
|
+
@channel.set_permissions(path, perm)
|
134
|
+
rescue
|
135
|
+
exec("chmod -R #{sprintf("%o",perm)} #{path}")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def owner(path)
|
140
|
+
path = real_path(path)
|
141
|
+
begin
|
142
|
+
@channel.owner(path)
|
143
|
+
rescue
|
144
|
+
user, group = exec("stat --format=%U:%G #{path}").chomp.split(":")
|
145
|
+
{:user => user, :group => group}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def set_owner(path, user, group=nil)
|
150
|
+
path = real_path(path)
|
151
|
+
begin
|
152
|
+
@channel.set_owner(path, user, group)
|
153
|
+
rescue
|
154
|
+
exec(%{chown -R #{user}#{group ? ":#{group}" : ''} #{path}})
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def binary_exists?(bin)
|
159
|
+
exec("which #{bin}") =~ /\/#{bin}/ ? true : false
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def real_path(path)
|
165
|
+
return File.expand_path(path, @cwd) if @cwd
|
166
|
+
path
|
167
|
+
end
|
168
|
+
|
169
|
+
def exec(cmd, forcelog=false)
|
170
|
+
send(@exec, cmd, forcelog)
|
171
|
+
end
|
172
|
+
|
173
|
+
def plain_run(cmd, forcelog=false)
|
174
|
+
@channel.run(cmd, forcelog)
|
175
|
+
end
|
176
|
+
|
177
|
+
def sudo_run(cmd, forcelog=false)
|
178
|
+
@channel.run("sudo #{cmd}", forcelog)
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# Windows Command helper for Inprovise
|
2
|
+
#
|
3
|
+
# Author:: Martin Corino
|
4
|
+
# License:: Distributes under the same license as Ruby
|
5
|
+
|
6
|
+
require 'digest/sha1'
|
7
|
+
|
8
|
+
Inprovise::CmdHelper.define('windows') do
|
9
|
+
|
10
|
+
# platform properties
|
11
|
+
|
12
|
+
def admin_user
|
13
|
+
'administrator'
|
14
|
+
end
|
15
|
+
|
16
|
+
def env_reference(varname)
|
17
|
+
"%#{varname}%"
|
18
|
+
end
|
19
|
+
|
20
|
+
# generic command runution
|
21
|
+
|
22
|
+
def sudo
|
23
|
+
# not implemented yet
|
24
|
+
return self
|
25
|
+
end
|
26
|
+
|
27
|
+
# basic commands
|
28
|
+
|
29
|
+
def echo(arg)
|
30
|
+
run("echo #{arg}")
|
31
|
+
end
|
32
|
+
|
33
|
+
def cat(path)
|
34
|
+
begin
|
35
|
+
@channel.content(path)
|
36
|
+
rescue
|
37
|
+
run("type #{path}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def hash_for(path)
|
42
|
+
Digest::SHA1.hexdigest(cat(path))
|
43
|
+
end
|
44
|
+
|
45
|
+
def mkdir(path)
|
46
|
+
run("mkdir #{path}") # assumes CMD extensions are enabled
|
47
|
+
end
|
48
|
+
|
49
|
+
def exists?(path)
|
50
|
+
begin
|
51
|
+
@channel.exists?(path)
|
52
|
+
rescue
|
53
|
+
run(%{if exist #{path} ] (echo true) else (echo false)}).strip == 'true'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def file?(path)
|
58
|
+
begin
|
59
|
+
@channel.file?(path)
|
60
|
+
rescue
|
61
|
+
!run("for %p in (#{path}) do echo %~ap-").chomp.start_with?('d')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def directory?(path)
|
66
|
+
begin
|
67
|
+
@channel.file?(path)
|
68
|
+
rescue
|
69
|
+
run("for %p in (#{path}) do echo %~ap-").chomp.start_with?('d')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def copy(from, to)
|
74
|
+
run("copy #{from} #{to}")
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete(path)
|
78
|
+
begin
|
79
|
+
@channel.delete(path)
|
80
|
+
rescue
|
81
|
+
run("del #{path}")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def permissions(path)
|
86
|
+
begin
|
87
|
+
@channel.permissions(path)
|
88
|
+
rescue
|
89
|
+
# not implemented yet
|
90
|
+
0
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_permissions(path, perm)
|
95
|
+
begin
|
96
|
+
@channel.set_permissions(path, perm)
|
97
|
+
rescue
|
98
|
+
# not implemented yet
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def owner(path)
|
103
|
+
begin
|
104
|
+
@channel.owner(path)
|
105
|
+
rescue
|
106
|
+
# not implemented yet
|
107
|
+
{:user => nil, :group => nil}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def set_owner(path, user, group=nil)
|
112
|
+
begin
|
113
|
+
@channel.set_owner(path, user, group)
|
114
|
+
rescue
|
115
|
+
# not implemented yet
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def binary_exists?(bin)
|
120
|
+
run(%{for %p in (#{bin}) do (if exist "%~$PATH:p" echo %~$PATH:p)}).chomp =~ /#{bin}/ ? true : false
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Infrastructure basics for Inprovise
|
2
|
+
#
|
3
|
+
# Author:: Martin Corino
|
4
|
+
# License:: Distributes under the same license as Ruby
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
require 'monitor'
|
8
|
+
|
9
|
+
module Inprovise::Infrastructure
|
10
|
+
|
11
|
+
# setup JSON parameters
|
12
|
+
JSON.load_default_options[:symbolize_names] = true
|
13
|
+
JSON.create_id = :json_class
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def targets
|
17
|
+
@targets ||= Hash.new.extend(MonitorMixin)
|
18
|
+
end
|
19
|
+
private :targets
|
20
|
+
|
21
|
+
def find(name)
|
22
|
+
return name if name.is_a?(Target)
|
23
|
+
targets.synchronize do
|
24
|
+
return targets[name]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def names
|
29
|
+
targets.synchronize do
|
30
|
+
targets.keys.sort
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def list(type=Target)
|
35
|
+
targets.synchronize do
|
36
|
+
targets.values.select {|t| type === t}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def register(tgt)
|
41
|
+
targets.synchronize do
|
42
|
+
raise ArgumentError, "Existing [#{targets[tgt.name].to_s}] found" if targets.has_key?(tgt.name)
|
43
|
+
targets[tgt.name] = tgt
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def deregister(tgt)
|
48
|
+
targets.synchronize do
|
49
|
+
raise ArgumentError, "Invalid infrastructure target [#{tgt.to_s}]" unless targets.delete(Target === tgt ? tgt.name : tgt.to_s)
|
50
|
+
targets.each_value {|t| t.remove_target(tgt) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def save
|
55
|
+
targets.synchronize do
|
56
|
+
data = []
|
57
|
+
targets.each_value {|t| t.is_a?(Node) ? data.insert(0,t) : data.push(t) }
|
58
|
+
File.open(Inprovise.infra, 'w') {|f| f << JSON.pretty_generate(data) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def init(path)
|
63
|
+
File.open(path, 'w') {|f| f << JSON.pretty_generate([]) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def load
|
67
|
+
targets.synchronize do
|
68
|
+
JSON.load(IO.read(Inprovise.infra)) if File.readable?(Inprovise.infra)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Target
|
74
|
+
attr_reader :name, :config
|
75
|
+
|
76
|
+
def initialize(name, config = {})
|
77
|
+
@name = name
|
78
|
+
@config = config
|
79
|
+
Inprovise::Infrastructure.register(self)
|
80
|
+
end
|
81
|
+
|
82
|
+
def get(option)
|
83
|
+
config[option]
|
84
|
+
end
|
85
|
+
|
86
|
+
def set(option, value)
|
87
|
+
config[option.to_sym] = value
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_to(grp)
|
91
|
+
grp.add_target(self)
|
92
|
+
end
|
93
|
+
|
94
|
+
def remove_from(grp)
|
95
|
+
grp.remove_target(self)
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_target(tgt)
|
99
|
+
raise RuntimeError, "Cannot add #{tgt.to_s} to #{self.to_s}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def remove_target(tgt)
|
103
|
+
# ignore
|
104
|
+
end
|
105
|
+
|
106
|
+
def includes?(tgt)
|
107
|
+
false
|
108
|
+
end
|
109
|
+
|
110
|
+
def targets
|
111
|
+
[self]
|
112
|
+
end
|
113
|
+
|
114
|
+
def targets_with_config
|
115
|
+
{self => @config.dup}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
require_relative './node.rb'
|
122
|
+
require_relative './group.rb'
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# LocalFile support for Inprovise
|
2
|
+
#
|
3
|
+
# Author:: Martin Corino
|
4
|
+
# License:: Distributes under the same license as Ruby
|
5
|
+
|
6
|
+
require 'digest/sha1'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'etc'
|
9
|
+
|
10
|
+
class Inprovise::LocalFile
|
11
|
+
attr_reader :path
|
12
|
+
|
13
|
+
def initialize(path)
|
14
|
+
@path = resolve(path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def resolve(path)
|
18
|
+
if path =~ /^\//
|
19
|
+
path
|
20
|
+
else
|
21
|
+
File.join(Inprovise.root, path)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def hash
|
26
|
+
return nil unless exists?
|
27
|
+
Digest::SHA1.file(path).hexdigest
|
28
|
+
end
|
29
|
+
|
30
|
+
def exists?
|
31
|
+
File.exists?(@path)
|
32
|
+
end
|
33
|
+
|
34
|
+
def directory?
|
35
|
+
File.directory?(path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def file?
|
39
|
+
File.file?(path)
|
40
|
+
end
|
41
|
+
|
42
|
+
def content
|
43
|
+
return File.read(@path) if exists?
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# deosnt check permissions or user. should it?
|
48
|
+
def matches?(other)
|
49
|
+
self.exists? && other.exists? && self.hash == other.hash
|
50
|
+
end
|
51
|
+
|
52
|
+
def copy_to(destination)
|
53
|
+
if destination.is_local?
|
54
|
+
duplicate(destination)
|
55
|
+
else
|
56
|
+
upload(destination)
|
57
|
+
end
|
58
|
+
destination
|
59
|
+
end
|
60
|
+
|
61
|
+
def copy_from(source)
|
62
|
+
source.copy_to(self)
|
63
|
+
end
|
64
|
+
|
65
|
+
def duplicate(destination)
|
66
|
+
FileUtils.cp(path, destination.path)
|
67
|
+
destination
|
68
|
+
end
|
69
|
+
|
70
|
+
def upload(destination)
|
71
|
+
destination = @context.remote(destination) if String === destination
|
72
|
+
if destination.is_local?
|
73
|
+
FileUtils.cp(path, destination.path)
|
74
|
+
else
|
75
|
+
destination.upload(self)
|
76
|
+
end
|
77
|
+
destination
|
78
|
+
end
|
79
|
+
|
80
|
+
def download(source)
|
81
|
+
source = @context.remote(source) if String === source
|
82
|
+
if source.is_local?
|
83
|
+
FileUtils.cp(source.path, path)
|
84
|
+
else
|
85
|
+
source.download(self)
|
86
|
+
end
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
def delete!
|
91
|
+
FileUtils.rm(path) if exists?
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
def set_permissions(mask)
|
96
|
+
FileUtils.chmod_R(mask, path)
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
def permissions
|
101
|
+
File.stat(path).mode & 0777
|
102
|
+
end
|
103
|
+
|
104
|
+
def set_owner(user, group=nil)
|
105
|
+
FileUtils.chown_R(user, group, path)
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def user
|
110
|
+
Etc.getpwuid(File.stat(path).uid).name
|
111
|
+
end
|
112
|
+
|
113
|
+
def group
|
114
|
+
Etc.getgrgid(File.stat(path).gid).name
|
115
|
+
end
|
116
|
+
|
117
|
+
def is_local?
|
118
|
+
true
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Logger for Inprovise
|
2
|
+
#
|
3
|
+
# Author:: Martin Corino
|
4
|
+
# License:: Distributes under the same license as Ruby
|
5
|
+
|
6
|
+
class Inprovise::Logger
|
7
|
+
attr_accessor :node
|
8
|
+
attr_reader :task
|
9
|
+
|
10
|
+
def initialize(node, task)
|
11
|
+
@node = node
|
12
|
+
@nl = true
|
13
|
+
set_task(task)
|
14
|
+
end
|
15
|
+
|
16
|
+
def clone_for_node(node)
|
17
|
+
copy = self.dup
|
18
|
+
copy.node = node
|
19
|
+
copy
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_task(task)
|
23
|
+
oldtask = @task
|
24
|
+
@task = task.to_s
|
25
|
+
oldtask
|
26
|
+
end
|
27
|
+
|
28
|
+
def command(msg)
|
29
|
+
say(msg, :yellow)
|
30
|
+
end
|
31
|
+
|
32
|
+
def local(cmd)
|
33
|
+
say(cmd, :bold)
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute(cmd)
|
37
|
+
say(cmd, :cyan)
|
38
|
+
end
|
39
|
+
|
40
|
+
def mock_execute(cmd)
|
41
|
+
execute(cmd)
|
42
|
+
end
|
43
|
+
|
44
|
+
def cached(cmd)
|
45
|
+
execute(cmd)
|
46
|
+
end
|
47
|
+
|
48
|
+
def remote(cmd)
|
49
|
+
say(cmd, :blue)
|
50
|
+
end
|
51
|
+
|
52
|
+
def log(msg)
|
53
|
+
say(msg)
|
54
|
+
end
|
55
|
+
|
56
|
+
def print(msg)
|
57
|
+
Thread.exclusive do
|
58
|
+
$stdout.print "#{@node.to_s} [#{@task.bold}] " if @nl
|
59
|
+
$stdout.print msg.sub("\r", "\r".to_eol << "#{@node.to_s} [#{@task.bold}] ")
|
60
|
+
end
|
61
|
+
@nl = false
|
62
|
+
end
|
63
|
+
|
64
|
+
def stdout(msg, force=false)
|
65
|
+
say(msg, :green) if force || Inprovise.verbosity>0
|
66
|
+
end
|
67
|
+
|
68
|
+
def stderr(msg, force=false)
|
69
|
+
say(msg, :red, $stderr) if force || Inprovise.verbosity>0
|
70
|
+
end
|
71
|
+
|
72
|
+
def say(msg, color=nil, stream=$stdout)
|
73
|
+
msg.to_s.split("\n").each do |line|
|
74
|
+
out = color ? line.send(color) : line
|
75
|
+
Thread.exclusive { stream.puts unless @nl; stream.puts "#{@node.to_s} [#{@task.bold}] #{out}" }
|
76
|
+
@nl = true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|