Skip to content

Commit c581fb3

Browse files
committed
Daemon - improve exit routine
It turns out that calling exit from another thread whilst an at_exit block is executing can cause it to stop before completion. The whole point of the Daemon Booter is that the at_exit block is *always* executed to completion. To improve the chances of this, we now have a thread safe exit class which ensures that the at_exit block isn't interupted, executes only once and if it's already in the process of running, further calls to the exit method will just kill the current thread and *not* call Kernel.exit. Kernel.exit should only be called within the SafeExit class and nowhere else for this to behave correctly. To call exit we now need to use SafeExit#exit.
1 parent 8008017 commit c581fb3

File tree

1 file changed

+67
-16
lines changed

1 file changed

+67
-16
lines changed

app/server/ruby/bin/daemon.rb

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ module Daemon
110110
class Init
111111

112112
def initialize
113+
114+
@safe_exit = SafeExit.new do
115+
# Register exit routine
116+
# This will only be called once
117+
Util.log "Daemon Booter is now exiting."
118+
Util.log "Cleaning up any running processes..."
119+
cleanup_any_running_processes
120+
Util.log "Daemon Booter - Over and Out."
121+
Util.close_log
122+
end
123+
113124
# This is where the Daemon begins and ends.
114125
@scsynth_booter = nil
115126
@tau_booter = nil
@@ -120,13 +131,7 @@ def initialize
120131
Util.open_log
121132
Util.log "Welcome to the Daemon Booter"
122133

123-
at_exit do
124-
cleanup_any_running_processes
125-
Util.log "Daemon Booter - Over and Out."
126-
Util.close_log
127-
end
128-
129-
ports = PortDiscovery.new.ports
134+
ports = PortDiscovery.new(@safe_exit).ports
130135

131136
Util.log "Selected ports: "
132137
Util.log ports.inspect
@@ -135,7 +140,7 @@ def initialize
135140

136141
# Let the calling process (likely the GUI) know which port to
137142
# listen to and communicate on with the Ruby spider server via
138-
# STDOUT:
143+
# STDOUT.
139144
puts "#{ports["daemon-keep-alive"]} #{ports["gui-listen-to-server"]} #{ports["gui-send-to-server"]} #{ports["scsynth"]} #{ports["osc-cues"]}"
140145
STDOUT.flush
141146

@@ -158,7 +163,7 @@ def initialize
158163

159164
@spider_booter.wait if @spider_booter
160165
Util.log "Spider Server process has completed"
161-
end
166+
end
162167

163168

164169
# This is the Zombie Kill Switch
@@ -190,7 +195,7 @@ def spawn_zombie_kill_switch(port_num, &blk)
190195

191196
unless IO.select([keep_alive_server], nil, nil, connect_timeout)
192197
Util.log "Error. Unable to connect to GUI process on TCP port #{port_num}"
193-
exit
198+
@safe_exit.exit
194199
end
195200

196201
client = keep_alive_server.accept
@@ -202,18 +207,19 @@ def spawn_zombie_kill_switch(port_num, &blk)
202207
# For debug:
203208
# Util.log "RCV #{received_data}"
204209
end
205-
210+
rescue Errno::ECONNRESET
211+
Util.log "GUI forcibly closed the connection."
206212
rescue StandardError => e
207213
Util.log "Oh no, something went wrong reading keep alive messages from the GUI"
208214
Util.log "Error Class: #{e.class}"
209215
Util.log "Error Message: #{e.message}"
210216
Util.log "Error Backtrace: #{e.backtrace.inspect}"
211217
end
212218

213-
Util.log "Lost connection to server... shutting down..."
219+
Util.log "Shutting down..."
214220
client.close if client
215221
keep_alive_server.close if keep_alive_server
216-
exit
222+
@safe_exit.exit
217223
end
218224
end
219225

@@ -322,6 +328,50 @@ def self.os
322328
end
323329
end
324330

331+
class SafeExit
332+
333+
def initialize(&cleanup_procedure)
334+
335+
@exit_mut = Mutex.new
336+
@exit_cleanup_mut = Mutex.new
337+
@exit_in_progress = false
338+
@exit_cleanup_completed = false
339+
@cleanup_procedure = cleanup_procedure
340+
341+
at_exit do
342+
@exit_mut.synchronize do
343+
@exit_in_progress = true
344+
idempotent_exit_cleanup
345+
end
346+
end
347+
end
348+
349+
def exit
350+
Thread.current.kill if @exit_in_progress
351+
352+
@exit_mut.synchronize do
353+
if @exit_in_progress
354+
Thread.current.kill
355+
else
356+
@exit_in_progress = true
357+
idempotent_exit_cleanup
358+
Kernel.exit
359+
end
360+
end
361+
end
362+
363+
private
364+
365+
def idempotent_exit_cleanup
366+
@exit_cleanup_mut.synchronize do
367+
unless @exit_cleanup_completed
368+
@cleanup_procedure.call
369+
@exit_cleanup_completed = true
370+
end
371+
end
372+
end
373+
end
374+
325375

326376

327377
class ProcessBooter
@@ -694,7 +744,8 @@ class PortDiscovery
694744
"websocket" => :dynamic
695745
}.freeze
696746

697-
def initialize
747+
def initialize(safe_exit)
748+
@safe_exit = safe_exit
698749
# choose random port to try first
699750
@last_free_port = 49152 + rand(2000)
700751

@@ -735,7 +786,7 @@ def initialize
735786
port = find_free_port
736787
elsif default == :paired
737788
raise "Invalid port default for port: #{port_name}. This port can not be paired."
738-
exit
789+
@safe_exit.exit
739790
else
740791
port = default
741792
if(!check_port(port))
@@ -765,7 +816,7 @@ def check_port(port)
765816
def find_free_port
766817
while !check_port(@last_free_port += 1)
767818
if @last_free_port > 65535
768-
exit
819+
@safe_exit.exit
769820
end
770821
end
771822
@last_free_port

0 commit comments

Comments
 (0)