engineyard-serverside 1.6.5 → 1.7.0.pre2

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.
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