siding 0.0.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 +7 -0
- data/CLAUDE.md +248 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +182 -0
- data/Rakefile +51 -0
- data/exe/siding +5 -0
- data/lib/siding/boot_component.rb +18 -0
- data/lib/siding/cli.rb +459 -0
- data/lib/siding/client.rb +417 -0
- data/lib/siding/error.rb +5 -0
- data/lib/siding/invocation.rb +35 -0
- data/lib/siding/life_cycle.rb +114 -0
- data/lib/siding/load_manifest.rb +475 -0
- data/lib/siding/logger.rb +89 -0
- data/lib/siding/platform.rb +37 -0
- data/lib/siding/project_key.rb +64 -0
- data/lib/siding/protocol.rb +154 -0
- data/lib/siding/restarter.rb +205 -0
- data/lib/siding/runtime.rb +165 -0
- data/lib/siding/server.rb +398 -0
- data/lib/siding/staleness.rb +191 -0
- data/lib/siding/version.rb +5 -0
- data/lib/siding/watch.rb +115 -0
- data/lib/siding/worker.rb +266 -0
- data/lib/siding.rb +27 -0
- metadata +97 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
require_relative "error"
|
|
7
|
+
|
|
8
|
+
module Siding
|
|
9
|
+
module Protocol
|
|
10
|
+
VERSION = 1
|
|
11
|
+
|
|
12
|
+
LENGTH_BYTES = 4
|
|
13
|
+
LENGTH_FORMAT = "N"
|
|
14
|
+
|
|
15
|
+
MAX_MESSAGE_BYTES = 1024 * 1024
|
|
16
|
+
|
|
17
|
+
# Client -> server.
|
|
18
|
+
HELLO = "Hello"
|
|
19
|
+
RUN = "Run"
|
|
20
|
+
SIGNAL = "Signal"
|
|
21
|
+
STATUS = "Status"
|
|
22
|
+
STOP = "Stop"
|
|
23
|
+
|
|
24
|
+
# Server -> client.
|
|
25
|
+
WELCOME = "Welcome"
|
|
26
|
+
VERSION_MISMATCH = "VersionMismatch"
|
|
27
|
+
BOOTING = "Booting"
|
|
28
|
+
BOOT_FAILED = "BootFailed"
|
|
29
|
+
STARTED = "Started"
|
|
30
|
+
FINISHED = "Finished"
|
|
31
|
+
STATUS_REPORT = "StatusReport"
|
|
32
|
+
|
|
33
|
+
class ProtocolError < Error; end
|
|
34
|
+
class TruncatedMessage < ProtocolError; end
|
|
35
|
+
class MessageTooLarge < ProtocolError; end
|
|
36
|
+
|
|
37
|
+
class VersionMismatch < ProtocolError
|
|
38
|
+
attr_reader :server_version, :client_version
|
|
39
|
+
|
|
40
|
+
def initialize(server_version:, client_version: VERSION)
|
|
41
|
+
@server_version = server_version
|
|
42
|
+
@client_version = client_version
|
|
43
|
+
super("server speaks protocol version #{server_version}, this client speaks #{client_version}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Message = Struct.new(:type, :payload, keyword_init: true) do
|
|
48
|
+
def [](key) = payload[key]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
def write_message(io, type, payload = {})
|
|
54
|
+
body = JSON.generate({ "type" => type }.merge(stringify(payload)))
|
|
55
|
+
raise MessageTooLarge, "message of #{body.bytesize} bytes exceeds #{MAX_MESSAGE_BYTES}" if body.bytesize > MAX_MESSAGE_BYTES
|
|
56
|
+
|
|
57
|
+
# Length and body in a single write. Two writes would give a reader a window in which a
|
|
58
|
+
# crash leaves a valid-looking prefix with nothing behind it -- exactly the state the
|
|
59
|
+
# framing is here to make unrepresentable.
|
|
60
|
+
io.write([body.bytesize].pack(LENGTH_FORMAT) + body)
|
|
61
|
+
io.flush if io.respond_to?(:flush)
|
|
62
|
+
body.bytesize
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def read_message(io)
|
|
66
|
+
header = read_exactly(io, LENGTH_BYTES, allow_eof: true)
|
|
67
|
+
return nil if header.nil?
|
|
68
|
+
|
|
69
|
+
length = header.unpack1(LENGTH_FORMAT)
|
|
70
|
+
raise MessageTooLarge, "peer announced #{length} bytes, over the #{MAX_MESSAGE_BYTES} limit" if
|
|
71
|
+
length > MAX_MESSAGE_BYTES
|
|
72
|
+
|
|
73
|
+
body = read_exactly(io, length)
|
|
74
|
+
parsed = begin
|
|
75
|
+
JSON.parse(body)
|
|
76
|
+
rescue JSON::ParserError => e
|
|
77
|
+
raise ProtocolError, "malformed message body: #{e.message}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
raise ProtocolError, "message has no type" unless parsed.is_a?(Hash) && parsed["type"]
|
|
81
|
+
|
|
82
|
+
Message.new(type: parsed.delete("type"), payload: parsed)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def read_exactly(io, count, allow_eof: false)
|
|
86
|
+
return +"" if count.zero?
|
|
87
|
+
|
|
88
|
+
buffer = +""
|
|
89
|
+
while buffer.bytesize < count
|
|
90
|
+
chunk = read_some(io, count - buffer.bytesize)
|
|
91
|
+
if chunk.nil? || chunk.empty?
|
|
92
|
+
return nil if buffer.empty? && allow_eof
|
|
93
|
+
|
|
94
|
+
raise TruncatedMessage, "peer sent #{buffer.bytesize} of #{count} bytes before closing"
|
|
95
|
+
end
|
|
96
|
+
buffer << chunk
|
|
97
|
+
end
|
|
98
|
+
buffer
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def read_some(io, count)
|
|
102
|
+
io.respond_to?(:recv) ? io.recv(count) : io.read(count)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def stringify(payload)
|
|
106
|
+
payload.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
STREAM_ORDER = %i[stdin stdout stderr].freeze
|
|
110
|
+
|
|
111
|
+
def send_streams(io, stdin: $stdin, stdout: $stdout, stderr: $stderr)
|
|
112
|
+
[stdin, stdout, stderr].each { |stream| io.send_io(stream) }
|
|
113
|
+
STREAM_ORDER
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def receive_streams(io)
|
|
117
|
+
STREAM_ORDER.each_with_object({}) { |name, streams| streams[name] = io.recv_io }
|
|
118
|
+
rescue EOFError, SocketError, SystemCallError => e
|
|
119
|
+
# A peer that dies mid-pass surfaces as `SocketError` ("file descriptor was not passed")
|
|
120
|
+
# rather than as EOF, because the control message arrives empty rather than not at all.
|
|
121
|
+
# Both mean the same thing here: the descriptors we were promised are not coming.
|
|
122
|
+
raise TruncatedMessage, "peer closed while passing descriptors: #{e.message}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def client_handshake(io)
|
|
126
|
+
write_message(io, HELLO, protocol_version: VERSION)
|
|
127
|
+
reply = read_message(io)
|
|
128
|
+
raise TruncatedMessage, "server closed during handshake" if reply.nil?
|
|
129
|
+
|
|
130
|
+
case reply.type
|
|
131
|
+
when WELCOME
|
|
132
|
+
reply["protocol_version"]
|
|
133
|
+
when VERSION_MISMATCH
|
|
134
|
+
raise VersionMismatch.new(server_version: reply["protocol_version"])
|
|
135
|
+
else
|
|
136
|
+
raise ProtocolError, "unexpected #{reply.type.inspect} in response to #{HELLO}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def server_handshake(io)
|
|
141
|
+
hello = read_message(io)
|
|
142
|
+
raise TruncatedMessage, "client closed during handshake" if hello.nil?
|
|
143
|
+
raise ProtocolError, "expected #{HELLO}, got #{hello.type.inspect}" unless hello.type == HELLO
|
|
144
|
+
|
|
145
|
+
if hello["protocol_version"] == VERSION
|
|
146
|
+
write_message(io, WELCOME, protocol_version: VERSION)
|
|
147
|
+
true
|
|
148
|
+
else
|
|
149
|
+
write_message(io, VERSION_MISMATCH, protocol_version: VERSION)
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "staleness"
|
|
4
|
+
require_relative "watch"
|
|
5
|
+
|
|
6
|
+
module Siding
|
|
7
|
+
class Restarter
|
|
8
|
+
MIN_INTERVAL = 3.0
|
|
9
|
+
MAX_INTERVAL = 60.0
|
|
10
|
+
IDLE_FRACTION = 0.1
|
|
11
|
+
SETTLE = 1.0
|
|
12
|
+
HANDOVER_INTERVAL = 0.5
|
|
13
|
+
|
|
14
|
+
QUIET = :quiet
|
|
15
|
+
SETTLING = :settling
|
|
16
|
+
BOOT = :boot
|
|
17
|
+
SUPERSEDED = :superseded
|
|
18
|
+
|
|
19
|
+
attr_reader :manifest, :project_key, :runtime, :logger
|
|
20
|
+
|
|
21
|
+
def initialize(manifest:, project_key:, runtime:, logger:, env: ENV, clock: nil, busy: nil, superseded: nil, on_superseded: nil, spawner: nil)
|
|
22
|
+
@manifest = manifest
|
|
23
|
+
@project_key = project_key
|
|
24
|
+
@runtime = runtime
|
|
25
|
+
@logger = logger
|
|
26
|
+
@env = env
|
|
27
|
+
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
28
|
+
@busy = busy || -> { false }
|
|
29
|
+
@superseded = superseded || -> { false }
|
|
30
|
+
@on_superseded = on_superseded
|
|
31
|
+
@spawner = spawner
|
|
32
|
+
@gate = Mutex.new
|
|
33
|
+
@signal = Thread::Queue.new
|
|
34
|
+
@running = false
|
|
35
|
+
@last_activity = @clock.call
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def start
|
|
39
|
+
return self if @thread
|
|
40
|
+
|
|
41
|
+
@watch = start_watch
|
|
42
|
+
|
|
43
|
+
@running = true
|
|
44
|
+
@thread = Thread.new { poll_loop }
|
|
45
|
+
@thread.name = "siding-restarter" if @thread.respond_to?(:name=)
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stop
|
|
50
|
+
thread = @thread
|
|
51
|
+
@running = false
|
|
52
|
+
@thread = nil
|
|
53
|
+
@watch&.stop
|
|
54
|
+
return self if thread.nil?
|
|
55
|
+
|
|
56
|
+
@signal << :stop
|
|
57
|
+
thread.join(1) || thread.kill
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def watch_mode
|
|
62
|
+
@watch ? @watch.mode_label : "poll"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def busy!
|
|
66
|
+
@last_activity = @clock.call
|
|
67
|
+
end
|
|
68
|
+
alias idle! busy!
|
|
69
|
+
|
|
70
|
+
def idle_seconds(now = @clock.call)
|
|
71
|
+
now - @last_activity
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def interval_for(idle)
|
|
75
|
+
[[idle * IDLE_FRACTION, MIN_INTERVAL].max, MAX_INTERVAL].min
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def quiesce
|
|
79
|
+
@gate.synchronize { yield }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def around_fork
|
|
83
|
+
quiesce do
|
|
84
|
+
@watch&.stop
|
|
85
|
+
yield
|
|
86
|
+
ensure
|
|
87
|
+
@watch = start_watch if @running
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def poll(now: @clock.call)
|
|
92
|
+
decision = tick(now: now)
|
|
93
|
+
act(decision)
|
|
94
|
+
decision
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def tick(now: @clock.call)
|
|
98
|
+
return SUPERSEDED if @superseded.call
|
|
99
|
+
return QUIET if @busy.call
|
|
100
|
+
|
|
101
|
+
verdict = Staleness.validate(manifest, env: @env)
|
|
102
|
+
return settled(QUIET) if verdict.fresh?
|
|
103
|
+
|
|
104
|
+
settle(verdict.revision_label, now)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def replacement_pid
|
|
108
|
+
pid = @replacement_pid
|
|
109
|
+
return nil if pid.nil?
|
|
110
|
+
return pid if alive?(pid)
|
|
111
|
+
|
|
112
|
+
@replacement_pid = nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def start_watch
|
|
118
|
+
Watch.start(manifest:, env: @env, logger:) do
|
|
119
|
+
@signal << :changed if @signal.empty?
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def poll_loop
|
|
124
|
+
while @running
|
|
125
|
+
wait(wait_interval)
|
|
126
|
+
break unless @running
|
|
127
|
+
|
|
128
|
+
@last_decision = quiesce { poll }
|
|
129
|
+
end
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
logger.debug("restarter stopped: #{e.class}: #{e.message}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def wait(seconds)
|
|
135
|
+
@signal.pop(timeout: seconds)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def wait_interval
|
|
139
|
+
return SETTLE if @last_decision == SETTLING
|
|
140
|
+
return HANDOVER_INTERVAL if replacement_pid
|
|
141
|
+
return MAX_INTERVAL if @watch&.watching?
|
|
142
|
+
|
|
143
|
+
interval_for(idle_seconds)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def act(decision)
|
|
147
|
+
case decision
|
|
148
|
+
when BOOT then spawn_replacement
|
|
149
|
+
when SUPERSEDED then withdraw
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def settle(label, now)
|
|
154
|
+
if label != @pending_label
|
|
155
|
+
@pending_label = label
|
|
156
|
+
@pending_since = now
|
|
157
|
+
return SETTLING
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
return SETTLING if now - @pending_since < SETTLE
|
|
161
|
+
return QUIET if label == @attempted_label
|
|
162
|
+
|
|
163
|
+
@attempted_label = label
|
|
164
|
+
BOOT
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def settled(decision)
|
|
168
|
+
@pending_label = nil
|
|
169
|
+
@pending_since = nil
|
|
170
|
+
decision
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def spawn_replacement
|
|
174
|
+
require_relative "server"
|
|
175
|
+
|
|
176
|
+
@replacement_pid = spawner.call
|
|
177
|
+
reaper = Process.detach(@replacement_pid) if @replacement_pid
|
|
178
|
+
# A reaper that finds the pid was never ours has nothing to report to anyone's terminal.
|
|
179
|
+
reaper&.report_on_exception = false
|
|
180
|
+
logger.debug("restarter booting #{@pending_label} as #{@replacement_pid}")
|
|
181
|
+
@replacement_pid
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
logger.debug("restarter could not boot a replacement: #{e.message}")
|
|
184
|
+
@replacement_pid = nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def spawner
|
|
188
|
+
@spawner || -> { Server.spawn(project_key:, runtime:, env: @env) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def withdraw
|
|
192
|
+
@running = false
|
|
193
|
+
@on_superseded&.call
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def alive?(pid)
|
|
197
|
+
Process.kill(0, pid)
|
|
198
|
+
true
|
|
199
|
+
rescue Errno::ESRCH
|
|
200
|
+
false
|
|
201
|
+
rescue SystemCallError
|
|
202
|
+
true
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "platform"
|
|
7
|
+
|
|
8
|
+
module Siding
|
|
9
|
+
class Runtime
|
|
10
|
+
DIRECTORY_MODE = 0o700
|
|
11
|
+
|
|
12
|
+
NAMESPACE = "siding"
|
|
13
|
+
|
|
14
|
+
# Used when XDG_RUNTIME_DIR is unset -- common on macOS, where there is no such convention.
|
|
15
|
+
# `~/.local/state` is the XDG base-directory spec's home for state that should persist between
|
|
16
|
+
# restarts but is not configuration.
|
|
17
|
+
FALLBACK_ROOT = File.join(".local", "state", NAMESPACE)
|
|
18
|
+
|
|
19
|
+
class Unavailable < Error
|
|
20
|
+
attr_reader :path
|
|
21
|
+
|
|
22
|
+
def initialize(message, path: nil)
|
|
23
|
+
@path = path
|
|
24
|
+
super(message)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :project_key, :root
|
|
29
|
+
|
|
30
|
+
def self.for(project_key, env: ENV)
|
|
31
|
+
new(project_key: project_key, root: root_for(env))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.root_for(env)
|
|
35
|
+
xdg = env["XDG_RUNTIME_DIR"]
|
|
36
|
+
return File.join(xdg, NAMESPACE) if xdg && !xdg.empty?
|
|
37
|
+
|
|
38
|
+
File.join(Dir.home, FALLBACK_ROOT)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(project_key:, root:)
|
|
42
|
+
@project_key = project_key
|
|
43
|
+
@root = root
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def dir = File.join(root, project_key.digest)
|
|
47
|
+
def socket_path = File.join(dir, "sock")
|
|
48
|
+
def lock_path = File.join(dir, "lock")
|
|
49
|
+
def server_info_path = File.join(dir, "server.json")
|
|
50
|
+
def boot_log_path = File.join(dir, "boot.log")
|
|
51
|
+
def events_path = File.join(dir, "events.jsonl")
|
|
52
|
+
def log_path = File.join(dir, "siding.log")
|
|
53
|
+
|
|
54
|
+
def server_info
|
|
55
|
+
info = JSON.parse(File.read(server_info_path))
|
|
56
|
+
info.is_a?(Hash) ? info : nil
|
|
57
|
+
rescue SystemCallError, JSON::ParserError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def live_server_info
|
|
62
|
+
info = server_info
|
|
63
|
+
return nil if info.nil?
|
|
64
|
+
|
|
65
|
+
pid = info["pid"]
|
|
66
|
+
return nil unless pid.is_a?(Integer) && self.class.process_alive?(pid)
|
|
67
|
+
|
|
68
|
+
info
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def server_pid = live_server_info&.[]("pid")
|
|
72
|
+
|
|
73
|
+
def discard_socket
|
|
74
|
+
File.unlink(socket_path)
|
|
75
|
+
true
|
|
76
|
+
rescue SystemCallError
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def discard_records
|
|
81
|
+
[socket_path, server_info_path].each do |path|
|
|
82
|
+
File.unlink(path)
|
|
83
|
+
rescue SystemCallError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.process_alive?(pid)
|
|
90
|
+
Process.kill(0, pid)
|
|
91
|
+
true
|
|
92
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
93
|
+
false
|
|
94
|
+
rescue SystemCallError
|
|
95
|
+
# Anything else means the question could not be asked, and a live process wrongly called dead
|
|
96
|
+
# would have us boot a second server over a working one.
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def prepare
|
|
101
|
+
check_socket_path_length
|
|
102
|
+
ensure_directory(root)
|
|
103
|
+
ensure_directory(dir)
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def prepared?
|
|
108
|
+
File.directory?(dir) && safe_directory?(dir) && socket_path_within_limit?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def unavailable_reason
|
|
112
|
+
prepare
|
|
113
|
+
nil
|
|
114
|
+
rescue Unavailable => e
|
|
115
|
+
e.message
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def socket_path_within_limit?
|
|
119
|
+
socket_path.bytesize <= Platform::UNIX_SOCKET_PATH_LIMIT - 1
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def ensure_directory(path)
|
|
125
|
+
FileUtils.mkdir_p(path, mode: DIRECTORY_MODE)
|
|
126
|
+
File.chmod(DIRECTORY_MODE, path)
|
|
127
|
+
verify_ownership(path)
|
|
128
|
+
rescue SystemCallError => e
|
|
129
|
+
raise Unavailable.new("cannot use runtime directory #{path}: #{e.message}", path: path)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def verify_ownership(path)
|
|
133
|
+
stat = File.stat(path)
|
|
134
|
+
unless stat.uid == Process.uid
|
|
135
|
+
raise Unavailable.new(
|
|
136
|
+
"runtime directory #{path} is owned by uid #{stat.uid}, not #{Process.uid}", path: path
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return if (stat.mode & 0o077).zero?
|
|
141
|
+
|
|
142
|
+
raise Unavailable.new(
|
|
143
|
+
"runtime directory #{path} is accessible to other users (mode #{format('%o', stat.mode & 0o777)})",
|
|
144
|
+
path: path
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def safe_directory?(path)
|
|
149
|
+
stat = File.stat(path)
|
|
150
|
+
stat.uid == Process.uid && (stat.mode & 0o077).zero?
|
|
151
|
+
rescue SystemCallError
|
|
152
|
+
false
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def check_socket_path_length
|
|
156
|
+
return if socket_path_within_limit?
|
|
157
|
+
|
|
158
|
+
raise Unavailable.new(
|
|
159
|
+
"socket path #{socket_path} is #{socket_path.bytesize} bytes, over the " \
|
|
160
|
+
"#{Platform::UNIX_SOCKET_PATH_LIMIT - 1}-byte limit for Unix domain sockets",
|
|
161
|
+
path: socket_path
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|