tomo 0.8.1 → 0.9.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.
- checksums.yaml +4 -4
- data/README.md +1 -15
- data/lib/tomo/plugin/bundler.rb +1 -1
- data/lib/tomo/plugin/core.rb +6 -6
- data/lib/tomo/plugin/env.rb +1 -1
- data/lib/tomo/plugin/git.rb +1 -1
- data/lib/tomo/plugin/puma/systemd/service.erb +22 -0
- data/lib/tomo/plugin/puma/systemd/socket.erb +13 -0
- data/lib/tomo/plugin/puma/tasks.rb +96 -35
- data/lib/tomo/plugin/puma.rb +10 -4
- data/lib/tomo/plugin/rails/tasks.rb +1 -0
- data/lib/tomo/runtime/settings_interpolation.rb +45 -3
- data/lib/tomo/runtime.rb +3 -3
- data/lib/tomo/templates/config.rb.erb +3 -1
- data/lib/tomo/testing/Dockerfile +4 -0
- data/lib/tomo/testing/docker_image.rb +10 -2
- data/lib/tomo/testing/loginctl.sh +6 -0
- data/lib/tomo/testing/systemctl.rb +187 -0
- data/lib/tomo/version.rb +1 -1
- metadata +26 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67815fa19818dc29ff1226179db8b2b364c4843eb03c3a507561dd105b480b9f
|
4
|
+
data.tar.gz: 7119dc681c2e8e167712a1ee2a35bb7725d16f4f14b0438ec036abd7f30d42e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
data/lib/tomo/plugin/bundler.rb
CHANGED
@@ -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: "
|
15
|
+
bundler_path: "%{shared_path}/bundle",
|
16
16
|
bundler_retry: "3",
|
17
17
|
bundler_version: nil,
|
18
18
|
bundler_without: %w[development test]
|
data/lib/tomo/plugin/core.rb
CHANGED
@@ -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: "
|
15
|
-
deploy_to: "/var/www
|
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: "
|
21
|
-
releases_path: "
|
22
|
-
revision_log_path: "
|
23
|
-
shared_path: "
|
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
|
data/lib/tomo/plugin/env.rb
CHANGED
data/lib/tomo/plugin/git.rb
CHANGED
@@ -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: "
|
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
|
-
|
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
|
-
|
7
|
-
|
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
|
13
|
-
|
14
|
-
|
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
|
-
|
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
|
-
|
24
|
-
|
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
|
28
|
-
|
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
|
-
|
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
|
-
|
33
|
-
|
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
|
40
|
-
|
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
|
45
|
-
|
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
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
data/lib/tomo/plugin/puma.rb
CHANGED
@@ -6,9 +6,15 @@ module Tomo::Plugin
|
|
6
6
|
|
7
7
|
tasks Tomo::Plugin::Puma::Tasks
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
@@ -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(
|
28
|
-
|
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 "
|
88
|
-
when :new then "
|
89
|
-
when :tmp then "
|
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
|
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"
|
data/lib/tomo/testing/Dockerfile
CHANGED
@@ -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
|
-
|
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,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
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.
|
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-
|
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: '
|
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: '
|
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.
|
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.
|
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.
|
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.
|
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
|