tomo 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e355e8b9452472cd2e2da9cbbfb6d577d4bf4ae0bfdb57a16216a2b45387433b
4
- data.tar.gz: ceae2f4b9e5d766824bca43b5f44f71d3d0c2af1e2ae3f4608b33a60c41a61ae
3
+ metadata.gz: 67815fa19818dc29ff1226179db8b2b364c4843eb03c3a507561dd105b480b9f
4
+ data.tar.gz: 7119dc681c2e8e167712a1ee2a35bb7725d16f4f14b0438ec036abd7f30d42e5
5
5
  SHA512:
6
- metadata.gz: 8e36ee70864b8b0d663d7766dc83b6e4f788f7d525ff36624c96cd1f56a87f2182b8e955e87a5e93cc708253edf352d4677fbe554adab1cc33877faa100ff56c
7
- data.tar.gz: f99aa7197d92bf4e81836f2c892ac1a76b4c7580de1cf72e8f0c1ea47914dfc85504019ac80922336179c7ff62510f6fe3bfe86cf7bb7b6a1a8bae8a526bca6b
6
+ metadata.gz: 271bafaa1a657c3d5961629f07f64ff7e595ca38b20fc34abc3c747418e108dee5a2b50cc6b164ba0721a71e7779100e4af5544f3085d596d9397c8455c6c910
7
+ data.tar.gz: e8a9dfd41c8226238add4a7dbddd5b212cff0953d9eb25b2cfbaf9c435d48e741bd806fbaec76b5c7591d0073e0cd0982885ab0b7a03b70f3628fb9f0eebef5f
data/README.md CHANGED
@@ -60,7 +60,7 @@ plugin "rails"
60
60
  host "user@hostname.or.ip.address"
61
61
 
62
62
  set application: "my-rails-app"
63
- set deploy_to: "/var/www/%<application>"
63
+ set deploy_to: "/var/www/%{application}"
64
64
  set git_url: "git@github.com:my-username/my-rails-app.git"
65
65
  set git_branch: "master"
66
66
  # ...
@@ -202,20 +202,6 @@ By default, tomo uses the ["accept-new"](https://www.openssh.com/txt/release-7.6
202
202
  set ssh_strict_host_key_checking: true # or false
203
203
  ```
204
204
 
205
- #### Why does my deploy hang after starting puma?
206
-
207
- Puma 4.1.0 [has a bug](https://github.com/puma/puma/issues/1906) where its output isn't properly detached prior to daemonzing. This causes tomo to hang waiting for output. You may see something like this prior to the deploy freezing:
208
-
209
- ```
210
- Puma starting in single mode...
211
- * Version 4.1.0 (ruby 2.6.4-p104), codename: Fourth and One
212
- * Min threads: 5, max threads: 5
213
- * Environment: production
214
- * Daemonizing...
215
- ```
216
-
217
- To fix, upgrade to puma 4.1.1 or newer.
218
-
219
205
  ## Support
220
206
 
221
207
  This project is a labor of love and I can only spend a few hours a week maintaining it, at most. If you'd like to help by submitting a pull request, or if you've discovered a bug that needs my attention, please let me know. Check out [CONTRIBUTING.md](https://github.com/mattbrictson/tomo/blob/master/CONTRIBUTING.md) to get started. Happy hacking! —Matt
@@ -12,7 +12,7 @@ module Tomo::Plugin
12
12
  bundler_deployment: true,
13
13
  bundler_gemfile: nil,
14
14
  bundler_jobs: "4",
15
- bundler_path: "%<shared_path>/bundle",
15
+ bundler_path: "%{shared_path}/bundle",
16
16
  bundler_retry: "3",
17
17
  bundler_version: nil,
18
18
  bundler_without: %w[development test]
@@ -11,16 +11,16 @@ module Tomo::Plugin
11
11
  defaults Tomo::SSH::Options::DEFAULTS.merge(
12
12
  application: "default",
13
13
  concurrency: 10,
14
- current_path: "%<deploy_to>/current",
15
- deploy_to: "/var/www/%<application>",
14
+ current_path: "%{deploy_to}/current",
15
+ deploy_to: "/var/www/%{application}",
16
16
  keep_releases: 10,
17
17
  linked_dirs: [],
18
18
  linked_files: [],
19
19
  local_user: nil, # determined at runtime
20
- release_json_path: "%<release_path>/.tomo_release.json",
21
- releases_path: "%<deploy_to>/releases",
22
- revision_log_path: "%<deploy_to>/revisions.log",
23
- shared_path: "%<deploy_to>/shared",
20
+ release_json_path: "%{release_path}/.tomo_release.json",
21
+ releases_path: "%{deploy_to}/releases",
22
+ revision_log_path: "%{deploy_to}/revisions.log",
23
+ shared_path: "%{deploy_to}/shared",
24
24
  tmp_path: "/tmp/tomo",
25
25
  tomo_config_file_path: nil, # determined at runtime
26
26
  run_args: [] # determined at runtime
@@ -7,7 +7,7 @@ module Tomo::Plugin
7
7
  tasks Tomo::Plugin::Env::Tasks
8
8
 
9
9
  defaults bashrc_path: ".bashrc",
10
- env_path: "%<deploy_to>/envrc",
10
+ env_path: "%{deploy_to}/envrc",
11
11
  env_vars: {}
12
12
  end
13
13
  end
@@ -10,7 +10,7 @@ module Tomo::Plugin
10
10
 
11
11
  # rubocop:disable Metrics/LineLength
12
12
  defaults git_branch: "master",
13
- git_repo_path: "%<deploy_to>/git_repo",
13
+ git_repo_path: "%{deploy_to}/git_repo",
14
14
  git_exclusions: [],
15
15
  git_env: { GIT_SSH_COMMAND: "ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no" },
16
16
  git_ref: nil,
@@ -0,0 +1,22 @@
1
+ [Unit]
2
+ Description=Puma HTTP Server for <%= settings[:application] %>
3
+ After=network.target
4
+ Requires=<%= settings[:puma_systemd_socket] %>
5
+ ConditionPathExists=<%= paths.current %>
6
+
7
+ [Service]
8
+ ExecStart=/bin/bash -lc 'exec bundle exec --keep-file-descriptors puma -C config/puma.rb -b tcp://0.0.0.0:<%= settings[:puma_port] %>'
9
+ KillMode=mixed
10
+ Restart=always
11
+ StandardError=syslog
12
+ StandardInput=null
13
+ StandardOutput=syslog
14
+ SyslogIdentifier=%n
15
+ TimeoutStopSec=5
16
+ Type=simple
17
+ WorkingDirectory=<%= paths.current %>
18
+ # Helpful for debugging socket activation, etc.
19
+ # Environment=PUMA_DEBUG=1
20
+
21
+ [Install]
22
+ WantedBy=multi-user.target
@@ -0,0 +1,13 @@
1
+ [Unit]
2
+ Description=Puma HTTP Server Accept Sockets for <%= settings[:application] %>
3
+
4
+ [Socket]
5
+ ListenStream=0.0.0.0:<%= settings[:puma_port] %>
6
+
7
+ # Socket options matching Puma defaults
8
+ NoDelay=true
9
+ ReusePort=true
10
+ Backlog=1024
11
+
12
+ [Install]
13
+ WantedBy=sockets.target
@@ -1,60 +1,121 @@
1
1
  module Tomo::Plugin::Puma
2
2
  class Tasks < Tomo::TaskLibrary
3
+ SystemdUnit = Struct.new(:name, :template, :path)
4
+
5
+ # rubocop:disable Metrics/AbcSize
6
+ def setup_systemd
7
+ linger_must_be_enabled!
8
+
9
+ setup_directories
10
+ remote.write template: socket.template, to: socket.path
11
+ remote.write template: service.template, to: service.path
12
+
13
+ remote.run "systemctl --user daemon-reload"
14
+ remote.run "systemctl", "--user", "enable", service.name, socket.name
15
+ end
16
+ # rubocop:enable Metrics/AbcSize
17
+
18
+ %i[start stop].each do |action|
19
+ define_method(action) do
20
+ remote.run "systemctl", "--user", action, socket.name, service.name
21
+ end
22
+ end
23
+
3
24
  def restart
4
- return if try_restart
25
+ remote.run "systemctl", "--user", "start", socket.name
26
+ remote.run "systemctl", "--user", "restart", service.name
27
+ end
28
+
29
+ def check_active
30
+ logger.info "Checking if puma is active and listening on port #{port}..."
31
+
32
+ active = wait_until { dry_run? || (assert_active! && listening?) }
33
+ remote.run("systemctl", "--user", "status", service.name)
34
+ return if active
35
+
36
+ logger.warn "Timed out waiting for puma to respond on port #{port}"
37
+ end
5
38
 
6
- logger.info "Puma is not running. Starting it now."
7
- start
39
+ def log
40
+ remote.attach "journalctl", "-q",
41
+ raw("--user-unit=#{service.name.shellescape}"),
42
+ *settings[:run_args]
8
43
  end
9
44
 
10
45
  private
11
46
 
12
- def try_restart
13
- ctl_result = remote.chdir(paths.current) do
14
- remote.bundle(
15
- "exec", "pumactl", *control_options, "restart",
16
- raise_on_error: false,
17
- silent: true
18
- )
19
- end
47
+ def port
48
+ settings[:puma_port]
49
+ end
20
50
 
21
- return false if dry_run? || ctl_result.failure?
51
+ def service
52
+ SystemdUnit.new(
53
+ settings[:puma_systemd_service],
54
+ paths.puma_systemd_service_template,
55
+ paths.puma_systemd_service
56
+ )
57
+ end
22
58
 
23
- logger.info(ctl_result.output)
24
- true
59
+ def socket
60
+ SystemdUnit.new(
61
+ settings[:puma_systemd_socket],
62
+ paths.puma_systemd_socket_template,
63
+ paths.puma_systemd_socket
64
+ )
25
65
  end
26
66
 
27
- def start
28
- require_settings :puma_stdout_path, :puma_stderr_path
67
+ def linger_must_be_enabled!
68
+ loginctl_result = remote.run "loginctl", "user-status", remote.host.user
69
+ return unless loginctl_result.stdout.match?(/^\s*Linger:\s*no\s*$/i)
29
70
 
30
- ensure_output_directory
71
+ die <<~ERROR.strip
72
+ Linger must be enabled for the #{remote.host.user} user in order for
73
+ puma to stay running in the background via systemd. Run the following
74
+ command as root:
31
75
 
32
- remote.chdir(paths.current) do
33
- remote.bundle(
34
- "exec", "puma", "--daemon", *control_options, *output_options
35
- )
36
- end
76
+ loginctl enable-linger #{remote.host.user}
77
+ ERROR
37
78
  end
38
79
 
39
- def ensure_output_directory
40
- dirs = [paths.puma_stdout, paths.puma_stderr].map(&:dirname).map(&:to_s)
80
+ def setup_directories
81
+ files = [service.path, socket.path].compact
82
+ dirs = files.map(&:dirname).map(&:to_s)
41
83
  remote.mkdir_p dirs.uniq
42
84
  end
43
85
 
44
- def control_options
45
- require_settings :puma_control_token, :puma_control_url
86
+ def wait_until
87
+ timeout = settings[:puma_check_timeout].to_i
88
+ start = Time.now.to_i
89
+ delay = 1
90
+
91
+ loop do
92
+ sleep delay
93
+ return true if yield
94
+
95
+ elapsed = Time.now.to_i - start
96
+ return false if elapsed >= timeout
97
+
98
+ delay = [delay + 1, timeout - elapsed].min
99
+ end
100
+ end
101
+
102
+ def assert_active!
103
+ return true if remote.run? "systemctl", "--user", "is-active",
104
+ service.name,
105
+ silent: true, raise_on_error: false
106
+
107
+ remote.run "systemctl", "--user", "status", service.name,
108
+ raise_on_error: false
109
+ remote.run "journalctl -q -n 50 --user-unit=#{service.name.shellescape}",
110
+ raise_on_error: false
46
111
 
47
- [
48
- "--control-url", settings[:puma_control_url],
49
- "--control-token", settings[:puma_control_token]
50
- ]
112
+ die "puma failed to start (see previous systemctl and journalctl output)"
51
113
  end
52
114
 
53
- def output_options
54
- options = []
55
- options << ["--redirect-stdout", paths.puma_stdout] if paths.puma_stdout
56
- options << ["--redirect-stderr", paths.puma_stderr] if paths.puma_stderr
57
- options.flatten
115
+ def listening?
116
+ test_url = "http://localhost:#{port}"
117
+ remote.run? "curl -sS --connect-timeout 1 --max-time 10 #{test_url}"\
118
+ " > /dev/null"
58
119
  end
59
120
  end
60
121
  end
@@ -6,9 +6,15 @@ module Tomo::Plugin
6
6
 
7
7
  tasks Tomo::Plugin::Puma::Tasks
8
8
 
9
- defaults puma_control_token: "tomo",
10
- puma_control_url: "tcp://127.0.0.1:9293",
11
- puma_stderr_path: "%<shared_path>/log/puma.err",
12
- puma_stdout_path: "%<shared_path>/log/puma.out"
9
+ # rubocop:disable Metrics/LineLength
10
+ defaults puma_check_timeout: 15,
11
+ puma_port: "3000",
12
+ puma_systemd_service: "puma_%{application}.service",
13
+ puma_systemd_socket: "puma_%{application}.socket",
14
+ puma_systemd_service_path: ".config/systemd/user/%{puma_systemd_service}",
15
+ puma_systemd_socket_path: ".config/systemd/user/%{puma_systemd_socket}",
16
+ puma_systemd_service_template_path: File.expand_path("puma/systemd/service.erb", __dir__),
17
+ puma_systemd_socket_template_path: File.expand_path("puma/systemd/socket.erb", __dir__)
18
+ # rubocop:enable Metrics/LineLength
13
19
  end
14
20
  end
@@ -50,6 +50,7 @@ module Tomo::Plugin::Rails
50
50
  end
51
51
  end
52
52
 
53
+ # TODO: remove
53
54
  def log_tail
54
55
  log_path = raw("#{paths.release.to_s.shellescape}/log/${RAILS_ENV}.log")
55
56
  remote.run("tail", settings[:run_args], log_path)
@@ -7,33 +7,75 @@ module Tomo
7
7
 
8
8
  def initialize(settings)
9
9
  @settings = symbolize(settings)
10
+ @deprecation_warnings = []
10
11
  end
11
12
 
12
13
  def call
13
14
  hash = Hash[settings.keys.map { |name| [name, fetch(name)] }]
14
15
  dump_settings(hash) if Tomo.debug?
16
+ print_deprecation_warnings
15
17
  hash
16
18
  end
17
19
 
18
20
  private
19
21
 
20
- attr_reader :settings
22
+ attr_reader :settings, :deprecation_warnings
21
23
 
24
+ # rubocop:disable Metrics/AbcSize
22
25
  def fetch(name, stack=[])
23
26
  raise_circular_dependency_error(name, stack) if stack.include?(name)
24
27
  value = settings.fetch(name)
25
28
  return value unless value.is_a?(String)
26
29
 
27
- value.gsub(/%<(\w+)>/) do
28
- fetch(Regexp.last_match[1].to_sym, stack + [name])
30
+ value.gsub(/%{(\w+)}|%<(\w+)>/) do
31
+ token = Regexp.last_match[1] || Regexp.last_match[2]
32
+ deprecation_warnings << name if Regexp.last_match[2]
33
+
34
+ fetch(token.to_sym, stack + [name])
29
35
  end
30
36
  end
37
+ # rubocop:enable Metrics/AbcSize
31
38
 
32
39
  def raise_circular_dependency_error(name, stack)
33
40
  dependencies = [*stack, name].join(" -> ")
34
41
  raise "Circular dependency detected in settings: #{dependencies}"
35
42
  end
36
43
 
44
+ # rubocop:disable Metrics/AbcSize
45
+ # rubocop:disable Metrics/MethodLength
46
+ def print_deprecation_warnings
47
+ return if deprecation_warnings.empty?
48
+
49
+ examples = ""
50
+ deprecation_warnings.uniq.each do |name|
51
+ sett = settings[name].inspect
52
+ old_syntax = sett.gsub(
53
+ /%<(\w+)>/,
54
+ Colors.red("%<") + '\1' + Colors.red(">")
55
+ )
56
+ new_syntax = sett.gsub(
57
+ /%<(\w+)>/,
58
+ Colors.green("%{") + '\1' + Colors.green("}")
59
+ )
60
+
61
+ examples << "\n:#{name}\n\n"
62
+ examples << " Replace: set #{name}: #{old_syntax}\n"
63
+ examples << " with this: set #{name}: #{new_syntax}\n"
64
+ end
65
+
66
+ Tomo.logger.warn <<~WARNING
67
+ There are settings using the deprecated %<...> interpolation syntax.
68
+ #{examples}
69
+ #{Colors.red('The %<...> syntax will not work in future versions of tomo.')}
70
+
71
+ WARNING
72
+
73
+ # Make sure people see the warning!
74
+ sleep 5
75
+ end
76
+ # rubocop:enable Metrics/AbcSize
77
+ # rubocop:enable Metrics/MethodLength
78
+
37
79
  def symbolize(hash)
38
80
  hash.each_with_object({}) do |(key, value), symbolized|
39
81
  symbolized[key.to_sym] = value
data/lib/tomo/runtime.rb CHANGED
@@ -84,9 +84,9 @@ module Tomo
84
84
  release = start_time.utc.strftime("%Y%m%d%H%M%S")
85
85
 
86
86
  case type
87
- when :current then "%<current_path>"
88
- when :new then "%<releases_path>/#{release}"
89
- when :tmp then "%<tmp_path>/#{release}"
87
+ when :current then "%{current_path}"
88
+ when :new then "%{releases_path}/#{release}"
89
+ when :tmp then "%{tmp_path}/#{release}"
90
90
  else
91
91
  raise ArgumentError, "release: must be :current, :new, or :tmp"
92
92
  end
@@ -10,7 +10,7 @@ plugin "./plugins/<%= app %>.rb"
10
10
  host "user@hostname.or.ip.address"
11
11
 
12
12
  set application: <%= app.inspect %>
13
- set deploy_to: "/var/www/%<application>"
13
+ set deploy_to: "/var/www/%{application}"
14
14
  set rbenv_ruby_version: <%= RUBY_VERSION.inspect %>
15
15
  set nodenv_node_version: <%= node_version&.inspect || "nil # FIXME" %>
16
16
  set nodenv_yarn_version: <%= yarn_version.inspect %>
@@ -51,6 +51,7 @@ setup do
51
51
  run "rails:db_create"
52
52
  run "rails:db_schema_load"
53
53
  run "rails:db_seed"
54
+ run "puma:setup_systemd"
54
55
  end
55
56
 
56
57
  deploy do
@@ -64,6 +65,7 @@ deploy do
64
65
  run "rails:assets_precompile"
65
66
  run "core:symlink_current"
66
67
  run "puma:restart"
68
+ run "puma:check_active"
67
69
  run "core:clean_releases"
68
70
  run "bundler:clean"
69
71
  run "core:log_revision"
@@ -5,6 +5,10 @@ COPY ./ubuntu_setup.sh ./
5
5
  RUN ./ubuntu_setup.sh
6
6
  COPY ./custom_setup.sh ./
7
7
  RUN ./custom_setup.sh
8
+ COPY ./systemctl.rb /usr/local/bin/systemctl
9
+ RUN chmod a+x /usr/local/bin/systemctl
10
+ COPY ./loginctl.sh /usr/local/bin/loginctl
11
+ RUN chmod a+x /usr/local/bin/loginctl
8
12
  EXPOSE 22
9
13
  EXPOSE 3000
10
14
  CMD ["/usr/sbin/sshd", "-D"]
@@ -9,6 +9,15 @@ at_exit { Tomo::Testing::DockerImage.running_images.each(&:stop) }
9
9
  module Tomo
10
10
  module Testing
11
11
  class DockerImage
12
+ FILES_TO_COPY = %w[
13
+ Dockerfile
14
+ loginctl.sh
15
+ systemctl.rb
16
+ tomo_test_ed25519.pub
17
+ ubuntu_setup.sh
18
+ ].freeze
19
+ private_constant :FILES_TO_COPY
20
+
12
21
  class << self
13
22
  attr_reader :running_images
14
23
  end
@@ -100,8 +109,7 @@ module Tomo
100
109
 
101
110
  def set_up_build_dir
102
111
  FileUtils.mkdir_p(build_dir)
103
- files = %w[Dockerfile tomo_test_ed25519.pub ubuntu_setup.sh]
104
- files.each do |file|
112
+ FILES_TO_COPY.each do |file|
105
113
  FileUtils.cp(File.expand_path(file, __dir__), build_dir)
106
114
  end
107
115
  IO.write(File.join(build_dir, "custom_setup.sh"), setup_script)
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+
3
+ # THIS FILE IS FOR TESTING PURPOSES ONLY.
4
+ #
5
+ # The real loginctl command does not work in a Docker container, so this empty
6
+ # script takes its place, allowing tomo to work during an E2E test.
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # THIS SCRIPT IS FOR TESTING PURPOSES ONLY.
4
+ #
5
+ # We use Docker to run tomo deploy tests. Docker is not able to run systemd, but
6
+ # tomo needs systemd for starting long-lived processes (e.g. puma). This script
7
+ # simulates the behavior of systemctl commands so that a tomo deploy can succeed
8
+ # in a Docker container where the real systemctl is unavailable.
9
+ #
10
+ # This basic workflow is supported:
11
+ #
12
+ # 1. systemctl --user enable [units...]
13
+ # 2. systemctl --user start [units...]
14
+ # 3. systemctl --user restart [units...]
15
+ # 4. systemctl --user is-active [units...]
16
+ # 5. systemctl --user status [units...]
17
+ #
18
+ # No other commands or options are supported. The only configuration that this
19
+ # script understands is the ExecStart and WorkingDirectory attributes in a
20
+ # *.service file that is expected to be installed in ~/.config/systemd/user/.
21
+ #
22
+ # This script will fork and exec the command listed in ExecStart and store the
23
+ # resulting PID so that it can later be used when stopping or restarting the
24
+ # service. It does not monitor the process, handle stdout/stderr of the process,
25
+ # or do any of the real work that systemd is designed to handle. It simply is
26
+ # the bare minimum behavior needed for tomo deploy to pass an E2E test.
27
+
28
+ require "pstore"
29
+
30
+ COMMANDS = %w[
31
+ daemon-reload
32
+ enable
33
+ is-active
34
+ restart
35
+ start
36
+ status
37
+ stop
38
+ ].freeze
39
+
40
+ def main(args)
41
+ args = args.dup
42
+ raise "First arg must be --user" unless args.shift == "--user"
43
+ raise "Missing command" if args.empty?
44
+
45
+ command = args.shift
46
+ raise "Unknown command: #{command}" unless COMMANDS.include?(command)
47
+
48
+ run(command, args)
49
+ end
50
+
51
+ def run(command, args)
52
+ return daemon_reload(args) if command == "daemon-reload"
53
+ raise "#{command} requires an argument" if args.empty?
54
+
55
+ args.each { |name| Unit.find(name).public_send(command.tr("-", "_")) }
56
+ end
57
+
58
+ def daemon_reload(args)
59
+ raise "daemon-reload does not accept arguments" unless args.empty?
60
+ end
61
+
62
+ class Unit
63
+ def self.find(name)
64
+ path = File.join(File.expand_path("~/.config/systemd/user/"), name)
65
+ raise "Unknown unit: #{name}" unless File.file?(path)
66
+ return Service.new(name, IO.read(path)) if name.end_with?(".service")
67
+
68
+ new(name, IO.read(path))
69
+ end
70
+
71
+ def initialize(name, spec)
72
+ @name = name
73
+ @spec = spec
74
+ end
75
+
76
+ def enable
77
+ with_persistent_state { |state| state[:enabled] = true }
78
+ end
79
+
80
+ def status
81
+ puts "● #{name}"
82
+ puts " Loaded: loaded (enabled; vendor preset: enabled)" if enabled?
83
+ end
84
+
85
+ def start
86
+ must_be_enabled!
87
+ end
88
+
89
+ def stop
90
+ must_be_enabled!
91
+ end
92
+
93
+ def restart
94
+ must_be_enabled!
95
+ end
96
+
97
+ private
98
+
99
+ attr_reader :name, :spec
100
+
101
+ def must_be_enabled!
102
+ raise "#{name} must be enabled first" unless enabled?
103
+ end
104
+
105
+ def enabled?
106
+ with_persistent_state { |state| state[:enabled] }
107
+ end
108
+
109
+ def with_persistent_state
110
+ @pstore ||= begin
111
+ pstore_path = File.expand_path("~/.config/systemd/state.db")
112
+ PStore.new(pstore_path)
113
+ end
114
+
115
+ @pstore.transaction do
116
+ state = @pstore[name] ||= {}
117
+ yield(state)
118
+ end
119
+ end
120
+ end
121
+
122
+ class Service < Unit
123
+ def is_active # rubocop:disable Naming/PredicateName
124
+ exit(false) unless started?
125
+ puts "active"
126
+ end
127
+
128
+ def start
129
+ super
130
+ raise "#{name} is already running" if started?
131
+
132
+ working_dir, executable = parse
133
+
134
+ if (pid = Process.fork)
135
+ with_persistent_state { |state| state[:pid] = pid }
136
+ Process.detach(pid)
137
+ return
138
+ end
139
+
140
+ with_detached_io { Dir.chdir(working_dir) { Process.exec(executable) } }
141
+ end
142
+
143
+ def stop
144
+ with_persistent_state do |state|
145
+ pid = state.delete(:pid)
146
+ Process.kill("TERM", pid) unless pid.nil?
147
+ end
148
+ end
149
+
150
+ def restart
151
+ super
152
+ stop if started?
153
+ start
154
+ end
155
+
156
+ def status
157
+ super
158
+ puts " Active: active (running)" if started?
159
+ end
160
+
161
+ private
162
+
163
+ def started?
164
+ with_persistent_state { |state| !state[:pid].nil? }
165
+ end
166
+
167
+ def parse
168
+ config = Hash[spec.scan(/^([^\s=]+)=\s*(\S.*?)\s*$/)]
169
+ working_dir = config["WorkingDirectory"] || File.expand_path("~")
170
+ executable = config.fetch("ExecStart") do
171
+ raise "#{name} is missing ExecStart attribute"
172
+ end
173
+
174
+ [working_dir, executable]
175
+ end
176
+
177
+ def with_detached_io
178
+ null_in = File.open(File::NULL, "r")
179
+ null_out = File.open(File::NULL, "w")
180
+ $stdin.reopen(null_in)
181
+ $stderr.reopen(null_out)
182
+ $stdout.reopen(null_out)
183
+ yield
184
+ end
185
+ end
186
+
187
+ main(ARGV) if $PROGRAM_NAME == __FILE__
data/lib/tomo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Tomo
2
- VERSION = "0.8.1".freeze
2
+ VERSION = "0.9.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tomo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Brictson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-09-29 00:00:00.000000000 Z
11
+ date: 2019-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -100,42 +100,56 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '12.3'
103
+ version: '13.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '12.3'
110
+ version: '13.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rubocop
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - '='
116
116
  - !ruby/object:Gem::Version
117
- version: 0.74.0
117
+ version: 0.75.1
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - '='
123
123
  - !ruby/object:Gem::Version
124
- version: 0.74.0
124
+ version: 0.75.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-minitest
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '='
130
+ - !ruby/object:Gem::Version
131
+ version: 0.3.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '='
137
+ - !ruby/object:Gem::Version
138
+ version: 0.3.0
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: rubocop-performance
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - '='
130
144
  - !ruby/object:Gem::Version
131
- version: 1.4.1
145
+ version: 1.5.0
132
146
  type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
150
  - - '='
137
151
  - !ruby/object:Gem::Version
138
- version: 1.4.1
152
+ version: 1.5.0
139
153
  description:
140
154
  email:
141
155
  - opensource@mattbrictson.com
@@ -221,6 +235,8 @@ files:
221
235
  - lib/tomo/plugin/nodenv.rb
222
236
  - lib/tomo/plugin/nodenv/tasks.rb
223
237
  - lib/tomo/plugin/puma.rb
238
+ - lib/tomo/plugin/puma/systemd/service.erb
239
+ - lib/tomo/plugin/puma/systemd/socket.erb
224
240
  - lib/tomo/plugin/puma/tasks.rb
225
241
  - lib/tomo/plugin/rails.rb
226
242
  - lib/tomo/plugin/rails/helpers.rb
@@ -274,12 +290,14 @@ files:
274
290
  - lib/tomo/testing/host_extensions.rb
275
291
  - lib/tomo/testing/local.rb
276
292
  - lib/tomo/testing/log_capturing.rb
293
+ - lib/tomo/testing/loginctl.sh
277
294
  - lib/tomo/testing/mock_plugin_tester.rb
278
295
  - lib/tomo/testing/mocked_exec_error.rb
279
296
  - lib/tomo/testing/mocked_exit_error.rb
280
297
  - lib/tomo/testing/plugin_tester.rb
281
298
  - lib/tomo/testing/remote_extensions.rb
282
299
  - lib/tomo/testing/ssh_extensions.rb
300
+ - lib/tomo/testing/systemctl.rb
283
301
  - lib/tomo/testing/tomo_test_ed25519
284
302
  - lib/tomo/testing/tomo_test_ed25519.pub
285
303
  - lib/tomo/testing/ubuntu_setup.sh