feat(reboot): split scsynth vs sclang reboot
Context: previous commit's REBOOT button only restarted scsynth (the
audio engine), keeping the sclang interpreter alive with all its
loaded modules. The user clarified they want a real 'reboot sclang' :
kill the whole interpreter and reload everything from boot.scd. That
needs cooperation between the web bridge (which can only reach sclang)
and the launcher (which controls the sclang process).
Approach: a sentinel-file pattern lets the bridge signal the launcher
without sharing a port. The bridge handler /control/rebootSclang
writes ~/.av-live-restart-sclang then pkills sclang. The launcher's
new sentinel watcher fires every 1s ; on detection it removes the
sentinel and calls restartSclang(), which sets a wantsRestart flag
and terminates the current process — the existing terminationHandler
sees the flag and respawns sclang 0.6s later (so scsynth can be
reaped). Two distinct buttons in the topbar : ⟳ scsynth (small,
existing /control/rebootServer) and ⟳ sclang (big, new path).
Changes:
- web_bridge.scd : new /control/rebootSclang OSCdef writes the
sentinel file then unixCmd's pkill -TERM -f 'sclang.*boot.scd'
after a 0.5s delay
- launcher/ProcessManager.swift :
* sclangWantsRestart flag
* restartSclang() : sets the flag, calls stopSclang, re-sets the
flag (since stopSclang clears it for normal stops)
* startSentinelWatcher() : 1s Timer that checks for the sentinel
file in /Users/electron, removes it and calls restartSclang() on hit
* maybeRestartSclang() : called from terminationHandler, respawns
if wantsRestart is set, with 0.6s delay so scsynth pkill finishes
- launcher/AVLiveLauncherApp.swift : startSentinelWatcher() invoked
in applicationDidFinishLaunching
- web/public/control/index.html :
* #btnReboot split into #btnRebootScsynth (small ⟳ scsynth) and
#btnRebootSclang (full ⟳ sclang)
- web/public/control/app.js : two makeArmedButton bindings, one per
reboot path
- web/public/control/style.css : .btn.warn.small variant for the
scsynth button
Impact: ⟳ sclang now does a full engine restart (kills sclang,
launcher respawns it, ~albums and all wrappers reload). ⟳ scsynth
keeps the lighter path that just bounces the audio server. Both
follow the same two-click armed pattern as TEARDOWN.
This commit is contained in:
@@ -55,6 +55,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
self?.processManager.startAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Watch the sentinel file written by the web bridge's
|
||||
// /control/rebootSclang handler — restarts sclang when touched.
|
||||
processManager.startSentinelWatcher()
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
|
||||
@@ -30,6 +30,7 @@ final class ProcessManager: ObservableObject {
|
||||
private var sclangProc: Process?
|
||||
private var oscopeProc: Process?
|
||||
private var webProc: Process?
|
||||
private var sclangWantsRestart = false
|
||||
let osc = OSCSender(host: "127.0.0.1", port: 57121)
|
||||
private let logQueue = DispatchQueue(label: "cc.saillant.avlive.log")
|
||||
private let maxLogLines = 2000
|
||||
@@ -264,6 +265,7 @@ final class ProcessManager: ObservableObject {
|
||||
DispatchQueue.main.async {
|
||||
self?.sclangProc = nil
|
||||
self?.sclangRunning = false
|
||||
self?.maybeRestartSclang()
|
||||
}
|
||||
}
|
||||
append(source: "launcher", text: "started sclang \(sclangPath) \(soundAlgoLoadFile)")
|
||||
@@ -273,6 +275,7 @@ final class ProcessManager: ObservableObject {
|
||||
}
|
||||
|
||||
func stopSclang() {
|
||||
sclangWantsRestart = false
|
||||
sclangProc?.terminate()
|
||||
// sclang's terminate doesn't always tear scsynth down cleanly —
|
||||
// run a belt-and-braces pkill so the next boot can grab :57110.
|
||||
@@ -282,6 +285,43 @@ final class ProcessManager: ObservableObject {
|
||||
try? kill.run()
|
||||
}
|
||||
|
||||
/// Restart sclang : fire and forget. Sets the auto-restart flag,
|
||||
/// terminates the current process. terminationHandler observes the
|
||||
/// flag and re-spawns after scsynth is reaped.
|
||||
func restartSclang() {
|
||||
guard sclangProc != nil else { startSclang(); return }
|
||||
append(source: "launcher", text: "restarting sclang…")
|
||||
sclangWantsRestart = true
|
||||
stopSclang()
|
||||
// stopSclang resets the flag — re-set it because we want restart
|
||||
sclangWantsRestart = true
|
||||
}
|
||||
|
||||
/// Watches the .av-live-restart-sclang sentinel file. Triggered by
|
||||
/// the bridge's /control/rebootSclang OSC handler.
|
||||
private var sentinelTimer: Timer?
|
||||
func startSentinelWatcher() {
|
||||
let path = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".av-live-restart-sclang").path
|
||||
sentinelTimer?.invalidate()
|
||||
sentinelTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
try? FileManager.default.removeItem(atPath: path)
|
||||
self?.append(source: "launcher", text: "sentinel detected — restarting sclang")
|
||||
self?.restartSclang()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func maybeRestartSclang() {
|
||||
guard sclangWantsRestart else { return }
|
||||
sclangWantsRestart = false
|
||||
// Small delay so scsynth pkill finishes and the audio device is free
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { [weak self] in
|
||||
self?.startSclang()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - oscope-of
|
||||
|
||||
func startOscope() {
|
||||
|
||||
@@ -647,12 +647,20 @@ function makeArmedButton(btn, options) {
|
||||
});
|
||||
}
|
||||
|
||||
makeArmedButton(document.getElementById("btnReboot"), {
|
||||
armedLabel: "CONFIRM ⟳ ?",
|
||||
makeArmedButton(document.getElementById("btnRebootScsynth"), {
|
||||
armedLabel: "CONFIRM ?",
|
||||
armedClass: "warn",
|
||||
sendOnConfirm: () => send("/control/rebootServer"),
|
||||
toastArmed: "REBOOT armed — click again within 2s",
|
||||
toastFired: "Reboot scsynth sent"
|
||||
toastArmed: "scsynth reboot armed — click again",
|
||||
toastFired: "scsynth reboot sent"
|
||||
});
|
||||
|
||||
makeArmedButton(document.getElementById("btnRebootSclang"), {
|
||||
armedLabel: "CONFIRM sclang ?",
|
||||
armedClass: "warn",
|
||||
sendOnConfirm: () => send("/control/rebootSclang"),
|
||||
toastArmed: "sclang reboot armed — full engine restart",
|
||||
toastFired: "sclang reboot sent — engine restarting"
|
||||
});
|
||||
|
||||
makeArmedButton(document.getElementById("btnTeardown"), {
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
<span class="ws-dot offline" id="scDot"></span>
|
||||
<span class="ws-label" id="scLabel">sclang</span>
|
||||
</div>
|
||||
<button class="btn warn" id="btnReboot" title="Reboot scsynth (click twice within 2s)">⟳ REBOOT</button>
|
||||
<button class="btn warn small" id="btnRebootScsynth" title="Reboot scsynth seul (click twice within 2s)">⟳ scsynth</button>
|
||||
<button class="btn warn" id="btnRebootSclang" title="Reboot sclang complet — recharge le moteur (click twice within 2s)">⟳ sclang</button>
|
||||
<button class="btn danger teardown" id="btnTeardown" title="Click twice within 2s to confirm">TEARDOWN</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1380,3 +1380,9 @@ kbd {
|
||||
0 0 24px rgba(255, 194, 74, 0.55) !important;
|
||||
animation: armed-pulse 0.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* Smaller variant of warn button (scsynth reboot) */
|
||||
.btn.warn.small {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
@@ -190,9 +190,9 @@ thisProcess.openUDPPort(~webBridgeIn);
|
||||
|
||||
~webBridgeAdd.("/control/teardown", { ~teardown.() });
|
||||
|
||||
// Reboot scsynth + replay setupAll (recharge engine + wrappers + tracks
|
||||
// + web_bridge). Conserve l'etat sclang (catalogues, dicts, OSCdef).
|
||||
// Le client web doit re-emettre listAlbums/Sections apres ~5s.
|
||||
// Reboot juste scsynth (audio engine). Conserve l'etat sclang
|
||||
// (catalogues, dicts, OSCdef). Le client web doit re-emettre
|
||||
// listAlbums/Sections apres ~5s via le retry persistant.
|
||||
~webBridgeAdd.("/control/rebootServer", {
|
||||
"[web_bridge] /control/rebootServer reçu — quit puis ~setupAll".postln;
|
||||
Routine({
|
||||
@@ -203,6 +203,20 @@ thisProcess.openUDPPort(~webBridgeIn);
|
||||
}).play(AppClock);
|
||||
});
|
||||
|
||||
// Reboot complet sclang : suicide via pkill, le launcher detecte
|
||||
// l'exit et relance sclang via terminationHandler + wantsAutoRestart.
|
||||
~webBridgeAdd.("/control/rebootSclang", {
|
||||
"[web_bridge] /control/rebootSclang reçu — sclang suicide en 0.5s...".postln;
|
||||
AppClock.sched(0.5, {
|
||||
// Touche un fichier sentinel que le launcher surveille
|
||||
var sentinel = Platform.userHomeDir ++ "/.av-live-restart-sclang";
|
||||
File.use(sentinel, "w") { |f| f.write("1") };
|
||||
// Puis suicide
|
||||
"pkill -TERM -f 'sclang.*boot.scd' 2>/dev/null".unixCmd;
|
||||
nil;
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- Mixer ----------------------------------------------------
|
||||
~webBridgeAdd.("/control/setVol", { |msg|
|
||||
~setVol.(msg[1].asString.asSymbol, msg[2]);
|
||||
|
||||
Reference in New Issue
Block a user