tomo 0.1.0

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 (135) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +221 -0
  4. data/exe/tomo +4 -0
  5. data/lib/tomo/cli/command.rb +36 -0
  6. data/lib/tomo/cli/common_options.rb +48 -0
  7. data/lib/tomo/cli/completions.rb +70 -0
  8. data/lib/tomo/cli/deploy_options.rb +59 -0
  9. data/lib/tomo/cli/error.rb +16 -0
  10. data/lib/tomo/cli/interrupted_error.rb +9 -0
  11. data/lib/tomo/cli/options.rb +38 -0
  12. data/lib/tomo/cli/parser.rb +92 -0
  13. data/lib/tomo/cli/project_options.rb +47 -0
  14. data/lib/tomo/cli/rules/argument.rb +42 -0
  15. data/lib/tomo/cli/rules/switch.rb +43 -0
  16. data/lib/tomo/cli/rules/value_switch.rb +58 -0
  17. data/lib/tomo/cli/rules.rb +98 -0
  18. data/lib/tomo/cli/rules_evaluator.rb +71 -0
  19. data/lib/tomo/cli/state.rb +29 -0
  20. data/lib/tomo/cli/usage.rb +42 -0
  21. data/lib/tomo/cli.rb +75 -0
  22. data/lib/tomo/colors.rb +46 -0
  23. data/lib/tomo/commands/completion_script.rb +46 -0
  24. data/lib/tomo/commands/default.rb +72 -0
  25. data/lib/tomo/commands/deploy.rb +67 -0
  26. data/lib/tomo/commands/help.rb +9 -0
  27. data/lib/tomo/commands/init.rb +92 -0
  28. data/lib/tomo/commands/run.rb +76 -0
  29. data/lib/tomo/commands/setup.rb +54 -0
  30. data/lib/tomo/commands/tasks.rb +32 -0
  31. data/lib/tomo/commands/version.rb +23 -0
  32. data/lib/tomo/commands.rb +13 -0
  33. data/lib/tomo/configuration/dsl/batch_block.rb +17 -0
  34. data/lib/tomo/configuration/dsl/config_file.rb +39 -0
  35. data/lib/tomo/configuration/dsl/environment_block.rb +13 -0
  36. data/lib/tomo/configuration/dsl/error_formatter.rb +75 -0
  37. data/lib/tomo/configuration/dsl/hosts_and_settings.rb +24 -0
  38. data/lib/tomo/configuration/dsl/tasks_block.rb +24 -0
  39. data/lib/tomo/configuration/dsl.rb +12 -0
  40. data/lib/tomo/configuration/environment.rb +12 -0
  41. data/lib/tomo/configuration/glob.rb +26 -0
  42. data/lib/tomo/configuration/plugin_file_not_found_error.rb +14 -0
  43. data/lib/tomo/configuration/plugin_resolver.rb +63 -0
  44. data/lib/tomo/configuration/plugins_registry/file_resolver.rb +43 -0
  45. data/lib/tomo/configuration/plugins_registry/gem_resolver.rb +63 -0
  46. data/lib/tomo/configuration/plugins_registry.rb +67 -0
  47. data/lib/tomo/configuration/project_not_found_error.rb +28 -0
  48. data/lib/tomo/configuration/role_based_task_filter.rb +42 -0
  49. data/lib/tomo/configuration/unknown_environment_error.rb +46 -0
  50. data/lib/tomo/configuration/unknown_plugin_error.rb +28 -0
  51. data/lib/tomo/configuration/unspecified_environment_error.rb +28 -0
  52. data/lib/tomo/configuration.rb +124 -0
  53. data/lib/tomo/console/key_reader.rb +51 -0
  54. data/lib/tomo/console/menu.rb +109 -0
  55. data/lib/tomo/console.rb +33 -0
  56. data/lib/tomo/error/suggestions.rb +44 -0
  57. data/lib/tomo/error.rb +22 -0
  58. data/lib/tomo/host.rb +57 -0
  59. data/lib/tomo/logger/tagged_io.rb +38 -0
  60. data/lib/tomo/logger.rb +70 -0
  61. data/lib/tomo/path.rb +19 -0
  62. data/lib/tomo/paths.rb +36 -0
  63. data/lib/tomo/plugin/bundler/helpers.rb +14 -0
  64. data/lib/tomo/plugin/bundler/tasks.rb +57 -0
  65. data/lib/tomo/plugin/bundler.rb +17 -0
  66. data/lib/tomo/plugin/core/helpers.rb +65 -0
  67. data/lib/tomo/plugin/core/tasks.rb +138 -0
  68. data/lib/tomo/plugin/core.rb +31 -0
  69. data/lib/tomo/plugin/env/tasks.rb +113 -0
  70. data/lib/tomo/plugin/env.rb +13 -0
  71. data/lib/tomo/plugin/git/helpers.rb +11 -0
  72. data/lib/tomo/plugin/git/tasks.rb +78 -0
  73. data/lib/tomo/plugin/git.rb +19 -0
  74. data/lib/tomo/plugin/nvm/tasks.rb +61 -0
  75. data/lib/tomo/plugin/nvm.rb +14 -0
  76. data/lib/tomo/plugin/puma/tasks.rb +38 -0
  77. data/lib/tomo/plugin/puma.rb +12 -0
  78. data/lib/tomo/plugin/rails/helpers.rb +20 -0
  79. data/lib/tomo/plugin/rails/tasks.rb +79 -0
  80. data/lib/tomo/plugin/rails.rb +11 -0
  81. data/lib/tomo/plugin/rbenv/tasks.rb +55 -0
  82. data/lib/tomo/plugin/rbenv.rb +12 -0
  83. data/lib/tomo/plugin/testing.rb +16 -0
  84. data/lib/tomo/plugin.rb +4 -0
  85. data/lib/tomo/plugin_dsl.rb +23 -0
  86. data/lib/tomo/remote.rb +55 -0
  87. data/lib/tomo/result.rb +28 -0
  88. data/lib/tomo/runtime/concurrent_ruby_load_error.rb +26 -0
  89. data/lib/tomo/runtime/concurrent_ruby_thread_pool.rb +50 -0
  90. data/lib/tomo/runtime/context.rb +21 -0
  91. data/lib/tomo/runtime/current.rb +41 -0
  92. data/lib/tomo/runtime/execution_plan.rb +107 -0
  93. data/lib/tomo/runtime/host_execution_step.rb +49 -0
  94. data/lib/tomo/runtime/inline_thread_pool.rb +27 -0
  95. data/lib/tomo/runtime/privileged_task.rb +6 -0
  96. data/lib/tomo/runtime/settings_interpolation.rb +55 -0
  97. data/lib/tomo/runtime/settings_required_error.rb +33 -0
  98. data/lib/tomo/runtime/task_aborted_error.rb +15 -0
  99. data/lib/tomo/runtime/task_runner.rb +56 -0
  100. data/lib/tomo/runtime/unknown_task_error.rb +23 -0
  101. data/lib/tomo/runtime.rb +82 -0
  102. data/lib/tomo/script.rb +44 -0
  103. data/lib/tomo/shell_builder.rb +108 -0
  104. data/lib/tomo/ssh/child_process.rb +64 -0
  105. data/lib/tomo/ssh/connection.rb +82 -0
  106. data/lib/tomo/ssh/connection_error.rb +16 -0
  107. data/lib/tomo/ssh/connection_validator.rb +87 -0
  108. data/lib/tomo/ssh/error.rb +11 -0
  109. data/lib/tomo/ssh/executable_error.rb +21 -0
  110. data/lib/tomo/ssh/options.rb +67 -0
  111. data/lib/tomo/ssh/permission_error.rb +18 -0
  112. data/lib/tomo/ssh/script_error.rb +23 -0
  113. data/lib/tomo/ssh/unknown_error.rb +13 -0
  114. data/lib/tomo/ssh/unsupported_version_error.rb +15 -0
  115. data/lib/tomo/ssh.rb +36 -0
  116. data/lib/tomo/task_library.rb +51 -0
  117. data/lib/tomo/templates/config.rb.erb +66 -0
  118. data/lib/tomo/testing/Dockerfile +10 -0
  119. data/lib/tomo/testing/connection.rb +34 -0
  120. data/lib/tomo/testing/docker_image.rb +115 -0
  121. data/lib/tomo/testing/docker_plugin_tester.rb +39 -0
  122. data/lib/tomo/testing/host_extensions.rb +27 -0
  123. data/lib/tomo/testing/local.rb +75 -0
  124. data/lib/tomo/testing/mock_plugin_tester.rb +26 -0
  125. data/lib/tomo/testing/mocked_exec_error.rb +6 -0
  126. data/lib/tomo/testing/plugin_tester.rb +49 -0
  127. data/lib/tomo/testing/remote_extensions.rb +10 -0
  128. data/lib/tomo/testing/ssh_extensions.rb +13 -0
  129. data/lib/tomo/testing/tomo_test_ed25519 +7 -0
  130. data/lib/tomo/testing/tomo_test_ed25519.pub +1 -0
  131. data/lib/tomo/testing/ubuntu_setup.sh +33 -0
  132. data/lib/tomo/testing.rb +39 -0
  133. data/lib/tomo/version.rb +3 -0
  134. data/lib/tomo.rb +45 -0
  135. metadata +308 -0
@@ -0,0 +1,78 @@
1
+ require "shellwords"
2
+ require "time"
3
+
4
+ module Tomo::Plugin::Git
5
+ class Tasks < Tomo::TaskLibrary
6
+ # rubocop:disable Metrics/AbcSize
7
+ def clone
8
+ require_setting :git_url
9
+
10
+ if remote.directory?(paths.git_repo) && !dry_run?
11
+ set_origin_url
12
+ else
13
+ remote.mkdir_p(paths.git_repo.dirname)
14
+ remote.git(
15
+ "clone", "--mirror", settings[:git_url], paths.git_repo
16
+ )
17
+ end
18
+ end
19
+
20
+ def create_release
21
+ configure_git_attributes
22
+ remote.chdir(paths.git_repo) do
23
+ remote.git("remote update --prune")
24
+ remote.mkdir_p(paths.release)
25
+ remote.git(
26
+ "archive #{branch.shellescape} | "\
27
+ "tar -x -f - -C #{paths.release.shellescape}"
28
+ )
29
+ end
30
+ store_release_info
31
+ end
32
+ # rubocop:enable Metrics/AbcSize
33
+
34
+ private
35
+
36
+ def branch
37
+ settings[:git_branch]
38
+ end
39
+
40
+ def set_origin_url
41
+ remote.chdir(paths.git_repo) do
42
+ remote.git("remote", "set-url", "origin", settings[:git_url])
43
+ end
44
+ end
45
+
46
+ def configure_git_attributes
47
+ exclusions = settings[:git_exclusions] || []
48
+ attributes = exclusions.map { |excl| "#{excl} export-ignore" }.join("\n")
49
+
50
+ remote.write(
51
+ text: attributes,
52
+ to: paths.git_repo.join("info/attributes")
53
+ )
54
+ end
55
+
56
+ # rubocop:disable Metrics/AbcSize
57
+ # rubocop:disable Metrics/MethodLength
58
+ def store_release_info
59
+ log = remote.chdir(paths.git_repo) do
60
+ remote.git(
61
+ 'log -n1 --date=iso --pretty=format:"%H/%cd/%ae" '\
62
+ "#{branch.shellescape}",
63
+ silent: true
64
+ ).stdout.strip
65
+ end
66
+
67
+ sha, date, email = log.split("/", 3)
68
+ remote.release[:branch] = branch
69
+ remote.release[:author] = email
70
+ remote.release[:revision] = sha
71
+ remote.release[:revision_date] = date
72
+ remote.release[:deploy_date] = Time.now.to_s
73
+ remote.release[:deploy_user] = ENV["USER"] || ENV["USERNAME"]
74
+ end
75
+ # rubocop:enable Metrics/MethodLength
76
+ # rubocop:enable Metrics/AbcSize
77
+ end
78
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "git/helpers"
2
+ require_relative "git/tasks"
3
+
4
+ module Tomo::Plugin
5
+ module Git
6
+ extend Tomo::PluginDSL
7
+
8
+ helpers Tomo::Plugin::Git::Helpers
9
+ tasks Tomo::Plugin::Git::Tasks
10
+
11
+ # rubocop:disable Metrics/LineLength
12
+ defaults git_branch: "master",
13
+ git_repo_path: "%<deploy_to>/git_repo",
14
+ git_exclusions: [],
15
+ git_env: { GIT_SSH_COMMAND: "ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no" },
16
+ git_url: nil
17
+ # rubocop:enable Metrics/LineLength
18
+ end
19
+ end
@@ -0,0 +1,61 @@
1
+ require "shellwords"
2
+
3
+ module Tomo::Plugin::Nvm
4
+ class Tasks < Tomo::TaskLibrary
5
+ def install
6
+ remote.mkdir_p raw("$HOME/.nvm")
7
+ modify_bashrc
8
+ run_installer
9
+ install_node
10
+ install_yarn
11
+ end
12
+
13
+ private
14
+
15
+ def run_installer
16
+ require_setting :nvm_version
17
+
18
+ nvm_version = settings[:nvm_version]
19
+ install_url = "https://raw.githubusercontent.com/creationix/nvm/"\
20
+ "v#{nvm_version}/install.sh"
21
+ remote.run("curl -o- #{install_url.shellescape} | bash")
22
+ end
23
+
24
+ def modify_bashrc
25
+ existing_rc = remote.capture("cat", paths.bashrc, raise_on_error: false)
26
+ return if existing_rc.include?("nvm.sh")
27
+
28
+ remote.write(text: <<~BASHRC + existing_rc, to: paths.bashrc)
29
+ export NVM_DIR="$HOME/.nvm"
30
+ [ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh"
31
+
32
+ BASHRC
33
+ end
34
+
35
+ def install_node
36
+ require_setting :nvm_node_version
37
+ node_version = settings[:nvm_node_version]
38
+
39
+ unless node_installed?(node_version)
40
+ remote.run "nvm", "install", node_version
41
+ end
42
+ remote.run "nvm", "alias", "default", node_version
43
+ end
44
+
45
+ def install_yarn
46
+ version = settings[:nvm_yarn_version]
47
+ return remote.run "npm i -g yarn@#{version.shellescape}" if version
48
+
49
+ logger.info "No :nvm_yarn_version specified; skipping yarn installation."
50
+ end
51
+
52
+ def node_installed?(version)
53
+ versions = remote.capture("nvm ls", raise_on_error: false)
54
+ if versions.include?("v#{version}")
55
+ logger.info("Node #{version} is already installed.")
56
+ return true
57
+ end
58
+ false
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "nvm/tasks"
2
+
3
+ module Tomo::Plugin
4
+ module Nvm
5
+ extend Tomo::PluginDSL
6
+
7
+ defaults bashrc_path: ".bashrc",
8
+ nvm_version: "0.34.0",
9
+ nvm_node_version: nil,
10
+ nvm_yarn_version: nil
11
+
12
+ tasks Tomo::Plugin::Nvm::Tasks
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ module Tomo::Plugin::Puma
2
+ class Tasks < Tomo::TaskLibrary
3
+ def restart
4
+ return if try_restart
5
+
6
+ remote.chdir(paths.current) do
7
+ logger.info "Puma is not running. Starting it now."
8
+ remote.bundle("exec", "puma", "--daemon", *control_options)
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def try_restart
15
+ ctl_result = remote.chdir(paths.current) do
16
+ ctl_result = remote.bundle(
17
+ "exec", "pumactl", *control_options, "restart",
18
+ raise_on_error: false,
19
+ silent: true
20
+ )
21
+ end
22
+
23
+ return false if dry_run? || ctl_result.failure?
24
+
25
+ logger.info(ctl_result.output)
26
+ true
27
+ end
28
+
29
+ def control_options
30
+ require_settings :puma_control_token, :puma_control_url
31
+
32
+ [
33
+ "--control-url", settings[:puma_control_url],
34
+ "--control-token", settings[:puma_control_token]
35
+ ]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "puma/tasks"
2
+
3
+ module Tomo::Plugin
4
+ module Puma
5
+ extend Tomo::PluginDSL
6
+
7
+ tasks Tomo::Plugin::Puma::Tasks
8
+
9
+ defaults puma_control_token: "tomo",
10
+ puma_control_url: "tcp://127.0.0.1:9293"
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ module Tomo::Plugin::Rails
2
+ module Helpers
3
+ def rails(*args, **opts)
4
+ prepend("exec", "rails") do
5
+ bundle(*args, **opts)
6
+ end
7
+ end
8
+
9
+ def rake(*args, **opts)
10
+ prepend("exec", "rails") do
11
+ bundle(*args, **opts)
12
+ end
13
+ end
14
+
15
+ def rake?(*args, **opts)
16
+ result = rake(*args, **opts.merge(raise_on_error: false))
17
+ result.success?
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,79 @@
1
+ module Tomo::Plugin::Rails
2
+ class Tasks < Tomo::TaskLibrary
3
+ def assets_precompile
4
+ remote.rake("assets:precompile")
5
+ end
6
+
7
+ def console
8
+ remote.rails("console", settings[:run_args], attach: true)
9
+ end
10
+
11
+ def db_migrate
12
+ remote.rake("db:migrate")
13
+ end
14
+
15
+ def db_seed
16
+ remote.rake("db:seed")
17
+ end
18
+
19
+ def db_create
20
+ return remote.rake("db:create") unless database_exists?
21
+
22
+ logger.info "Database already exists; skipping db:create."
23
+ end
24
+
25
+ def db_setup
26
+ return remote.rake("db:setup") unless database_exists?
27
+
28
+ logger.info "Database already exists; skipping db:setup."
29
+ end
30
+
31
+ def db_schema_load
32
+ if !schema_rb_present?
33
+ logger.warn "db/schema.rb is not present; skipping schema:load."
34
+ elsif database_schema_loaded?
35
+ logger.info "Database schema already loaded; skipping db:schema:load."
36
+ else
37
+ remote.rake("db:schema:load")
38
+ end
39
+ end
40
+
41
+ def db_structure_load
42
+ if !structure_sql_present?
43
+ logger.warn "db/structure.sql is not present; "\
44
+ "skipping db:structure:load."
45
+ elsif database_schema_loaded?
46
+ logger.info "Database structure already loaded; "\
47
+ "skipping db:structure:load."
48
+ else
49
+ remote.rake("db:structure:load")
50
+ end
51
+ end
52
+
53
+ def log_tail
54
+ log_path = raw("#{paths.release.to_s.shellescape}/log/${RAILS_ENV}.log")
55
+ remote.run("tail", settings[:run_args], log_path)
56
+ end
57
+
58
+ private
59
+
60
+ def database_exists?
61
+ remote.rake?("db:version", silent: true) && !dry_run?
62
+ end
63
+
64
+ def database_schema_loaded?
65
+ result = remote.rake("db:version", silent: true, raise_on_error: false)
66
+ schema_version = result.output[/version:\s*(\d+)$/i, 1].to_i
67
+
68
+ result.success? && schema_version.positive? && !dry_run?
69
+ end
70
+
71
+ def schema_rb_present?
72
+ remote.file?(paths.release.join("db/schema.rb"))
73
+ end
74
+
75
+ def structure_sql_present?
76
+ remote.file?(paths.release.join("db/structure.sql"))
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,11 @@
1
+ require_relative "rails/helpers"
2
+ require_relative "rails/tasks"
3
+
4
+ module Tomo::Plugin
5
+ module Rails
6
+ extend Tomo::PluginDSL
7
+
8
+ helpers Tomo::Plugin::Rails::Helpers
9
+ tasks Tomo::Plugin::Rails::Tasks
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ require "shellwords"
2
+
3
+ module Tomo::Plugin::Rbenv
4
+ class Tasks < Tomo::TaskLibrary
5
+ def install
6
+ run_installer
7
+ modify_bashrc
8
+ compile_ruby
9
+ end
10
+
11
+ private
12
+
13
+ def run_installer
14
+ install_url = "https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-installer"
15
+ remote.env PATH: raw("$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH") do
16
+ remote.run("curl -fsSL #{install_url.shellescape} | bash")
17
+ end
18
+ end
19
+
20
+ def modify_bashrc
21
+ existing_rc = remote.capture("cat", paths.bashrc, raise_on_error: false)
22
+ return if existing_rc.include?("rbenv init")
23
+
24
+ remote.write(text: <<~BASHRC + existing_rc, to: paths.bashrc)
25
+ if [ -d $HOME/.rbenv ]; then
26
+ export PATH="$HOME/.rbenv/bin:$PATH"
27
+ eval "$(rbenv init -)"
28
+ fi
29
+
30
+ BASHRC
31
+ end
32
+
33
+ def compile_ruby
34
+ require_setting :rbenv_ruby_version
35
+ ruby_version = settings[:rbenv_ruby_version]
36
+
37
+ unless ruby_installed?(ruby_version)
38
+ logger.info(
39
+ "Installing ruby #{ruby_version} -- this may take several minutes"
40
+ )
41
+ remote.run "CFLAGS=-O3 rbenv install #{ruby_version.shellescape}"
42
+ end
43
+ remote.run "rbenv global #{ruby_version.shellescape}"
44
+ end
45
+
46
+ def ruby_installed?(version)
47
+ versions = remote.capture("rbenv versions", raise_on_error: false)
48
+ if versions.include?(version)
49
+ logger.info("Ruby #{version} is already installed.")
50
+ return true
51
+ end
52
+ false
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "rbenv/tasks"
2
+
3
+ module Tomo::Plugin
4
+ module Rbenv
5
+ extend Tomo::PluginDSL
6
+
7
+ defaults bashrc_path: ".bashrc",
8
+ rbenv_ruby_version: nil
9
+
10
+ tasks Tomo::Plugin::Rbenv::Tasks
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ unless defined?(Tomo::Testing)
2
+ raise "The testing plugin cannot be used outside of unit tests"
3
+ end
4
+
5
+ module Tomo::Plugin
6
+ class Testing < Tomo::TaskLibrary
7
+ extend Tomo::PluginDSL
8
+ tasks self
9
+
10
+ def call_helper
11
+ helper, args, kwargs = settings[:run_args]
12
+ value = remote.public_send(helper, *args, **(kwargs || {}))
13
+ remote.host.helper_values << value
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ module Tomo
2
+ module Plugin
3
+ end
4
+ end
@@ -0,0 +1,23 @@
1
+ module Tomo
2
+ module PluginDSL
3
+ def self.extended(mod)
4
+ mod.instance_variable_set(:@helper_modules, [])
5
+ mod.instance_variable_set(:@default_settings, {})
6
+ mod.instance_variable_set(:@tasks_classes, [])
7
+ end
8
+
9
+ attr_reader :helper_modules, :default_settings, :tasks_classes
10
+
11
+ def helpers(mod, *more_mods)
12
+ @helper_modules.push(mod, *more_mods)
13
+ end
14
+
15
+ def defaults(settings)
16
+ @default_settings.merge!(settings)
17
+ end
18
+
19
+ def tasks(tasks_class, *more_tasks_classes)
20
+ @tasks_classes.push(tasks_class, *more_tasks_classes)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ require "forwardable"
2
+
3
+ module Tomo
4
+ class Remote
5
+ extend Forwardable
6
+ def_delegators :ssh, :close, :host
7
+ def_delegators :shell_builder, :chdir, :env, :prepend, :umask
8
+
9
+ attr_reader :release
10
+
11
+ def initialize(ssh, context, helper_modules)
12
+ @ssh = ssh
13
+ @context = context
14
+ @release = {}
15
+ @shell_builder = ShellBuilder.new
16
+ helper_modules.each { |mod| extend(mod) }
17
+ freeze
18
+ end
19
+
20
+ def attach(*command, default_chdir: nil, **command_opts)
21
+ full_command = shell_builder.build(*command, default_chdir: default_chdir)
22
+ ssh.ssh_exec(
23
+ Script.new(full_command, { pty: true }.merge(**command_opts))
24
+ )
25
+ end
26
+
27
+ def run(*command, attach: false, default_chdir: nil, **command_opts)
28
+ attach(*command, default_chdir: default_chdir, **command_opts) if attach
29
+
30
+ full_command = shell_builder.build(*command, default_chdir: default_chdir)
31
+ ssh.ssh_subprocess(Script.new(full_command, **command_opts))
32
+ end
33
+
34
+ private
35
+
36
+ def_delegators :context, :paths, :settings
37
+ attr_reader :context, :ssh, :shell_builder
38
+
39
+ def dry_run?
40
+ Tomo.dry_run?
41
+ end
42
+
43
+ def logger
44
+ Tomo.logger
45
+ end
46
+
47
+ def raw(str)
48
+ ShellBuilder.raw(str)
49
+ end
50
+
51
+ def remote
52
+ self
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ module Tomo
2
+ class Result
3
+ def self.empty_success
4
+ new(stdout: "", stderr: "", exit_status: 0)
5
+ end
6
+
7
+ attr_reader :stdout, :stderr, :exit_status
8
+
9
+ def initialize(stdout:, stderr:, exit_status:)
10
+ @stdout = stdout
11
+ @stderr = stderr
12
+ @exit_status = exit_status
13
+ freeze
14
+ end
15
+
16
+ def success?
17
+ exit_status.zero?
18
+ end
19
+
20
+ def failure?
21
+ !success?
22
+ end
23
+
24
+ def output
25
+ [stdout, stderr].compact.join
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ module Tomo
2
+ class Runtime
3
+ class ConcurrentRubyLoadError < Tomo::Error
4
+ attr_accessor :version
5
+
6
+ def to_console
7
+ <<~ERROR
8
+ Running tasks on multiple hosts requires the #{yellow('concurrent-ruby')} gem.
9
+ To install it, #{install_instructions}
10
+ ERROR
11
+ end
12
+
13
+ private
14
+
15
+ def install_instructions
16
+ if Tomo.bundled?
17
+ gem_entry = %Q(gem "concurrent-ruby", "#{version}")
18
+ "add this entry to your Gemfile:\n\n #{blue(gem_entry)}"
19
+ else
20
+ gem_install = "gem install concurrent-ruby -v '#{version}'"
21
+ "run:\n\n #{blue(gem_install)}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,50 @@
1
+ concurrent_ver = "~> 1.1"
2
+
3
+ begin
4
+ gem "concurrent-ruby", concurrent_ver
5
+ require "concurrent"
6
+ rescue LoadError => e
7
+ Tomo::Runtime::ConcurrentRubyLoadError.raise_with(
8
+ e.message,
9
+ version: concurrent_ver
10
+ )
11
+ end
12
+
13
+ module Tomo
14
+ class Runtime
15
+ class ConcurrentRubyThreadPool
16
+ include ::Concurrent::Promises::FactoryMethods
17
+
18
+ def initialize(size)
19
+ @executor = ::Concurrent::FixedThreadPool.new(size)
20
+ @promises = []
21
+ end
22
+
23
+ def post(*args, &block)
24
+ return if failure?
25
+
26
+ promises << future_on(executor, *args, &block)
27
+ .on_rejection_using(executor) do |reason|
28
+ self.failure = reason
29
+ end
30
+ nil
31
+ end
32
+
33
+ def run_to_completion
34
+ promises_to_wait = promises.dup
35
+ promises.clear
36
+ zip_futures_on(executor, *promises_to_wait).value
37
+ raise failure if failure?
38
+ end
39
+
40
+ def failure?
41
+ !!failure
42
+ end
43
+
44
+ private
45
+
46
+ attr_accessor :failure
47
+ attr_reader :executor, :promises
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ module Tomo
2
+ class Runtime
3
+ class Context
4
+ attr_reader :paths, :settings
5
+
6
+ def initialize(settings)
7
+ @paths = Paths.new(settings)
8
+ @settings = settings.freeze
9
+ freeze
10
+ end
11
+
12
+ def current_remote
13
+ Current.remote
14
+ end
15
+
16
+ def current_task
17
+ Current.task
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ module Tomo
2
+ class Runtime
3
+ module Current
4
+ class << self
5
+ def host
6
+ fiber_locals[:host] || remote&.host
7
+ end
8
+
9
+ def remote
10
+ fiber_locals[:remote]
11
+ end
12
+
13
+ def task
14
+ fiber_locals[:task]
15
+ end
16
+
17
+ def with(new_locals)
18
+ old_locals = slice(*new_locals.keys)
19
+ fiber_locals.merge!(new_locals)
20
+ yield
21
+ ensure
22
+ fiber_locals.merge!(old_locals)
23
+ end
24
+
25
+ def variables
26
+ fiber_locals.dup.freeze
27
+ end
28
+
29
+ private
30
+
31
+ def slice(*keys)
32
+ Hash[keys.map { |key| [key, fiber_locals[key]] }]
33
+ end
34
+
35
+ def fiber_locals
36
+ Thread.current["Tomo::Runtime::Current"] ||= {}
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end