inprovise 0.2.2
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 +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
|