engineyard-serverside 1.6.5 → 1.7.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. data/lib/engineyard-serverside.rb +2 -0
  2. data/lib/engineyard-serverside/cli.rb +83 -48
  3. data/lib/engineyard-serverside/configuration.rb +85 -18
  4. data/lib/engineyard-serverside/deploy.rb +105 -91
  5. data/lib/engineyard-serverside/deploy_hook.rb +22 -20
  6. data/lib/engineyard-serverside/deprecation.rb +9 -17
  7. data/lib/engineyard-serverside/future.rb +10 -4
  8. data/lib/engineyard-serverside/futures/celluloid.rb +3 -13
  9. data/lib/engineyard-serverside/futures/dataflow.rb +8 -13
  10. data/lib/engineyard-serverside/lockfile_parser.rb +1 -1
  11. data/lib/engineyard-serverside/rails_asset_support.rb +26 -10
  12. data/lib/engineyard-serverside/server.rb +17 -12
  13. data/lib/engineyard-serverside/shell.rb +98 -0
  14. data/lib/engineyard-serverside/shell/formatter.rb +71 -0
  15. data/lib/engineyard-serverside/shell/helpers.rb +29 -0
  16. data/lib/engineyard-serverside/strategies/git.rb +33 -63
  17. data/lib/engineyard-serverside/task.rb +34 -13
  18. data/lib/engineyard-serverside/version.rb +1 -1
  19. data/spec/basic_deploy_spec.rb +15 -50
  20. data/spec/bundler_deploy_spec.rb +3 -44
  21. data/spec/configuration_spec.rb +72 -0
  22. data/spec/custom_deploy_spec.rb +3 -4
  23. data/spec/deploy_hook_spec.rb +210 -162
  24. data/spec/deprecation_spec.rb +4 -26
  25. data/spec/ey_yml_customized_deploy_spec.rb +68 -0
  26. data/spec/fixtures/repos/assets_disabled/Gemfile +6 -0
  27. data/spec/fixtures/repos/assets_disabled/Gemfile.lock +90 -0
  28. data/spec/fixtures/repos/assets_disabled/README +1 -0
  29. data/spec/fixtures/repos/assets_disabled/Rakefile +5 -0
  30. data/spec/fixtures/repos/assets_disabled/config/application.rb +5 -0
  31. data/spec/fixtures/repos/assets_disabled_in_ey_yml/Gemfile +6 -0
  32. data/spec/fixtures/repos/assets_disabled_in_ey_yml/Gemfile.lock +90 -0
  33. data/spec/fixtures/repos/assets_disabled_in_ey_yml/README +1 -0
  34. data/spec/fixtures/repos/assets_disabled_in_ey_yml/Rakefile +5 -0
  35. data/spec/fixtures/repos/assets_disabled_in_ey_yml/config/application.rb +5 -0
  36. data/spec/fixtures/repos/assets_disabled_in_ey_yml/config/ey.yml +4 -0
  37. data/spec/fixtures/repos/assets_enabled/Gemfile +6 -0
  38. data/spec/fixtures/repos/assets_enabled/Gemfile.lock +90 -0
  39. data/spec/fixtures/repos/assets_enabled/README +1 -0
  40. data/spec/fixtures/repos/assets_enabled/Rakefile +5 -0
  41. data/spec/fixtures/repos/assets_enabled/config/application.rb +5 -0
  42. data/spec/fixtures/repos/assets_enabled_in_ey_yml/README +1 -0
  43. data/spec/fixtures/repos/assets_enabled_in_ey_yml/Rakefile +5 -0
  44. data/spec/fixtures/repos/assets_enabled_in_ey_yml/config/ey.yml +4 -0
  45. data/spec/fixtures/repos/assets_in_hook/Gemfile +6 -0
  46. data/spec/fixtures/repos/assets_in_hook/Gemfile.lock +90 -0
  47. data/spec/fixtures/repos/assets_in_hook/README +2 -0
  48. data/spec/fixtures/repos/assets_in_hook/Rakefile +5 -0
  49. data/spec/fixtures/repos/assets_in_hook/config/application.rb +5 -0
  50. data/spec/fixtures/repos/assets_in_hook/deploy/before_migrate.rb +1 -0
  51. data/spec/fixtures/repos/default/Gemfile +5 -0
  52. data/spec/fixtures/repos/default/Gemfile.lock +14 -0
  53. data/spec/fixtures/repos/default/README +5 -0
  54. data/spec/fixtures/repos/ey_yml/Gemfile +4 -0
  55. data/spec/fixtures/repos/ey_yml/Gemfile.lock +12 -0
  56. data/spec/fixtures/repos/ey_yml/README +1 -0
  57. data/spec/fixtures/repos/ey_yml/config/ey.yml +12 -0
  58. data/spec/fixtures/repos/ey_yml/deploy/before_migrate.rb +6 -0
  59. data/spec/fixtures/repos/ey_yml_alt/Gemfile +4 -0
  60. data/spec/fixtures/repos/ey_yml_alt/Gemfile.lock +12 -0
  61. data/spec/fixtures/repos/ey_yml_alt/README +1 -0
  62. data/spec/fixtures/repos/ey_yml_alt/deploy/before_migrate.rb +6 -0
  63. data/spec/fixtures/repos/ey_yml_alt/ey.yml +12 -0
  64. data/spec/fixtures/repos/hook_fails/README +1 -0
  65. data/spec/fixtures/repos/hook_fails/deploy/before_migrate.rb +1 -0
  66. data/spec/fixtures/repos/hooks/README +1 -0
  67. data/spec/fixtures/repos/hooks/deploy/after_bundle.rb +1 -0
  68. data/spec/fixtures/repos/hooks/deploy/after_compile_assets.rb +1 -0
  69. data/spec/fixtures/repos/hooks/deploy/after_migrate.rb +1 -0
  70. data/spec/fixtures/repos/hooks/deploy/after_restart.rb +1 -0
  71. data/spec/fixtures/repos/hooks/deploy/after_symlink.rb +1 -0
  72. data/spec/fixtures/repos/hooks/deploy/before_bundle.rb +1 -0
  73. data/spec/fixtures/repos/hooks/deploy/before_compile_assets.rb +1 -0
  74. data/spec/fixtures/repos/hooks/deploy/before_migrate.rb +1 -0
  75. data/spec/fixtures/repos/hooks/deploy/before_restart.rb +1 -0
  76. data/spec/fixtures/repos/hooks/deploy/before_symlink.rb +1 -0
  77. data/spec/fixtures/repos/no_ey_config/Gemfile +4 -0
  78. data/spec/fixtures/repos/no_ey_config/Gemfile.lock +12 -0
  79. data/spec/fixtures/repos/no_ey_config/README +1 -0
  80. data/spec/fixtures/repos/no_gemfile_lock/Gemfile +5 -0
  81. data/spec/fixtures/repos/no_gemfile_lock/README +1 -0
  82. data/spec/fixtures/repos/nodejs/README +1 -0
  83. data/spec/fixtures/repos/nodejs/package.json +7 -0
  84. data/spec/fixtures/repos/not_bundled/README +1 -0
  85. data/spec/fixtures/{gemfiles/1.0.21-rails-31-with-sqlite → repos/sqlite3/Gemfile} +0 -0
  86. data/spec/fixtures/{lockfiles/1.0.21-rails-31-with-sqlite → repos/sqlite3/Gemfile.lock} +0 -0
  87. data/spec/fixtures/repos/sqlite3/README +1 -0
  88. data/spec/git_strategy_spec.rb +11 -2
  89. data/spec/lockfile_parser_spec.rb +8 -3
  90. data/spec/nodejs_deploy_spec.rb +1 -26
  91. data/spec/rails31_deploy_spec.rb +23 -31
  92. data/spec/services_deploy_spec.rb +41 -100
  93. data/spec/shell_spec.rb +50 -0
  94. data/spec/spec_helper.rb +80 -66
  95. data/spec/sqlite3_deploy_spec.rb +10 -16
  96. data/spec/support/integration.rb +45 -139
  97. metadata +233 -78
  98. data/lib/engineyard-serverside/logged_output.rb +0 -91
  99. data/spec/logged_output_spec.rb +0 -55
@@ -1,41 +1,51 @@
1
+ require 'engineyard-serverside/shell/helpers'
2
+
1
3
  module EY
2
4
  module Serverside
3
5
  class DeployHook < Task
4
- def initialize(options)
5
- super(EY::Serverside::Deploy::Configuration.new(options))
6
- end
7
-
8
6
  def callback_context
9
- @context ||= CallbackContext.new(config)
7
+ @context ||= CallbackContext.new(config, shell)
10
8
  end
11
9
 
12
10
  def run(hook)
13
11
  hook_path = "#{c.release_path}/deploy/#{hook}.rb"
14
12
  if File.exist?(hook_path)
15
13
  Dir.chdir(c.release_path) do
16
- puts "~> running deploy hook: deploy/#{hook}.rb"
17
14
  if desc = syntax_error(hook_path)
18
15
  hook_name = File.basename(hook_path)
19
16
  abort "*** [Error] Invalid Ruby syntax in hook: #{hook_name} ***\n*** #{desc.chomp} ***"
20
17
  else
21
- callback_context.instance_eval(IO.read(hook_path))
18
+ eval_hook(IO.read(hook_path))
22
19
  end
23
20
  end
24
21
  end
25
22
  end
26
23
 
24
+ def eval_hook(code)
25
+ callback_context.instance_eval(code)
26
+ end
27
+
27
28
  def syntax_error(file)
28
29
  output = `ruby -c #{file} 2>&1`
29
30
  output unless output =~ /Syntax OK/
30
31
  end
31
32
 
32
33
  class CallbackContext
33
- def initialize(config)
34
+ include EY::Serverside::Shell::Helpers
35
+
36
+ attr_reader :shell
37
+
38
+ def initialize(config, shell)
34
39
  @configuration = config
35
40
  @configuration.set_framework_envs
41
+ @shell = shell
36
42
  @node = node
37
43
  end
38
44
 
45
+ def config
46
+ @configuration
47
+ end
48
+
39
49
  def method_missing(meth, *args, &blk)
40
50
  if @configuration.respond_to?(meth)
41
51
  @configuration.send(meth, *args, &blk)
@@ -44,24 +54,16 @@ module EY
44
54
  end
45
55
  end
46
56
 
47
- def respond_to?(meth, include_private=false)
48
- @configuration.respond_to?(meth, include_private) || super
57
+ def respond_to?(*a)
58
+ @configuration.respond_to?(*a) || super
49
59
  end
50
60
 
51
61
  def run(cmd)
52
- system(Escape.shell_command(["sh", "-l", "-c", cmd]))
62
+ shell.logged_system(Escape.shell_command(["sh", "-l", "-c", cmd])).success?
53
63
  end
54
64
 
55
65
  def sudo(cmd)
56
- system(Escape.shell_command(["sudo", "sh", "-l", "-c", cmd]))
57
- end
58
-
59
- def info(*args)
60
- $stderr.puts *args
61
- end
62
-
63
- def debug(*args)
64
- $stdout.puts *args
66
+ shell.logged_system(Escape.shell_command(["sudo", "sh", "-l", "-c", cmd])).success?
65
67
  end
66
68
 
67
69
  # convenience functions for running on certain instance types
@@ -1,26 +1,18 @@
1
+ require 'engineyard-serverside/shell/helpers'
2
+
1
3
  module EY
2
4
  module Serverside
3
5
  def self.deprecation_warning(msg)
4
6
  $stderr.puts "DEPRECATION WARNING: #{msg}"
5
7
  end
6
- end
7
8
 
8
- def self.const_missing(const)
9
- if EY::Serverside.const_defined?(const)
10
- EY::Serverside.deprecation_warning("EY::#{const} has been deprecated. use EY::Serverside::#{const} instead")
11
- EY::Serverside.class_eval(const.to_s)
12
- else
13
- super
9
+ def self.const_missing(const)
10
+ if const == :LoggedOutput
11
+ EY::Serverside.deprecation_warning("EY::Serverside::LoggedOutput has been deprecated. Use EY::Serverside::Shell::Helpers instead.")
12
+ EY::Serverside::Shell::Helpers
13
+ else
14
+ super
15
+ end
14
16
  end
15
17
  end
16
-
17
- def self.node
18
- EY::Serverside.deprecation_warning("EY.node has been deprecated. use EY::Serverside.node instead")
19
- EY::Serverside.node
20
- end
21
-
22
- def self.dna_json
23
- EY::Serverside.deprecation_warning("EY.dna_json has been deprecated. use EY::Serverside.dna_json instead")
24
- EY::Serverside.dna_json
25
- end
26
18
  end
@@ -1,18 +1,24 @@
1
1
  module EY
2
2
  module Serverside
3
3
  class Future
4
+ def self.map(blocks)
5
+ blocks.map { |block| new(&block) }
6
+ end
7
+
4
8
  def self.success?(futures)
5
9
  futures.empty? || futures.all? {|f| f.success?}
6
10
  end
7
11
 
8
- def initialize(server, *args, &block)
9
- @server = server
10
- @args = args
12
+ def initialize(&block)
11
13
  @block = block
12
14
  end
13
15
 
16
+ def result
17
+ @result ||= call
18
+ end
19
+
14
20
  def success?
15
- @value == true
21
+ result.success?
16
22
  end
17
23
 
18
24
  def error?
@@ -3,22 +3,12 @@ module EY
3
3
  $LOAD_PATH.unshift File.expand_path('../../vendor/celluloid/lib', File.dirname(__FILE__))
4
4
  require 'celluloid'
5
5
  class Future
6
- def self.call(servers, *args, &block)
7
- futures = servers.map do |server|
8
- new(server, *args, &block)
9
- end
10
-
11
- futures.each {|f| f.call}
12
- futures
13
- end
14
-
15
- def future
16
- Celluloid::Future.new(@server, *@args, &@block)
6
+ def self.call(blocks)
7
+ map(blocks).each {|f| f.result }
17
8
  end
18
9
 
19
10
  def call
20
- # Celluloid needs to call the block explicitely
21
- @value ||= future.call
11
+ Celluloid::Future.new(&@block).call
22
12
  end
23
13
  end
24
14
  end
@@ -6,25 +6,20 @@ module EY
6
6
  class Future
7
7
  extend Dataflow
8
8
 
9
- def self.call(servers, *args, &block)
10
- futures = []
11
- # Dataflow needs to call `barrier` and `need_later` in the same object
12
- barrier(*servers.map do |server|
13
- future = new(server, *args, &block)
14
- futures << future
9
+ def self.call(blocks)
10
+ futures = map(blocks)
15
11
 
16
- need_later { future.call }
17
- end)
12
+ # Dataflow needs to call `barrier` and `need_later` in the same object
13
+ need_laters = futures.map do |future|
14
+ need_later { future.result }
15
+ end
16
+ barrier(*need_laters)
18
17
 
19
18
  futures
20
19
  end
21
20
 
22
- def future
23
- @block.call(@server, *@args)
24
- end
25
-
26
21
  def call
27
- @value ||= future
22
+ @block.call
28
23
  end
29
24
  end
30
25
  end
@@ -2,7 +2,7 @@ require 'yaml'
2
2
  module EY
3
3
  module Serverside
4
4
  class LockfileParser
5
- DEFAULT = "1.0.21"
5
+ DEFAULT = "1.1.3"
6
6
 
7
7
  def self.default_version
8
8
  DEFAULT
@@ -2,44 +2,60 @@ module EY
2
2
  module Serverside
3
3
  module RailsAssetSupport
4
4
  def compile_assets
5
- asset_dir = "#{c.release_path}/app/assets"
6
5
  return unless app_needs_assets?
7
6
  rails_version = bundled_rails_version
8
7
  roles :app_master, :app, :solo do
9
8
  keep_existing_assets
10
- cmd = "cd #{c.release_path} && PATH=#{c.binstubs_path}:$PATH #{c.framework_envs} rake assets:precompile || true"
9
+ cmd = "cd #{c.release_path} && PATH=#{c.binstubs_path}:$PATH #{c.framework_envs} rake assets:precompile"
10
+
11
+ unless config.precompile_assets?
12
+ # If specifically requested, then we want to fail if compilation fails.
13
+ # If we are implicitly precompiling, we want to fail non-destructively
14
+ # because we don't know if the rake task exists or if the user
15
+ # actually intended for assets to be compiled.
16
+ cmd << %{ || (echo "Asset compilation failure ignored.\n Add 'precompile_assets: true' to ey.yml to abort deploy on failure." && true)}
17
+ end
18
+
11
19
  if rails_version
12
- info "~> Precompiling assets for rails v#{rails_version}"
20
+ shell.status "Precompiling assets for rails v#{rails_version}"
13
21
  else
14
- warning "Precompiling assets even though Rails was not bundled."
22
+ shell.warning "Precompiling assets even though Rails was not bundled."
15
23
  end
16
24
  run(cmd)
17
25
  end
18
26
  end
19
27
 
20
28
  def app_needs_assets?
29
+ if config.precompile_assets?
30
+ shell.status "Attempting Rails asset precompilation. (enabled in config)"
31
+ return true
32
+ elsif config.skip_precompile_assets?
33
+ shell.status "Skipping asset precompilation. (disabled in config)"
34
+ return false
35
+ end
36
+
21
37
  app_rb_path = File.join(c.release_path, 'config', 'application.rb')
22
38
  return unless File.readable?(app_rb_path) # Not a Rails app in the first place.
23
39
 
24
40
  if File.directory?(File.join(c.release_path, 'app', 'assets'))
25
- info "~> app/assets/ found. Attempting Rails asset pre-compilation."
41
+ shell.status "Attempting Rails asset precompilation. (found directory: 'app/assets')"
26
42
  else
27
43
  return false
28
44
  end
29
45
 
30
46
  if app_builds_own_assets?
31
- info "~> public/assets already exists, skipping pre-compilation."
47
+ shell.status "Skipping asset compilation. (found directory: 'public/assets')"
32
48
  return
33
49
  end
34
50
  if app_disables_assets?(app_rb_path)
35
- info "~> application.rb has disabled asset compilation. Skipping."
51
+ shell.status "Skipping asset compilation. (application.rb has disabled asset compilation)"
36
52
  return
37
53
  end
38
54
  # This check is very expensive, and has been deemed not worth the time.
39
55
  # Leaving this here in case someone comes up with a faster way.
40
56
  =begin
41
57
  unless app_has_asset_task?
42
- info "~> No 'assets:precompile' Rake task found. Skipping."
58
+ shell.status "No 'assets:precompile' Rake task found. Skipping."
43
59
  return
44
60
  end
45
61
  =end
@@ -62,8 +78,7 @@ module EY
62
78
  # have the same code anyway.
63
79
  task_check = "PATH=#{c.binstubs_path}:$PATH #{c.framework_envs} rake -T assets:precompile |grep 'assets:precompile'"
64
80
  cmd = "cd #{c.release_path} && #{task_check}"
65
- logged_system "cd #{c.release_path} && #{task_check}"
66
- $? == 0
81
+ shell.logged_system("cd #{c.release_path} && #{task_check}").success?
67
82
  end
68
83
 
69
84
  def app_builds_own_assets?
@@ -90,6 +105,7 @@ ln -nfs #{current} #{last_asset_path} #{c.release_path}/public
90
105
 
91
106
  def bundled_rails_version(lockfile_path = nil)
92
107
  lockfile_path ||= File.join(c.release_path, 'Gemfile.lock')
108
+ return unless File.exist?(lockfile_path)
93
109
  lockfile = File.open(lockfile_path) {|f| f.read}
94
110
  lockfile.each_line do |line|
95
111
  # scan for gemname (version) toplevel deps.
@@ -1,11 +1,8 @@
1
1
  require 'open-uri'
2
- require 'engineyard-serverside/logged_output'
3
2
 
4
3
  module EY
5
4
  module Serverside
6
5
  class Server < Struct.new(:hostname, :roles, :name, :user)
7
- include LoggedOutput
8
-
9
6
  class DuplicateHostname < StandardError
10
7
  def initialize(hostname)
11
8
  super "There is already an EY::Serverside::Server with hostname '#{hostname}'"
@@ -73,18 +70,26 @@ module EY
73
70
  hostname == 'localhost'
74
71
  end
75
72
 
76
- def sync_directory(directory)
77
- return if local?
78
- run "mkdir -p #{directory}"
79
- logged_system(%|rsync --delete -aq -e "#{ssh_command}" #{directory}/ #{user}@#{hostname}:#{directory}|)
73
+ def sync_directory_command(directory)
74
+ return nil if local?
75
+ [
76
+ remote_command("mkdir -p #{directory}"),
77
+ Escape.shell_command(%w[rsync --delete -aq -e] + [ssh_command, "#{directory}/", "#{user}@#{hostname}:#{directory}"])
78
+ ].join(' && ')
79
+ end
80
+
81
+ def command_on_server(prefix, cmd, &block)
82
+ command = block ? block.call(self, cmd.dup) : cmd
83
+ command = "#{prefix} #{Escape.shell_command([command])}"
84
+ local? ? command : remote_command(command)
80
85
  end
81
86
 
82
87
  def run(command)
83
- if local?
84
- logged_system(command)
85
- else
86
- logged_system(ssh_command + " " + Escape.shell_command(["#{user}@#{hostname}", command]))
87
- end
88
+ yield local? ? command : remote_command(command)
89
+ end
90
+
91
+ def remote_command(command)
92
+ ssh_command + Escape.shell_command(["#{user}@#{hostname}", command])
88
93
  end
89
94
 
90
95
  # Make a known hosts tempfile to absorb host fingerprints so we don't show
@@ -0,0 +1,98 @@
1
+ require 'logger'
2
+ require 'pathname'
3
+ require 'systemu'
4
+ require 'engineyard-serverside/shell/formatter'
5
+
6
+ module EY
7
+ module Serverside
8
+ class Shell
9
+ class YieldIO
10
+ def initialize(&block)
11
+ @block = block
12
+ end
13
+ def <<(str)
14
+ @block.call str
15
+ end
16
+ end
17
+
18
+ class CommandResult < Struct.new(:command, :exitstatus, :output)
19
+ def success?
20
+ exitstatus.zero?
21
+ end
22
+
23
+ def inspect
24
+ <<-EOM
25
+ $ #{command}
26
+ #{output}
27
+
28
+ # => #{exitstatus}
29
+ EOM
30
+ end
31
+ end
32
+
33
+ attr_reader :logger
34
+
35
+ def initialize(options)
36
+ @start_time = options[:start_time]
37
+ @verbose = options[:verbose]
38
+
39
+
40
+ @stdout = options[:stdout] || $stdout
41
+ @stderr = options[:stderr] || $stderr
42
+
43
+ log_pathname = Pathname.new(options[:log_path])
44
+ log_pathname.unlink if log_pathname.exist? # start fresh
45
+ @logger = Logger.new(log_pathname.to_s)
46
+ @logger.level = Logger::DEBUG # Always log to the file at debug, formatter hides debug for non-verbose
47
+ @logger.formatter = EY::Serverside::Shell::Formatter.new(@stdout, @stderr, start_time, @verbose)
48
+ end
49
+
50
+ def start_time
51
+ @start_time ||= Time.now
52
+ end
53
+
54
+ # a nice info outputter that prepends spermy operators for some reason.
55
+ def status(msg)
56
+ info msg.gsub(/^/, '~> ')
57
+ end
58
+
59
+ def substatus(msg)
60
+ debug msg.gsub(/^/, ' ~ ')
61
+ end
62
+
63
+ def fatal(msg) logger.fatal "FATAL: #{msg}" end
64
+ def error(msg) logger.error "ERROR: #{msg}" end
65
+ def warning(msg) logger.warn "WARNING: #{msg}" end
66
+ def notice(msg) logger.warn msg end
67
+ def info(msg) logger.info msg end
68
+ def debug(msg) logger.debug msg end
69
+ def unknown(msg) logger.unknown msg end
70
+
71
+ # a debug outputter that displays a command being run
72
+ # Formatis like this:
73
+ # $ cmd blah do \
74
+ # > something more
75
+ # > end
76
+ def show_command(cmd)
77
+ debug cmd.gsub(/^/, ' > ').sub(/>/, '$')
78
+ end
79
+
80
+ def logged_system(cmd)
81
+ show_command(cmd)
82
+ output = ""
83
+ outio = YieldIO.new { |msg| output << msg; debug msg.gsub(/^/,' ') }
84
+ errio = YieldIO.new { |msg| output << msg; unknown msg.gsub(/^/,' ') }
85
+ result = spawn_process(cmd, outio, errio)
86
+ CommandResult.new(cmd, result.exitstatus, output)
87
+ end
88
+
89
+ protected
90
+
91
+ # This is the meat of process spawning. It's nice to keep it separate even
92
+ # though it's simple because we've had to modify it frequently.
93
+ def spawn_process(cmd, outio, errio)
94
+ systemu cmd, 'stdout' => outio, 'stderr' => errio
95
+ end
96
+ end
97
+ end
98
+ end