stable-cli-rails 0.6.9 → 0.7.11
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/bin/stable +3 -2
- data/lib/stable/cli.rb +37 -630
- data/lib/stable/commands/doctor.rb +39 -0
- data/lib/stable/commands/list.rb +47 -0
- data/lib/stable/commands/new.rb +16 -0
- data/lib/stable/commands/remove.rb +24 -0
- data/lib/stable/commands/restart.rb +15 -0
- data/lib/stable/commands/setup.rb +11 -0
- data/lib/stable/commands/start.rb +15 -0
- data/lib/stable/commands/stop.rb +15 -0
- data/lib/stable/config/paths.rb +9 -0
- data/lib/stable/registry.rb +12 -2
- data/lib/stable/services/app_creator.rb +163 -0
- data/lib/stable/services/app_registry.rb +86 -0
- data/lib/stable/services/app_remover.rb +21 -0
- data/lib/stable/services/app_restarter.rb +32 -0
- data/lib/stable/services/app_starter.rb +95 -0
- data/lib/stable/services/app_stopper.rb +17 -0
- data/lib/stable/services/caddy_manager.rb +161 -0
- data/lib/stable/services/database/base.rb +47 -0
- data/lib/stable/services/database/mysql.rb +36 -0
- data/lib/stable/services/database/postgres.rb +14 -0
- data/lib/stable/services/dependency_checker.rb +59 -0
- data/lib/stable/services/hosts_manager.rb +31 -0
- data/lib/stable/services/process_manager.rb +61 -0
- data/lib/stable/services/ruby.rb +102 -0
- data/lib/stable/services/setup_runner.rb +82 -0
- data/lib/stable/system/shell.rb +13 -0
- data/lib/stable/utils/prompts.rb +21 -0
- data/lib/stable/validators/app_name.rb +49 -0
- data/lib/stable.rb +21 -1
- metadata +56 -14
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Commands
|
|
5
|
+
class Doctor
|
|
6
|
+
def call
|
|
7
|
+
puts 'Running Stable health checks...'
|
|
8
|
+
puts
|
|
9
|
+
|
|
10
|
+
checks = Services::DependencyChecker.new.run
|
|
11
|
+
|
|
12
|
+
checks.each do |check|
|
|
13
|
+
print_check(check)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
puts
|
|
17
|
+
summary(checks)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def print_check(check)
|
|
23
|
+
icon = check[:ok] ? '✔' : '✖'
|
|
24
|
+
puts "#{icon} #{check[:name]}"
|
|
25
|
+
puts " #{check[:message]}" unless check[:ok]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def summary(checks)
|
|
29
|
+
failures = checks.count { |c| !c[:ok] }
|
|
30
|
+
|
|
31
|
+
if failures.zero?
|
|
32
|
+
puts 'All checks passed.'
|
|
33
|
+
else
|
|
34
|
+
puts "#{failures} issue(s) detected. Fix the above and re-run `stable doctor`."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Commands
|
|
5
|
+
class List
|
|
6
|
+
def call
|
|
7
|
+
apps = Services::AppRegistry.all
|
|
8
|
+
|
|
9
|
+
if apps.empty?
|
|
10
|
+
puts 'No apps registered.'
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
print_header
|
|
15
|
+
|
|
16
|
+
apps.each do |app|
|
|
17
|
+
puts format_row(app)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def print_header
|
|
24
|
+
puts 'APP DOMAIN PORT RUBY STATUS '
|
|
25
|
+
puts '-' * 78
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def format_row(app)
|
|
29
|
+
status =
|
|
30
|
+
if app[:started_at]
|
|
31
|
+
'running'
|
|
32
|
+
else
|
|
33
|
+
'stopped'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
format(
|
|
37
|
+
'%-18s %-26s %-8s %-10s %-10s',
|
|
38
|
+
app[:name],
|
|
39
|
+
app[:domain],
|
|
40
|
+
app[:port],
|
|
41
|
+
app[:ruby],
|
|
42
|
+
status
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Commands
|
|
5
|
+
class Remove
|
|
6
|
+
def initialize(name)
|
|
7
|
+
@name = name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
app = Services::AppRegistry.find(@name)
|
|
12
|
+
abort 'App not found' unless app
|
|
13
|
+
|
|
14
|
+
Services::ProcessManager.stop(app)
|
|
15
|
+
Services::HostsManager.remove(app[:domain])
|
|
16
|
+
Services::CaddyManager.remove(app[:domain])
|
|
17
|
+
Services::AppRegistry.remove(@name)
|
|
18
|
+
Services::CaddyManager.reload
|
|
19
|
+
|
|
20
|
+
puts "Removed #{@name}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/stable/registry.rb
CHANGED
|
@@ -6,7 +6,7 @@ require 'fileutils'
|
|
|
6
6
|
module Stable
|
|
7
7
|
class Registry
|
|
8
8
|
def self.file_path
|
|
9
|
-
|
|
9
|
+
Stable::Paths.apps_file
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def self.save(apps)
|
|
@@ -15,7 +15,17 @@ module Stable
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def self.apps
|
|
18
|
-
File.exist?(file_path)
|
|
18
|
+
return [] unless File.exist?(file_path)
|
|
19
|
+
|
|
20
|
+
data = YAML.load_file(file_path) || []
|
|
21
|
+
data.map do |entry|
|
|
22
|
+
next entry unless entry.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
entry.each_with_object({}) do |(k, v), memo|
|
|
25
|
+
key = k.is_a?(String) ? k.to_sym : k
|
|
26
|
+
memo[key] = v
|
|
27
|
+
end
|
|
28
|
+
end
|
|
19
29
|
end
|
|
20
30
|
end
|
|
21
31
|
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Services
|
|
5
|
+
class AppCreator
|
|
6
|
+
def initialize(name, options)
|
|
7
|
+
@name = name
|
|
8
|
+
@options = options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
ruby = @options[:ruby] || RUBY_VERSION
|
|
13
|
+
port = @options[:port] || next_free_port
|
|
14
|
+
app_path = File.expand_path(@name)
|
|
15
|
+
domain = "#{@name}.test"
|
|
16
|
+
|
|
17
|
+
# --- Register app in registry ---
|
|
18
|
+
Services::AppRegistry.add(name: @name, path: app_path, domain: domain, port: port, ruby: ruby, started_at: nil,
|
|
19
|
+
pid: nil)
|
|
20
|
+
|
|
21
|
+
abort "Folder already exists: #{app_path}" if File.exist?(app_path)
|
|
22
|
+
|
|
23
|
+
# --- Ensure Ruby + gemset ---
|
|
24
|
+
Ruby.ensure_version(ruby)
|
|
25
|
+
Ruby.ensure_rvm!
|
|
26
|
+
System::Shell.run("bash -lc 'source #{Ruby.rvm_script} && rvm #{ruby}@#{@name} --create do true'")
|
|
27
|
+
|
|
28
|
+
rvm_cmd = Ruby.rvm_prefix(ruby, @name)
|
|
29
|
+
|
|
30
|
+
# --- Install Rails into gemset if missing ---
|
|
31
|
+
rails_version = @options[:rails]
|
|
32
|
+
rails_check_cmd = if rails_version
|
|
33
|
+
"#{rvm_cmd} gem list -i rails -v #{rails_version}"
|
|
34
|
+
else
|
|
35
|
+
"#{rvm_cmd} gem list -i rails"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
unless system("bash -lc '#{rails_check_cmd}'")
|
|
39
|
+
puts "Installing Rails #{rails_version || 'latest'} in gemset..."
|
|
40
|
+
install_cmd = rails_version ? "#{rvm_cmd} gem install rails -v #{rails_version}" : "#{rvm_cmd} gem install rails"
|
|
41
|
+
system("bash -lc '#{install_cmd}'") or abort('Failed to install Rails')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# --- Create Rails app ---
|
|
45
|
+
puts "Creating Rails app #{@name} (Ruby #{ruby})..."
|
|
46
|
+
System::Shell.run("bash -lc '#{rvm_cmd} rails new #{app_path}'")
|
|
47
|
+
|
|
48
|
+
# --- Write ruby version/gemset and bundle install inside gemset ---
|
|
49
|
+
Dir.chdir(app_path) do
|
|
50
|
+
File.write('.ruby-version', "#{ruby}\n")
|
|
51
|
+
File.write('.ruby-gemset', "#{@name}\n")
|
|
52
|
+
|
|
53
|
+
puts 'Running bundle install...'
|
|
54
|
+
System::Shell.run("bash -lc '#{rvm_cmd} bundle install --jobs=4 --retry=3'")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# --- Database integration ---
|
|
58
|
+
if @options[:db]
|
|
59
|
+
adapter = if @options[:mysql]
|
|
60
|
+
:mysql
|
|
61
|
+
else
|
|
62
|
+
:postgresql
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
gem_name = adapter == :postgresql ? 'pg' : 'mysql2'
|
|
66
|
+
gemfile_path = File.join(app_path, 'Gemfile')
|
|
67
|
+
unless File.read(gemfile_path).include?(gem_name)
|
|
68
|
+
File.open(gemfile_path, 'a') do |f|
|
|
69
|
+
f.puts "\n# Added by Stable CLI"
|
|
70
|
+
f.puts "gem '#{gem_name}'"
|
|
71
|
+
end
|
|
72
|
+
puts "✅ Added '#{gem_name}' gem to Gemfile"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# ensure gem is installed inside gemset
|
|
76
|
+
System::Shell.run("bash -lc 'cd #{app_path} && #{rvm_cmd} bundle install --jobs=4 --retry=3'")
|
|
77
|
+
|
|
78
|
+
# run adapter setup which will write database.yml and prepare
|
|
79
|
+
db_adapter = adapter == :mysql ? Database::MySQL : Database::Postgres
|
|
80
|
+
db_adapter.new(app_name: @name, app_path: app_path).setup
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# --- Refresh bundle and prepare DB (idempotent) ---
|
|
84
|
+
System::Shell.run("bash -lc 'cd #{app_path} && #{rvm_cmd} bundle check || #{rvm_cmd} bundle install'")
|
|
85
|
+
System::Shell.run("bash -lc 'cd #{app_path} && #{rvm_cmd} bundle exec rails db:prepare'")
|
|
86
|
+
|
|
87
|
+
# --- Hosts, certs, caddy ---
|
|
88
|
+
Services::HostsManager.add(domain)
|
|
89
|
+
Services::CaddyManager.add_app(@name, skip_ssl: @options[:skip_ssl])
|
|
90
|
+
Services::CaddyManager.ensure_running!
|
|
91
|
+
Services::CaddyManager.reload
|
|
92
|
+
|
|
93
|
+
# --- Start the app ---
|
|
94
|
+
puts "Starting Rails server for #{@name} on port #{port}..."
|
|
95
|
+
log_file = File.join(app_path, 'log', 'stable.log')
|
|
96
|
+
FileUtils.mkdir_p(File.dirname(log_file))
|
|
97
|
+
|
|
98
|
+
abort "Port #{port} is already in use. Choose another port." if port_in_use?(port)
|
|
99
|
+
|
|
100
|
+
pid = spawn('bash', '-lc', "cd \"#{app_path}\" && #{rvm_cmd} bundle exec rails s -p #{port} -b 127.0.0.1",
|
|
101
|
+
out: log_file, err: log_file)
|
|
102
|
+
Process.detach(pid)
|
|
103
|
+
|
|
104
|
+
AppRegistry.update(@name, started_at: Time.now.to_i, pid: pid)
|
|
105
|
+
|
|
106
|
+
sleep 1.5
|
|
107
|
+
wait_for_port(port)
|
|
108
|
+
prefix = @options[:skip_ssl] ? 'http' : 'https'
|
|
109
|
+
display_domain = if @options[:skip_ssl]
|
|
110
|
+
"#{domain}:#{port}"
|
|
111
|
+
else
|
|
112
|
+
domain
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
puts "✔ #{@name} running at #{prefix}://#{display_domain}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def create_rails_app
|
|
121
|
+
System::Shell.run("rails new #{@name}")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def setup_database
|
|
125
|
+
return unless @options[:mysql] || @options[:postgres]
|
|
126
|
+
|
|
127
|
+
adapter =
|
|
128
|
+
if @options[:mysql]
|
|
129
|
+
Database::MySQL
|
|
130
|
+
else
|
|
131
|
+
Database::Postgres
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
adapter.new(@name).setup
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def next_free_port
|
|
138
|
+
used_ports = Services::AppRegistry.all.map { |a| a[:port] }
|
|
139
|
+
port = 3000
|
|
140
|
+
port += 1 while used_ports.include?(port) || port_in_use?(port)
|
|
141
|
+
port
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def port_in_use?(port)
|
|
145
|
+
system("lsof -i tcp:#{port} > /dev/null 2>&1")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def wait_for_port(port, timeout: 20)
|
|
149
|
+
require 'socket'
|
|
150
|
+
start = Time.now
|
|
151
|
+
|
|
152
|
+
loop do
|
|
153
|
+
TCPSocket.new('127.0.0.1', port).close
|
|
154
|
+
return
|
|
155
|
+
rescue Errno::ECONNREFUSED
|
|
156
|
+
raise "Rails never bound port #{port}. Check log/stable.log" if Time.now - start > timeout
|
|
157
|
+
|
|
158
|
+
sleep 0.5
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Services
|
|
5
|
+
class AppRegistry
|
|
6
|
+
class << self
|
|
7
|
+
def all
|
|
8
|
+
Stable::Registry.apps
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def find(name)
|
|
12
|
+
Stable::Registry.apps.find { |a| a[:name] == name }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(name)
|
|
16
|
+
app = find(name)
|
|
17
|
+
abort("No app found with name #{name}") unless app
|
|
18
|
+
app
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Register a new app by name. If the folder already exists in the current
|
|
22
|
+
# working directory it will use that path. Port is allocated sequentially.
|
|
23
|
+
def register(name)
|
|
24
|
+
path = File.expand_path(name)
|
|
25
|
+
domain = "#{name}.test"
|
|
26
|
+
|
|
27
|
+
used_ports = Stable::Registry.apps.map { |a| a[:port] }.compact
|
|
28
|
+
port = (used_ports.max || 2999) + 1
|
|
29
|
+
|
|
30
|
+
app = {
|
|
31
|
+
name: name,
|
|
32
|
+
path: path,
|
|
33
|
+
domain: domain,
|
|
34
|
+
port: port,
|
|
35
|
+
ruby: nil,
|
|
36
|
+
started_at: nil,
|
|
37
|
+
pid: nil
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
add(app)
|
|
41
|
+
app
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def add(app)
|
|
45
|
+
apps = Stable::Registry.apps
|
|
46
|
+
apps.reject! { |a| a[:name] == app[:name] }
|
|
47
|
+
apps << app
|
|
48
|
+
Stable::Registry.save(apps)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Remove duplicate app entries (by name) and persist the canonical list
|
|
52
|
+
def dedupe
|
|
53
|
+
apps = Stable::Registry.apps
|
|
54
|
+
apps.uniq! { |a| a[:name] }
|
|
55
|
+
Stable::Registry.save(apps)
|
|
56
|
+
apps
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def remove(name)
|
|
60
|
+
apps = Stable::Registry.apps.reject { |a| a[:name] == name }
|
|
61
|
+
Stable::Registry.save(apps)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def update(name, attrs)
|
|
65
|
+
apps = Stable::Registry.apps
|
|
66
|
+
idx = apps.index { |a| a[:name] == name }
|
|
67
|
+
return unless idx
|
|
68
|
+
|
|
69
|
+
apps[idx] = apps[idx].merge(attrs)
|
|
70
|
+
Stable::Registry.save(apps)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def mark_stopped(name)
|
|
74
|
+
update(name, started_at: nil, pid: nil)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
alias register_app register
|
|
78
|
+
alias add_app add
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
module Stable
|
|
85
|
+
AppRegistry = Services::AppRegistry
|
|
86
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Services
|
|
5
|
+
class AppRemover
|
|
6
|
+
def initialize(name)
|
|
7
|
+
@name = name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
app = AppRegistry.fetch(@name)
|
|
12
|
+
ProcessManager.stop(app)
|
|
13
|
+
HostsManager.remove(app[:domain])
|
|
14
|
+
CaddyManager.remove(app[:domain])
|
|
15
|
+
AppRegistry.remove(@name)
|
|
16
|
+
CaddyManager.reload
|
|
17
|
+
puts "✔ #{@name} removed"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Services
|
|
5
|
+
class AppRestarter
|
|
6
|
+
def initialize(name)
|
|
7
|
+
@name = name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
app = Services::AppRegistry.find(@name)
|
|
12
|
+
unless app
|
|
13
|
+
puts("No app found with name #{@name}")
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Stop if running
|
|
18
|
+
if app[:pid]
|
|
19
|
+
begin
|
|
20
|
+
Process.kill('TERM', app[:pid].to_i)
|
|
21
|
+
rescue Errno::ESRCH
|
|
22
|
+
# already dead
|
|
23
|
+
end
|
|
24
|
+
AppRegistry.update(@name, started_at: nil, pid: nil)
|
|
25
|
+
puts "✔ #{@name} stopped"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Services::AppStarter.new(@name).call
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Services
|
|
5
|
+
class AppStarter
|
|
6
|
+
def initialize(name)
|
|
7
|
+
@name = name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
app = Services::AppRegistry.find(@name)
|
|
12
|
+
unless app
|
|
13
|
+
puts("No app found with name #{@name}")
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
port = app[:port] || next_free_port
|
|
18
|
+
ruby = app[:ruby]
|
|
19
|
+
path = app[:path]
|
|
20
|
+
|
|
21
|
+
if app_running?(app)
|
|
22
|
+
puts "#{@name} is already running on https://#{app[:domain]} (port #{port})"
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
gemset = Stable::Services::Ruby.gemset_for(app)
|
|
27
|
+
|
|
28
|
+
rvm_cmd =
|
|
29
|
+
if ruby && gemset
|
|
30
|
+
System::Shell.run("bash -lc 'source #{Stable::Services::Ruby.rvm_script} && rvm #{ruby}@#{gemset} --create do true'")
|
|
31
|
+
Stable::Services::Ruby.rvm_prefix(ruby, gemset)
|
|
32
|
+
elsif ruby
|
|
33
|
+
Stable::Services::Ruby.rvm_prefix(ruby, nil)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "Starting #{@name} on port #{port}..."
|
|
37
|
+
|
|
38
|
+
log_file = File.join(path, 'log', 'stable.log')
|
|
39
|
+
FileUtils.mkdir_p(File.dirname(log_file))
|
|
40
|
+
|
|
41
|
+
pid = spawn(
|
|
42
|
+
'bash',
|
|
43
|
+
'-lc',
|
|
44
|
+
"cd \"#{path}\" && #{rvm_cmd} bundle exec rails s -p #{port} -b 127.0.0.1",
|
|
45
|
+
out: log_file,
|
|
46
|
+
err: log_file
|
|
47
|
+
).to_i
|
|
48
|
+
|
|
49
|
+
Process.detach(pid)
|
|
50
|
+
|
|
51
|
+
wait_for_port(port, timeout: 30)
|
|
52
|
+
|
|
53
|
+
AppRegistry.update(app[:name], started_at: Time.now.to_i, pid: pid)
|
|
54
|
+
|
|
55
|
+
Stable::Services::CaddyManager.add_app(app[:name], skip_ssl: false)
|
|
56
|
+
Stable::Services::CaddyManager.reload
|
|
57
|
+
|
|
58
|
+
puts "#{@name} started on https://#{app[:domain]}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def next_free_port
|
|
64
|
+
used_ports = Services::AppRegistry.all.map { |a| a[:port] }
|
|
65
|
+
port = 3000
|
|
66
|
+
port += 1 while used_ports.include?(port) || port_in_use?(port)
|
|
67
|
+
port
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def port_in_use?(port)
|
|
71
|
+
system("lsof -i tcp:#{port} > /dev/null 2>&1")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def wait_for_port(port, timeout: 20)
|
|
75
|
+
require 'socket'
|
|
76
|
+
start = Time.now
|
|
77
|
+
|
|
78
|
+
loop do
|
|
79
|
+
TCPSocket.new('127.0.0.1', port).close
|
|
80
|
+
return
|
|
81
|
+
rescue Errno::ECONNREFUSED
|
|
82
|
+
raise "Rails never bound port #{port}. Check log/stable.log" if Time.now - start > timeout
|
|
83
|
+
|
|
84
|
+
sleep 0.5
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def app_running?(app)
|
|
89
|
+
return false unless app && app[:port]
|
|
90
|
+
|
|
91
|
+
system("lsof -i tcp:#{app[:port]} -sTCP:LISTEN > /dev/null 2>&1")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Services
|
|
5
|
+
class AppStopper
|
|
6
|
+
def initialize(name)
|
|
7
|
+
@app = AppRegistry.fetch(name)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
ProcessManager.stop(@app)
|
|
12
|
+
AppRegistry.mark_stopped(@app[:name])
|
|
13
|
+
puts "✔ #{@app[:name]} stopped"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|