PROP OUT: keep Location Assets mounted on the ProP Mac; SHARE NOT MOUNTED; hold remounts
- remote-access/mac-keep-mounted.sh: a user LaunchAgent that keeps a share mounted (every minute and at login; Keychain login; unmount + remount only when the share hangs), installed on NA ProP for Chronicles' Location Assets - PROP OUT says SHARE NOT MOUNTED instead of NOT IN <weekend> when the share is gone; its hold (and auto-fix) starts the agent and waits for the share Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -131,7 +131,7 @@ HyperDeck is recording). **STATUS PAGE** (bottom left) goes back.
|
|||||||
| row 0 (Ableton) | TRACKS | FOH MIX | BOH MIX (tap) | LIGHTS | MIDI | SONG KEYS | W TUNE |
|
| row 0 (Ableton) | TRACKS | FOH MIX | BOH MIX (tap) | LIGHTS | MIDI | SONG KEYS | W TUNE |
|
||||||
| row 1 (ProP / Resolume) | PROP | TIMECODE | BKGDS | LED CROSS | LIGHT TIMING | every blinder flash and FX cue (notes 30-39 on `LIGHTING CUES`: Blinder Flash 1-3, Blinder FX 1-5, named from `Ableton/LIGHTING CUES MIDI`) lands on the hit in the `Drums (Live)` stem under it, even when that clip is deactivated because the drummer plays live. Each cue's moment in the stem comes from the tempo map; the Tracks Mac listens in 2.5 ms steps out to almost a 16th either way, finds the strongest hit and where it starts (halfway up its rise); what is under the cue is looked at from 10 ms before it to 25 ms after (the stems' hits start a little after the grid). Each song's cues that sit on solid hits set what "on time" is for that song (its stem's hits start 7-12 ms after the grid). **14 CUES OFF** (amber): a strong hit more than 25 ms from the cue while the cue sits on a quiet spot; a cue already on a solid hit (a double hit follows) is never named. The details say `JEHOVAH bar 405.3.2: Blinder Flash 2 is 145 ms after the hit (about a 16th)` (bars as Live shows them, 4/4). Listened to once per saved set. | moves every cue that is off onto its hit in the open set (all flash/FX notes on that 16th; onto the 16th grid when the hit is within 10 ms of it), keeping where each cue ends, then saves the set (File › Save Live Set in Live, clicked through System Events: `/usr/bin/osascript` has Accessibility on the Tracks Mac since 2026-10-22; `ready.light_autosave`), but only when the set open in Live is this weekend's. If it cannot save, the key says **SAVE SET (n MOVED)** until the set is saved; Cmd+Z in Live takes a move back. Never runs by itself. Uses the status panel's command in AbletonOSC (`tools/abletonosc`: `statuspanel.py` plus a hook at the end of AbletonOSC's `abletonosc/__init__.py`; an AbletonOSC update removes it, so run `tools/abletonosc/install.sh` again, then restart Companion's `ableton` connection) |
|
| row 1 (ProP / Resolume) | PROP | TIMECODE | BKGDS | LED CROSS | LIGHT TIMING | every blinder flash and FX cue (notes 30-39 on `LIGHTING CUES`: Blinder Flash 1-3, Blinder FX 1-5, named from `Ableton/LIGHTING CUES MIDI`) lands on the hit in the `Drums (Live)` stem under it, even when that clip is deactivated because the drummer plays live. Each cue's moment in the stem comes from the tempo map; the Tracks Mac listens in 2.5 ms steps out to almost a 16th either way, finds the strongest hit and where it starts (halfway up its rise); what is under the cue is looked at from 10 ms before it to 25 ms after (the stems' hits start a little after the grid). Each song's cues that sit on solid hits set what "on time" is for that song (its stem's hits start 7-12 ms after the grid). **14 CUES OFF** (amber): a strong hit more than 25 ms from the cue while the cue sits on a quiet spot; a cue already on a solid hit (a double hit follows) is never named. The details say `JEHOVAH bar 405.3.2: Blinder Flash 2 is 145 ms after the hit (about a 16th)` (bars as Live shows them, 4/4). Listened to once per saved set. | moves every cue that is off onto its hit in the open set (all flash/FX notes on that 16th; onto the 16th grid when the hit is within 10 ms of it), keeping where each cue ends, then saves the set (File › Save Live Set in Live, clicked through System Events: `/usr/bin/osascript` has Accessibility on the Tracks Mac since 2026-10-22; `ready.light_autosave`), but only when the set open in Live is this weekend's. If it cannot save, the key says **SAVE SET (n MOVED)** until the set is saved; Cmd+Z in Live takes a move back. Never runs by itself. Uses the status panel's command in AbletonOSC (`tools/abletonosc`: `statuspanel.py` plus a hook at the end of AbletonOSC's `abletonosc/__init__.py`; an AbletonOSC update removes it, so run `tools/abletonosc/install.sh` again, then restart Companion's `ableton` connection) |
|
||||||
| row 2 (backstage) | LINEUP TV | ROUTING | IN EARS | | | | |
|
| row 2 (backstage) | LINEUP TV | ROUTING | IN EARS | | | | |
|
||||||
| row 3 (outgoing) | TRACKS OUT | PROP OUT | | | | | READY |
|
| row 3 (outgoing) | TRACKS OUT | PROP OUT | a `.probundle` is in this weekend's folder of the Chronicles share (`/Volumes/Location Assets/261025`, read on the ProPresenter Mac). **SHARE NOT MOUNTED**: the share itself is not mounted there (2026-10-22: it had dropped off and the key said NOT IN 261025 although the bundle was there). The ProP Mac keeps it mounted by itself: a LaunchAgent (`org.waterschurch.keep-mounted.location-assets`, from `remote-access/mac-keep-mounted.sh`) looks every minute and at login, mounts it again with the Chronicles login saved in the Keychain, and unmounts and remounts it when it hangs after sleep or a network drop (log: `~/Library/Logs/keep-mounted-location-assets.log` on the ProP Mac). | starts that agent now and waits for the share (about 6 s), then looks again. Also an auto-fix. |
|
||||||
|
|
||||||
The READY key at row 3 col 7 (and the one on the status page) shows how many keys still have something to do and
|
The READY key at row 3 col 7 (and the one on the status page) shows how many keys still have something to do and
|
||||||
the first of them.
|
the first of them.
|
||||||
|
|||||||
+9
-2
@@ -28,7 +28,8 @@
|
|||||||
"atem_midi",
|
"atem_midi",
|
||||||
"spotify_play",
|
"spotify_play",
|
||||||
"ready_ltc",
|
"ready_ltc",
|
||||||
"ready_ableton_mix"
|
"ready_ableton_mix",
|
||||||
|
"ready_mount_share"
|
||||||
],
|
],
|
||||||
"max_tries": 2,
|
"max_tries": 2,
|
||||||
"pause_after_network_seconds": 60,
|
"pause_after_network_seconds": 60,
|
||||||
@@ -510,6 +511,12 @@
|
|||||||
"never_auto": true,
|
"never_auto": true,
|
||||||
"short": "CUES",
|
"short": "CUES",
|
||||||
"label": "move the lighting cues that are off onto the drum hit in the open Ableton set (then save the set)"
|
"label": "move the lighting cues that are off onto the drum hit in the open Ableton set (then save the set)"
|
||||||
|
},
|
||||||
|
"ready_mount_share": {
|
||||||
|
"action": "mount_share",
|
||||||
|
"kind": "ready",
|
||||||
|
"short": "MOUNT",
|
||||||
|
"label": "mount Location Assets again on the ProPresenter Mac (its keep-mounted agent), then look for the bundle again"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flap": {
|
"flap": {
|
||||||
@@ -1396,7 +1403,7 @@
|
|||||||
{
|
{
|
||||||
"checks": [
|
"checks": [
|
||||||
{
|
{
|
||||||
"fix": "ready_recheck",
|
"fix": "ready_mount_share",
|
||||||
"item": "prop_bundle",
|
"item": "prop_bundle",
|
||||||
"type": "ready"
|
"type": "ready"
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-2
@@ -1655,8 +1655,12 @@ def eval_columns(rc, prop, found, get_page, comp):
|
|||||||
BUNDLE_SCRIPT = r'''
|
BUNDLE_SCRIPT = r'''
|
||||||
import glob, json, os
|
import glob, json, os
|
||||||
P = json.loads(%r)
|
P = json.loads(%r)
|
||||||
files = glob.glob(os.path.join(os.path.expanduser(P["folder"]), P["pattern"]))
|
folder = os.path.expanduser(P["folder"])
|
||||||
print(json.dumps(sorted([[os.path.basename(f), os.path.getmtime(f)] for f in files], key=lambda x: -x[1])[:5]))
|
files = glob.glob(os.path.join(folder, P["pattern"]))
|
||||||
|
parts = folder.split("/")
|
||||||
|
share = "/".join(parts[:3]) if folder.startswith("/Volumes/") and len(parts) > 2 else None # /Volumes/Location Assets
|
||||||
|
print(json.dumps({"files": sorted([[os.path.basename(f), os.path.getmtime(f)] for f in files], key=lambda x: -x[1])[:5],
|
||||||
|
"share": share, "mounted": os.path.ismount(share) if share else None}))
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@@ -1666,8 +1670,28 @@ def prop_bundle_folder(rc, plan):
|
|||||||
return str(rc.get("prop_bundle_folder", "")).replace("{date}", datetime.fromtimestamp(plan["time"]).strftime("%y%m%d"))
|
return str(rc.get("prop_bundle_folder", "")).replace("{date}", datetime.fromtimestamp(plan["time"]).strftime("%y%m%d"))
|
||||||
|
|
||||||
|
|
||||||
|
MOUNT_SCRIPT = r"""
|
||||||
|
import json, os, subprocess, time
|
||||||
|
P = json.loads(%r)
|
||||||
|
uid = os.getuid()
|
||||||
|
r = subprocess.run(["/bin/launchctl", "kickstart", "-k", "gui/%%d/%%s" %% (uid, P["label"])], capture_output=True, text=True)
|
||||||
|
for _ in range(60):
|
||||||
|
if os.path.ismount(P["share"]):
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
print(json.dumps({"mounted": os.path.ismount(P["share"]), "no_agent": r.returncode != 0 and not os.path.ismount(P["share"])}))
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def eval_prop_bundle(rc, files, since, folder=None):
|
def eval_prop_bundle(rc, files, since, folder=None):
|
||||||
line = Line()
|
line = Line()
|
||||||
|
if isinstance(files, dict):
|
||||||
|
if files.get("share") and files.get("mounted") is False:
|
||||||
|
# 2026-10-22: Location Assets dropped off NA ProP; the bundle was there all along
|
||||||
|
line.fault(ERR, "SHARE NOT MOUNTED", "%s is not mounted on %s: hold PROP OUT to mount it again" % (
|
||||||
|
files["share"], rc.get("prop_bundle_host")))
|
||||||
|
return line
|
||||||
|
files = files.get("files") or []
|
||||||
if "{date}" in str(rc.get("prop_bundle_folder", "")):
|
if "{date}" in str(rc.get("prop_bundle_folder", "")):
|
||||||
# This weekend's own folder: any bundle in it is this weekend's.
|
# This weekend's own folder: any bundle in it is this weekend's.
|
||||||
if not files:
|
if not files:
|
||||||
@@ -1943,6 +1967,15 @@ def run_action(cfg, action, chk=None, sleep=time.sleep):
|
|||||||
return "NO SET TO COPY"
|
return "NO SET TO COPY"
|
||||||
log.info("FIX tracks copy started to %s", ", ".join(r["started"]) or "nowhere (all there)")
|
log.info("FIX tracks copy started to %s", ", ".join(r["started"]) or "nowhere (all there)")
|
||||||
return True
|
return True
|
||||||
|
if action == "mount_share":
|
||||||
|
# The computer's keep-mounted agent (remote-access/mac-keep-mounted.sh) mounts it again, now.
|
||||||
|
label = "org.waterschurch.keep-mounted." + rc.get("prop_bundle_share_agent", "location-assets")
|
||||||
|
share = "/".join(str(rc.get("prop_bundle_folder", "")).split("/")[:3])
|
||||||
|
r = run_remote(cfg, rc["prop_bundle_host"], MOUNT_SCRIPT % json.dumps({"label": label, "share": share}), timeout=60)
|
||||||
|
if r.get("mounted"):
|
||||||
|
log.info("FIX %s mounted on %s", share, rc["prop_bundle_host"])
|
||||||
|
return True
|
||||||
|
return "NO AGENT" if r.get("no_agent") else "STILL NOT MOUNTED"
|
||||||
if action == "light_timing":
|
if action == "light_timing":
|
||||||
return fix_light_timing(cfg, rc)
|
return fix_light_timing(cfg, rc)
|
||||||
if action == "ableton_mix":
|
if action == "ableton_mix":
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
# Keeps a network share mounted on this Mac (2026-10-22: Location Assets dropped off NA ProP and PROP OUT
|
||||||
|
# could not see the bundle). Installs a LaunchAgent for the logged-in user that looks every minute and at
|
||||||
|
# login: mounted and answering = nothing; not mounted = mount it again with the login saved in the Keychain
|
||||||
|
# (tick "Remember this password" when connecting once by hand); mounted but dead (after sleep or a network
|
||||||
|
# drop) = unmount it, then mount it again. Never asks for a password and never shows a dialog.
|
||||||
|
#
|
||||||
|
# Run on the Mac, as the user who uses the share:
|
||||||
|
# zsh mac-keep-mounted.sh <name> <smb URL> <mount point>
|
||||||
|
# zsh mac-keep-mounted.sh location-assets "smb://Kenobi@Chronicles-NA._smb._tcp.local/Location%20Assets" "/Volumes/Location Assets"
|
||||||
|
# Undo: launchctl bootout gui/$(id -u)/org.waterschurch.keep-mounted.<name>; rm ~/Library/LaunchAgents/org.waterschurch.keep-mounted.<name>.plist ~/Library/Scripts/keep-mounted-<name>.sh
|
||||||
|
set -e
|
||||||
|
NAME=$1 URL=$2 MP=$3
|
||||||
|
[[ -n $NAME && -n $URL && -n $MP ]] || { echo "usage: zsh mac-keep-mounted.sh <name> <smb URL> <mount point>" >&2; exit 2; }
|
||||||
|
LABEL=org.waterschurch.keep-mounted.$NAME
|
||||||
|
SCRIPT=$HOME/Library/Scripts/keep-mounted-$NAME.sh
|
||||||
|
LOG=$HOME/Library/Logs/keep-mounted-$NAME.log
|
||||||
|
HOST=${${URL#*://}#*@}; HOST=${HOST%%/*}
|
||||||
|
mkdir -p "$HOME/Library/Scripts" "$HOME/Library/LaunchAgents" "$HOME/Library/Logs"
|
||||||
|
cat > "$SCRIPT" <<EOS
|
||||||
|
#!/bin/zsh
|
||||||
|
# Keep $MP mounted ($URL). Installed by the status panel's mac-keep-mounted.sh; see that file to undo.
|
||||||
|
MP="$MP"; URL="$URL"; HOST="$HOST"; LOG="$LOG"
|
||||||
|
say() { print -r -- "\$(date '+%Y-%m-%d %H:%M:%S') \$*" >> "\$LOG"; }
|
||||||
|
if mount | grep -qF " on \$MP ("; then
|
||||||
|
# Dead only when looking at it hangs (killed after 20 s: status 142). A refusal (macOS's privacy rules for
|
||||||
|
# a background job) or any other error leaves a working share alone.
|
||||||
|
/usr/bin/perl -e 'alarm 20; exec @ARGV' /bin/df "\$MP" >/dev/null 2>&1
|
||||||
|
[[ \$? -ne 142 ]] && exit 0
|
||||||
|
say "mounted but not answering (20 s): unmounting"
|
||||||
|
/usr/sbin/diskutil unmount force "\$MP" >> "\$LOG" 2>&1 || /sbin/umount -f "\$MP" >> "\$LOG" 2>&1 || true
|
||||||
|
fi
|
||||||
|
SERVER=\${HOST%%._smb._tcp.local}; SERVER=\${SERVER%%.local}
|
||||||
|
if ! /usr/bin/perl -e 'alarm 6; exec @ARGV' /usr/bin/nc -z -G 4 "\$SERVER.local" 445 >/dev/null 2>&1; then
|
||||||
|
exit 0 # the server is not on the network right now: try again in a minute
|
||||||
|
fi
|
||||||
|
say "not mounted: mounting \$URL"
|
||||||
|
/usr/bin/osascript -e 'with timeout of 40 seconds' -e "mount volume \"\$URL\"" -e 'end timeout' >> "\$LOG" 2>&1 || say "mount failed"
|
||||||
|
mount | grep -qF " on \$MP (" && say "mounted at \$MP" || say "not at \$MP after mounting: \$(mount | grep -i smbfs | tr '\n' ' ')"
|
||||||
|
EOS
|
||||||
|
chmod 755 "$SCRIPT"
|
||||||
|
cat > "$HOME/Library/LaunchAgents/$LABEL.plist" <<EOP
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0"><dict>
|
||||||
|
<key>Label</key><string>$LABEL</string>
|
||||||
|
<key>ProgramArguments</key><array><string>/bin/zsh</string><string>$SCRIPT</string></array>
|
||||||
|
<key>RunAtLoad</key><true/>
|
||||||
|
<key>StartInterval</key><integer>60</integer>
|
||||||
|
<key>ProcessType</key><string>Background</string>
|
||||||
|
</dict></plist>
|
||||||
|
EOP
|
||||||
|
launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
|
||||||
|
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/$LABEL.plist"
|
||||||
|
echo "installed $LABEL: keeps $MP mounted (log: $LOG)"
|
||||||
@@ -438,6 +438,9 @@ field("ready.prop_host", "str", None, "The ProPresenter computer: its macros and
|
|||||||
field("ready.prop_bundle_host", "str", None, "The computer that saves the ProPresenter bundle.", "Ready")
|
field("ready.prop_bundle_host", "str", None, "The computer that saves the ProPresenter bundle.", "Ready")
|
||||||
field("ready.prop_bundle_folder", "path", "", "Where the bundle for the locations goes. {date} is this weekend's folder "
|
field("ready.prop_bundle_folder", "path", "", "Where the bundle for the locations goes. {date} is this weekend's folder "
|
||||||
"(Location Assets/{date} -> Location Assets/261025): any bundle in it counts as sent.", "Ready")
|
"(Location Assets/{date} -> Location Assets/261025): any bundle in it counts as sent.", "Ready")
|
||||||
|
field("ready.prop_bundle_share_agent", "str", "location-assets", "The keep-mounted agent on that computer that mounts "
|
||||||
|
"the share the bundle folder is on (remote-access/mac-keep-mounted.sh <name>): PROP OUT's hold starts it.", "Ready",
|
||||||
|
advanced=True)
|
||||||
field("ready.prop_bundle_pattern", "str", "*.probundle", "The bundle's file name pattern.", "Ready", advanced=True)
|
field("ready.prop_bundle_pattern", "str", "*.probundle", "The bundle's file name pattern.", "Ready", advanced=True)
|
||||||
field("ready.vocal_positions", "list", ["Vocalist", "[Leader]"], "PCO positions that need a vocal mic.", "Ready")
|
field("ready.vocal_positions", "list", ["Vocalist", "[Leader]"], "PCO positions that need a vocal mic.", "Ready")
|
||||||
field("ready.mic_positions", "dict", {}, "Other PCO positions and the mic they need (MC -> MC).", "Ready")
|
field("ready.mic_positions", "dict", {}, "Other PCO positions and the mic they need (MC -> MC).", "Ready")
|
||||||
|
|||||||
+1
-1
@@ -819,7 +819,7 @@ def check_ready_manual(chk, snap, ctx):
|
|||||||
# What readiness.py looks at (check type "ready") and what a "ready" fix can do.
|
# What readiness.py looks at (check type "ready") and what a "ready" fix can do.
|
||||||
READY_ITEMS = ("tracks", "mix_foh", "lights", "light_timing", "keys", "tune", "routing", "ears", "prop", "timecode", "backgrounds", "led_cross", "lineup", "mics",
|
READY_ITEMS = ("tracks", "mix_foh", "lights", "light_timing", "keys", "tune", "routing", "ears", "prop", "timecode", "backgrounds", "led_cross", "lineup", "mics",
|
||||||
"track_bundle", "prop_bundle")
|
"track_bundle", "prop_bundle")
|
||||||
READY_ACTIONS = ("recheck", "lineup_sync", "led_cross", "copy_tracks", "ltc_to_pco", "ableton_mix", "light_timing")
|
READY_ACTIONS = ("recheck", "lineup_sync", "led_cross", "copy_tracks", "ltc_to_pco", "ableton_mix", "light_timing", "mount_share")
|
||||||
|
|
||||||
CHECKS = {
|
CHECKS = {
|
||||||
"conn": check_conn, "hyperdeck": check_hyperdeck, "dante": check_dante, "shure": check_shure,
|
"conn": check_conn, "hyperdeck": check_hyperdeck, "dante": check_dante, "shure": check_shure,
|
||||||
|
|||||||
@@ -623,6 +623,12 @@ class TestLineupCrossBundle(unittest.TestCase):
|
|||||||
self.assertEqual(folder, "~/Shared drives/Location Assets/261025")
|
self.assertEqual(folder, "~/Shared drives/Location Assets/261025")
|
||||||
self.assertEqual(msgs(rd.eval_prop_bundle(rc, [["WORSHIP BUNDLE.probundle", WEDNESDAY - 86400 * 10]], WEDNESDAY, folder)), [])
|
self.assertEqual(msgs(rd.eval_prop_bundle(rc, [["WORSHIP BUNDLE.probundle", WEDNESDAY - 86400 * 10]], WEDNESDAY, folder)), [])
|
||||||
self.assertEqual(msgs(rd.eval_prop_bundle(rc, [], WEDNESDAY, folder)), ["NOT IN 261025"])
|
self.assertEqual(msgs(rd.eval_prop_bundle(rc, [], WEDNESDAY, folder)), ["NOT IN 261025"])
|
||||||
|
# the share the folder is on is not mounted on the ProP Mac (2026-10-22): said so, not "NOT IN"
|
||||||
|
gone = {"files": [], "share": "/Volumes/Location Assets", "mounted": False}
|
||||||
|
self.assertEqual(msgs(rd.eval_prop_bundle(rc, gone, WEDNESDAY, folder)), ["SHARE NOT MOUNTED"])
|
||||||
|
there = {"files": [["WORSHIP BUNDLE.probundle", WEDNESDAY]], "share": "/Volumes/Location Assets", "mounted": True}
|
||||||
|
self.assertEqual(msgs(rd.eval_prop_bundle(rc, there, WEDNESDAY, folder)), [])
|
||||||
|
compile(rd.MOUNT_SCRIPT % json.dumps({"label": "x", "share": "/Volumes/X"}), "remote", "exec")
|
||||||
|
|
||||||
def test_prop_bundle_this_week(self):
|
def test_prop_bundle_this_week(self):
|
||||||
since = rd.week_start(WEDNESDAY)
|
since = rd.week_start(WEDNESDAY)
|
||||||
|
|||||||
Reference in New Issue
Block a user