stable-cli-rails 0.8.0 → 0.8.1
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/lib/stable/cli.rb +1 -1
- data/lib/stable/commands/list.rb +36 -32
- data/lib/stable/services/app_creator.rb +1 -1
- data/lib/stable/services/app_registry.rb +25 -1
- data/lib/stable/services/app_starter.rb +27 -4
- data/lib/stable/services/process_manager.rb +49 -7
- data/lib/stable/utils/platform.rb +29 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55fbeca00cb3d5ac0cf360dd86a8022081e3bf8b00af321c8ac8dec7482e7e5b
|
|
4
|
+
data.tar.gz: a1d3fa2c7d8ffb2ae339e75d4ebdd1aa67e4b1e7b88ef0377ce8d06dc170c94d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e90d7e10d507ea30367e375dc99576f0b9902d77f9d7d2facf45e16fe116fff5eaac586eacec099b664b31f05f25dc33ec83db2d5b89926b0bb1fcf196588c8
|
|
7
|
+
data.tar.gz: 81a7739540c1b55e429867f7c7efd22af8a96559b239710fb290179c67d8ca67e1c6e5fc4448d3f6380cedf61b62fae4c1b737c5f98d532848c63652f2c05dcf
|
data/lib/stable/cli.rb
CHANGED
data/lib/stable/commands/list.rb
CHANGED
|
@@ -1,48 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../utils/platform'
|
|
4
|
+
|
|
3
5
|
module Stable
|
|
4
6
|
module Commands
|
|
5
7
|
# List command - displays all registered applications
|
|
6
8
|
class List
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
def call
|
|
10
|
+
apps = Services::AppRegistry.all
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
if apps.empty?
|
|
13
|
+
puts 'No apps registered.'
|
|
14
|
+
return
|
|
15
|
+
end
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
print_header
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
apps.each do |app|
|
|
20
|
+
# Determine status based on whether the app is actually running (port check)
|
|
21
|
+
status = app_running?(app) ? 'running' : 'stopped'
|
|
22
|
+
puts format_row(app, status)
|
|
20
23
|
end
|
|
24
|
+
end
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
private
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
puts '-' * 78
|
|
27
|
-
end
|
|
28
|
+
def app_running?(app)
|
|
29
|
+
return false unless app && app[:port]
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
# Check if something is listening on the app's port (cross-platform)
|
|
32
|
+
Stable::Utils::Platform.port_in_use?(app[:port])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def format_row(app, status)
|
|
36
|
+
format(
|
|
37
|
+
'%<name>-18s %<domain>-26s %<port>-8s %<ruby>-10s %<status>-10s',
|
|
38
|
+
name: app[:name],
|
|
39
|
+
domain: app[:domain],
|
|
40
|
+
port: app[:port],
|
|
41
|
+
ruby: app[:ruby],
|
|
42
|
+
status: status
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def print_header
|
|
47
|
+
puts 'APP DOMAIN PORT RUBY STATUS '
|
|
48
|
+
puts '-' * 78
|
|
49
|
+
end
|
|
46
50
|
end
|
|
47
51
|
end
|
|
48
52
|
end
|
|
@@ -55,7 +55,31 @@ module Stable
|
|
|
55
55
|
return unless app
|
|
56
56
|
|
|
57
57
|
updated_app = app.merge(attrs)
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
# Check if this is a legacy app (from apps.yml) or new format app
|
|
60
|
+
config_file = Stable::Paths.app_config_file(name)
|
|
61
|
+
if File.exist?(config_file)
|
|
62
|
+
# New format: update individual config file
|
|
63
|
+
Stable::Registry.save_app_config(name, updated_app)
|
|
64
|
+
else
|
|
65
|
+
# Legacy format: update apps.yml file
|
|
66
|
+
update_legacy_app(name, updated_app)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def update_legacy_app(name, updated_app)
|
|
71
|
+
legacy_file = Stable::Paths.apps_file
|
|
72
|
+
return unless File.exist?(legacy_file)
|
|
73
|
+
|
|
74
|
+
data = YAML.load_file(legacy_file) || []
|
|
75
|
+
idx = data.find_index { |app| app['name'] == name || app[:name] == name }
|
|
76
|
+
|
|
77
|
+
if idx
|
|
78
|
+
# Convert symbols to strings for YAML compatibility
|
|
79
|
+
legacy_format = updated_app.transform_keys(&:to_s)
|
|
80
|
+
data[idx] = legacy_format
|
|
81
|
+
File.write(legacy_file, data.to_yaml)
|
|
82
|
+
end
|
|
59
83
|
end
|
|
60
84
|
|
|
61
85
|
def mark_stopped(name)
|
|
@@ -21,6 +21,14 @@ module Stable
|
|
|
21
21
|
|
|
22
22
|
if app_running?(app)
|
|
23
23
|
puts "#{@name} is already running on https://#{app[:domain]} (port #{port})"
|
|
24
|
+
# Update the registry with the correct PID if it's missing
|
|
25
|
+
if !app[:pid] || !app[:started_at]
|
|
26
|
+
rails_pid = find_rails_pid(port)
|
|
27
|
+
if rails_pid
|
|
28
|
+
AppRegistry.update(app[:name], started_at: Time.now.to_i, pid: rails_pid)
|
|
29
|
+
puts "Updated registry with correct PID (#{rails_pid})"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
24
32
|
return
|
|
25
33
|
end
|
|
26
34
|
|
|
@@ -51,7 +59,9 @@ module Stable
|
|
|
51
59
|
|
|
52
60
|
wait_for_port(port, timeout: 30)
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
# Find the actual Rails process PID by checking what's listening on the port
|
|
63
|
+
rails_pid = find_rails_pid(port)
|
|
64
|
+
AppRegistry.update(app[:name], started_at: Time.now.to_i, pid: rails_pid)
|
|
55
65
|
|
|
56
66
|
Stable::Services::CaddyManager.add_app(app[:name], skip_ssl: false)
|
|
57
67
|
Stable::Services::CaddyManager.reload
|
|
@@ -69,7 +79,7 @@ module Stable
|
|
|
69
79
|
end
|
|
70
80
|
|
|
71
81
|
def port_in_use?(port)
|
|
72
|
-
|
|
82
|
+
Stable::Utils::Platform.port_in_use?(port)
|
|
73
83
|
end
|
|
74
84
|
|
|
75
85
|
def wait_for_port(port, timeout: 20)
|
|
@@ -87,9 +97,22 @@ module Stable
|
|
|
87
97
|
end
|
|
88
98
|
|
|
89
99
|
def app_running?(app)
|
|
90
|
-
return false unless app
|
|
100
|
+
return false unless app
|
|
101
|
+
|
|
102
|
+
# First check if we have PID info and if the process is alive
|
|
103
|
+
if app[:pid] && app[:started_at]
|
|
104
|
+
return ProcessManager.pid_alive?(app[:pid])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Fallback to port checking if no PID info available
|
|
108
|
+
return false unless app[:port]
|
|
109
|
+
|
|
110
|
+
Stable::Utils::Platform.port_in_use?(app[:port])
|
|
111
|
+
end
|
|
91
112
|
|
|
92
|
-
|
|
113
|
+
def find_rails_pid(port)
|
|
114
|
+
pids = Stable::Utils::Platform.find_pids_by_port(port)
|
|
115
|
+
pids.first
|
|
93
116
|
end
|
|
94
117
|
end
|
|
95
118
|
end
|
|
@@ -36,20 +36,23 @@ module Stable
|
|
|
36
36
|
|
|
37
37
|
Process.detach(pid)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
# Wait a moment for Rails to start, then find the actual Rails PID
|
|
40
|
+
sleep 2
|
|
41
|
+
rails_pid = find_rails_pid(app[:port])
|
|
40
42
|
|
|
41
|
-
pid
|
|
43
|
+
AppRegistry.update(app[:name], started_at: Time.now.to_i, pid: rails_pid || pid)
|
|
44
|
+
|
|
45
|
+
rails_pid || pid
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
def self.stop(app)
|
|
45
|
-
|
|
46
|
-
return unless pid
|
|
49
|
+
return unless app[:port]
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
if
|
|
51
|
+
pids = Stable::Utils::Platform.find_pids_by_port(app[:port])
|
|
52
|
+
if pids.empty?
|
|
50
53
|
puts "No app running on port #{app[:port]}"
|
|
51
54
|
else
|
|
52
|
-
|
|
55
|
+
pids.each { |pid| Process.kill('TERM', pid.to_i) rescue nil }
|
|
53
56
|
puts "Stopped #{app[:name]} on port #{app[:port]}"
|
|
54
57
|
end
|
|
55
58
|
|
|
@@ -57,6 +60,45 @@ module Stable
|
|
|
57
60
|
rescue Errno::ESRCH
|
|
58
61
|
AppRegistry.update(app[:name], started_at: nil, pid: nil)
|
|
59
62
|
end
|
|
63
|
+
|
|
64
|
+
# Check if a process with the given PID is still running
|
|
65
|
+
def self.pid_alive?(pid)
|
|
66
|
+
return false unless pid
|
|
67
|
+
|
|
68
|
+
# Use a cross-platform method to check if PID exists
|
|
69
|
+
if RUBY_PLATFORM =~ /mingw|mswin|win32/
|
|
70
|
+
# Windows: use tasklist
|
|
71
|
+
system("tasklist /FI \"PID eq #{pid}\" 2>NUL | find /I \"#{pid}\" >NUL")
|
|
72
|
+
else
|
|
73
|
+
# Unix-like systems: check /proc or use ps
|
|
74
|
+
begin
|
|
75
|
+
Process.kill(0, pid)
|
|
76
|
+
true
|
|
77
|
+
rescue Errno::ESRCH
|
|
78
|
+
false
|
|
79
|
+
rescue Errno::EPERM
|
|
80
|
+
# Process exists but we don't have permission to signal it
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Validate and clean up stale app statuses
|
|
87
|
+
def self.validate_app_statuses
|
|
88
|
+
apps = AppRegistry.all
|
|
89
|
+
apps.each do |app|
|
|
90
|
+
next unless app[:started_at] && app[:pid]
|
|
91
|
+
|
|
92
|
+
unless pid_alive?(app[:pid])
|
|
93
|
+
AppRegistry.update(app[:name], started_at: nil, pid: nil)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.find_rails_pid(port)
|
|
99
|
+
pids = Stable::Utils::Platform.find_pids_by_port(port)
|
|
100
|
+
pids.first
|
|
101
|
+
end
|
|
60
102
|
end
|
|
61
103
|
end
|
|
62
104
|
end
|
|
@@ -49,6 +49,35 @@ module Stable
|
|
|
49
49
|
Dir.home
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def port_in_use?(port)
|
|
53
|
+
case current
|
|
54
|
+
when :macos, :linux
|
|
55
|
+
# Use lsof on Unix-like systems
|
|
56
|
+
system("lsof -i tcp:#{port} -sTCP:LISTEN > /dev/null 2>&1")
|
|
57
|
+
when :windows
|
|
58
|
+
# Use netstat on Windows
|
|
59
|
+
system("netstat -an | findstr :#{port} > nul 2>&1")
|
|
60
|
+
else
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def find_pids_by_port(port)
|
|
66
|
+
case current
|
|
67
|
+
when :macos, :linux
|
|
68
|
+
# Use lsof to find PIDs listening on the port
|
|
69
|
+
output = `lsof -i tcp:#{port} -sTCP:LISTEN -t 2>/dev/null`.strip
|
|
70
|
+
return [] if output.empty?
|
|
71
|
+
output.split("\n").map(&:to_i)
|
|
72
|
+
when :windows
|
|
73
|
+
# On Windows, this is more complex. For now, return empty array
|
|
74
|
+
# Could potentially parse netstat output in the future
|
|
75
|
+
[]
|
|
76
|
+
else
|
|
77
|
+
[]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
52
81
|
private
|
|
53
82
|
|
|
54
83
|
def detect_linux_package_manager
|