ProPresenter updates are safe: any installed copy counts as running, the newest version opens
- newest_of picks by CFBundleShortVersionString, not file name (ProPresenter.app sorted after ProPresenter 20.1.app) - mac_start takes newest_of; auto-setup uses it for programs on the Companion Mac - WS and SC switched over; NA's ProP restart re-rendered (same pick today) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -328,9 +328,9 @@
|
||||
"process": "ProPresenter",
|
||||
"wait_seconds": 10
|
||||
},
|
||||
"run": "APP=$(ls -d /Applications/ProPresenter*.app | sort -V | tail -1); pkill -TERM -x ProPresenter; i=0; while pgrep -xq ProPresenter && [ $i -lt 20 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq ProPresenter && pkill -KILL -x ProPresenter; sleep 1; open \"$APP\"",
|
||||
"short": "PROPRES",
|
||||
"template": "mac_restart"
|
||||
"template": "mac_restart",
|
||||
"run": "APP=$(for a in /Applications/ProPresenter*.app; do [ -d \"$a\" ] && printf '%s|%s\\n' \"$(defaults read \"$a/Contents/Info\" CFBundleShortVersionString 2>/dev/null)\" \"$a\"; done | sort -t '|' -k1,1V | tail -1 | cut -d '|' -f2-); pkill -TERM -x ProPresenter; i=0; while pgrep -xq ProPresenter && [ $i -lt 20 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq ProPresenter && pkill -KILL -x ProPresenter; sleep 1; open \"$APP\""
|
||||
},
|
||||
"ready_copy_tracks": {
|
||||
"action": "copy_tracks",
|
||||
|
||||
@@ -230,9 +230,15 @@ def items(inv, nets):
|
||||
continue
|
||||
fid = slug("%s_ingest" % a["app"])
|
||||
pretty = a["label"].title() if a["app"] not in ("propresenter", "ableton") else {"propresenter": "ProPresenter", "ableton": "Ableton"}[a["app"]]
|
||||
start = {"template": "mac_start", "params": {"host": "ingest", "process": a["process"], "app": app_path},
|
||||
# Any installed copy counts, and the newest opens: an update that installs beside the old one
|
||||
# ("ProPresenter 20.0.1.app" and "ProPresenter.app" at Woonsocket) never reads as stopped.
|
||||
newest = a["open"] if "*" in a.get("open", "") else ""
|
||||
if newest:
|
||||
exe = b.app_match(newest, a["process"])
|
||||
opens = {"app": "$APP", "newest_of": newest} if newest else {"app": app_path}
|
||||
start = {"template": "mac_start", "params": dict({"host": "ingest", "process": a["process"]}, **opens),
|
||||
"label": "start %s on this Mac if it is not running" % pretty, "short": a["label"]}
|
||||
again = {"template": "mac_restart", "params": {"host": "ingest", "process": a["process"], "app": app_path, "wait_seconds": 10, "force": True},
|
||||
again = {"template": "mac_restart", "params": dict({"host": "ingest", "process": a["process"], "wait_seconds": 10, "force": True}, **opens),
|
||||
"label": "restart %s on this Mac (closes it first)" % pretty, "short": a["label"]}
|
||||
item = {"ident": "app:ingest|%s" % a["process"], "group": a.get("group") or ("VIDEO" if a["app"] == "arena" else "STREAM" if a["app"] == "obs" else "SHOW"),
|
||||
"label": a["label"], "checks": [{"type": "process", "match": exe, "name": a["label"], "fix": fid}],
|
||||
|
||||
+21
-3
@@ -64,6 +64,22 @@ def mac_quit(process, wait_seconds, force, stop_children=False):
|
||||
return s
|
||||
|
||||
|
||||
def newest_app(pattern):
|
||||
"""APP=... the newest installed copy by its own version number (CFBundleShortVersionString), not by
|
||||
file name: "ProPresenter.app" sorts after "ProPresenter 20.1.app" by name (2026-10-21, before the
|
||||
Southcoast/Woonsocket ProPresenter update)."""
|
||||
return ("APP=$(for a in %s; do [ -d \"$a\" ] && printf '%%s|%%s\\n' \"$(defaults read \"$a/Contents/Info\" "
|
||||
"CFBundleShortVersionString 2>/dev/null)\" \"$a\"; done | sort -t '|' -k1,1V | tail -1 | cut -d '|' -f2-); "
|
||||
% str(pattern).strip())
|
||||
|
||||
|
||||
def app_match(pattern, process):
|
||||
"""A pgrep -f pattern for the program from any copy matching an /Applications glob (never its helpers)."""
|
||||
import re as _re
|
||||
parts = [_re.escape(x) for x in str(pattern).split("*")]
|
||||
return "[^/]*".join(parts).replace("\\ ", " ") + "/Contents/MacOS/" + _re.escape(process).replace("\\ ", " ") + "$"
|
||||
|
||||
|
||||
def mac_open(app, background=False, arg=None):
|
||||
"""open -a Name / open /path/App.app (+ -g to stay in the background)."""
|
||||
app = str(app)
|
||||
@@ -109,8 +125,10 @@ TEMPLATES = {
|
||||
"words": "Start a Mac program if it is not running",
|
||||
"os": "mac", "start_only": True,
|
||||
"params": [P_HOST, ("process", "Process name", "As pgrep sees it (Activity Monitor's process name).", "str", None),
|
||||
("app", "App to open", "Its name, or the full /Applications/... path.", "str", None)],
|
||||
"render": lambda p, cfg: "pgrep -xq %s || %s" % (q(p["process"]), mac_open(p["app"])),
|
||||
("app", "App to open", "Its name, the full /Applications/... path, or $APP with 'newest of' below.", "str", None),
|
||||
("newest_of", "Newest of", "A pattern like /Applications/ProPresenter*.app: the newest installed version opens.", "str", "")],
|
||||
"render": lambda p, cfg: ("pgrep -xq %s || { %s%s; }" % (q(p["process"]), newest_app(p["newest_of"]), mac_open("$APP"))
|
||||
if p.get("newest_of") else "pgrep -xq %s || %s" % (q(p["process"]), mac_open(p["app"]))),
|
||||
"suggest": lambda p, cfg: "start %s if it is not running" % p["app"],
|
||||
},
|
||||
"mac_restart": {
|
||||
@@ -123,7 +141,7 @@ TEMPLATES = {
|
||||
("background", "Open it in the background", "No window comes to the front.", "bool", False),
|
||||
("stop_children", "Also stop everything it started", "Its helper processes (a translation server, say).", "bool", False),
|
||||
("newest_of", "Newest of", "A pattern like /Applications/ProPresenter*.app: the newest installed version opens.", "str", "")],
|
||||
"render": lambda p, cfg: (("APP=$(ls -d %s | sort -V | tail -1); " % str(p["newest_of"]).strip()) if p.get("newest_of") else "")
|
||||
"render": lambda p, cfg: (newest_app(p["newest_of"]) if p.get("newest_of") else "")
|
||||
+ mac_quit(p["process"], p.get("wait_seconds", 10), p.get("force", True), p.get("stop_children", False))
|
||||
+ mac_open("$APP" if p.get("newest_of") else p["app"], p.get("background", False)),
|
||||
"suggest": lambda p, cfg: "restart %s" % (p["app"] if not p.get("newest_of") else "%s (newest installed)" % p["process"]),
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
},
|
||||
{
|
||||
"type": "process",
|
||||
"match": "/Applications/ProPresenter 20.0.1.app/Contents/MacOS/ProPresenter",
|
||||
"match": "/Applications/ProPresenter[^/]*\\.app/Contents/MacOS/ProPresenter$",
|
||||
"name": "PROP",
|
||||
"fix": "propresenter_ingest"
|
||||
}
|
||||
@@ -388,29 +388,30 @@
|
||||
"params": {
|
||||
"host": "ingest",
|
||||
"process": "ProPresenter",
|
||||
"app": "/Applications/ProPresenter 20.0.1.app"
|
||||
"app": "$APP",
|
||||
"newest_of": "/Applications/ProPresenter*.app"
|
||||
},
|
||||
"label": "start ProPresenter on this Mac if it is not running",
|
||||
"short": "PROP",
|
||||
"kind": "local",
|
||||
"run": "pgrep -xq ProPresenter || open '/Applications/ProPresenter 20.0.1.app'"
|
||||
"run": "pgrep -xq ProPresenter || { APP=$(for a in /Applications/ProPresenter*.app; do [ -d \"$a\" ] && printf '%s|%s\\n' \"$(defaults read \"$a/Contents/Info\" CFBundleShortVersionString 2>/dev/null)\" \"$a\"; done | sort -t '|' -k1,1V | tail -1 | cut -d '|' -f2-); open \"$APP\"; }"
|
||||
},
|
||||
"restart_propresenter_ingest": {
|
||||
"template": "mac_restart",
|
||||
"params": {
|
||||
"host": "ingest",
|
||||
"process": "ProPresenter",
|
||||
"app": "/Applications/ProPresenter 20.0.1.app",
|
||||
"app": "$APP",
|
||||
"wait_seconds": 10,
|
||||
"force": true,
|
||||
"background": false,
|
||||
"stop_children": false,
|
||||
"newest_of": ""
|
||||
"newest_of": "/Applications/ProPresenter*.app"
|
||||
},
|
||||
"label": "restart ProPresenter on this Mac (closes it first)",
|
||||
"short": "PROP",
|
||||
"kind": "local",
|
||||
"run": "pkill -TERM -x ProPresenter; i=0; while pgrep -xq ProPresenter && [ $i -lt 20 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq ProPresenter && pkill -KILL -x ProPresenter; sleep 1; open '/Applications/ProPresenter 20.0.1.app'"
|
||||
"run": "APP=$(for a in /Applications/ProPresenter*.app; do [ -d \"$a\" ] && printf '%s|%s\\n' \"$(defaults read \"$a/Contents/Info\" CFBundleShortVersionString 2>/dev/null)\" \"$a\"; done | sort -t '|' -k1,1V | tail -1 | cut -d '|' -f2-); pkill -TERM -x ProPresenter; i=0; while pgrep -xq ProPresenter && [ $i -lt 20 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq ProPresenter && pkill -KILL -x ProPresenter; sleep 1; open \"$APP\""
|
||||
},
|
||||
"prop_port_propresenter_api": {
|
||||
"kind": "propresenter_port",
|
||||
|
||||
+10
-9
@@ -71,13 +71,14 @@
|
||||
"kind": "local",
|
||||
"label": "start ProPresenter on this Mac if it is not running",
|
||||
"params": {
|
||||
"app": "/Applications/ProPresenter.app",
|
||||
"app": "$APP",
|
||||
"host": "ingest",
|
||||
"process": "ProPresenter"
|
||||
"process": "ProPresenter",
|
||||
"newest_of": "/Applications/ProPresenter*.app"
|
||||
},
|
||||
"run": "pgrep -xq ProPresenter || open /Applications/ProPresenter.app",
|
||||
"short": "PROP",
|
||||
"template": "mac_start"
|
||||
"template": "mac_start",
|
||||
"run": "pgrep -xq ProPresenter || { APP=$(for a in /Applications/ProPresenter*.app; do [ -d \"$a\" ] && printf '%s|%s\\n' \"$(defaults read \"$a/Contents/Info\" CFBundleShortVersionString 2>/dev/null)\" \"$a\"; done | sort -t '|' -k1,1V | tail -1 | cut -d '|' -f2-); open \"$APP\"; }"
|
||||
},
|
||||
"restart_ableton_ws_tracks": {
|
||||
"host": "ws_tracks",
|
||||
@@ -97,18 +98,18 @@
|
||||
"kind": "local",
|
||||
"label": "restart ProPresenter on this Mac (closes it first)",
|
||||
"params": {
|
||||
"app": "/Applications/ProPresenter.app",
|
||||
"app": "$APP",
|
||||
"background": false,
|
||||
"force": true,
|
||||
"host": "ingest",
|
||||
"newest_of": "",
|
||||
"newest_of": "/Applications/ProPresenter*.app",
|
||||
"process": "ProPresenter",
|
||||
"stop_children": false,
|
||||
"wait_seconds": 10
|
||||
},
|
||||
"run": "pkill -TERM -x ProPresenter; i=0; while pgrep -xq ProPresenter && [ $i -lt 20 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq ProPresenter && pkill -KILL -x ProPresenter; sleep 1; open /Applications/ProPresenter.app",
|
||||
"short": "PROP",
|
||||
"template": "mac_restart"
|
||||
"template": "mac_restart",
|
||||
"run": "APP=$(for a in /Applications/ProPresenter*.app; do [ -d \"$a\" ] && printf '%s|%s\\n' \"$(defaults read \"$a/Contents/Info\" CFBundleShortVersionString 2>/dev/null)\" \"$a\"; done | sort -t '|' -k1,1V | tail -1 | cut -d '|' -f2-); pkill -TERM -x ProPresenter; i=0; while pgrep -xq ProPresenter && [ $i -lt 20 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq ProPresenter && pkill -KILL -x ProPresenter; sleep 1; open \"$APP\""
|
||||
},
|
||||
"restart_superrack_ws_foh": {
|
||||
"host": "ws_foh",
|
||||
@@ -489,7 +490,7 @@
|
||||
},
|
||||
{
|
||||
"fix": "propresenter_ingest",
|
||||
"match": "/Applications/ProPresenter.app/Contents/MacOS/ProPresenter",
|
||||
"match": "/Applications/ProPresenter[^/]*\\.app/Contents/MacOS/ProPresenter$",
|
||||
"name": "PROP",
|
||||
"type": "process"
|
||||
}
|
||||
|
||||
@@ -162,9 +162,14 @@ class TestBuild(unittest.TestCase):
|
||||
cfg, _ = ac.build(BASE, inv, {"id": "ws", "name": "Woonsocket"}, "1234")
|
||||
self.assertEqual(schema.validate(cfg)[0], [])
|
||||
prop = next(t for t in cfg["tiles"] if t["label"] == "PROP")
|
||||
self.assertEqual([(c["type"], c.get("match") or c.get("conn")) for c in prop["checks"]], [("conn", "ProP"), ("process", exe)])
|
||||
import re
|
||||
self.assertEqual([c["type"] for c in prop["checks"]], ["conn", "process"])
|
||||
match = prop["checks"][1]["match"] # any installed copy counts (an update beside the old)
|
||||
self.assertTrue(re.search(match, exe) and re.search(match, "/Applications/ProPresenter 21.4.app/Contents/MacOS/ProPresenter"))
|
||||
fix = cfg["fixes"][prop["checks"][1]["fix"]]
|
||||
self.assertEqual((fix["kind"], fix["run"]), ("local", "pgrep -xq ProPresenter || open /Applications/ProPresenter.app"))
|
||||
self.assertEqual(fix["kind"], "local")
|
||||
self.assertTrue(fix["run"].startswith("pgrep -xq ProPresenter || { APP=$(for a in /Applications/ProPresenter*.app"), fix["run"])
|
||||
self.assertTrue(fix["run"].endswith('open "$APP"; }'), fix["run"]) # the newest version opens
|
||||
self.assertIn(prop["checks"][1]["fix"], cfg["auto_fix"]["fixes"])
|
||||
self.assertEqual([k["label"] for k in cfg["restart_page"]["keys"]], ["PROP"])
|
||||
self.assertEqual(cfg["fixes"][cfg["restart_page"]["keys"][0]["fix"]]["kind"], "local")
|
||||
|
||||
@@ -376,3 +376,30 @@ class TestBindingAndProbes(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class TestNewestCopy(unittest.TestCase):
|
||||
"""An update installed beside the old copy: the newest version opens, whatever the file names."""
|
||||
|
||||
@unittest.skipUnless(sys.platform == "darwin", "reads app versions with macOS's defaults")
|
||||
def test_the_newest_version_wins_not_the_last_name(self):
|
||||
import subprocess
|
||||
from settings import builders as b
|
||||
for names in ((("Show 20.0.1.app", "20.0.1"), ("Show.app", "20.1.0")), (("Show 20.0.1.app", "20.0.1"), ("Show.app", "19.4"))):
|
||||
root = tempfile.mkdtemp(prefix="sp-apps-")
|
||||
for name, ver in names:
|
||||
os.makedirs(os.path.join(root, name, "Contents"))
|
||||
subprocess.run(["/usr/bin/defaults", "write", os.path.join(root, name, "Contents", "Info"),
|
||||
"CFBundleShortVersionString", ver], check=True)
|
||||
out = subprocess.run(["/bin/sh", "-c", b.newest_app(root + "/Show*.app") + 'echo "$APP"'],
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
want = max(names, key=lambda n: tuple(int(x) for x in n[1].split(".")))[0]
|
||||
self.assertEqual(os.path.basename(out), want)
|
||||
|
||||
def test_any_copy_counts_as_running_but_never_a_helper(self):
|
||||
import re
|
||||
from settings import builders as b
|
||||
m = b.app_match("/Applications/ProPresenter*.app", "ProPresenter")
|
||||
self.assertTrue(re.search(m, "/Applications/ProPresenter 20.0.1.app/Contents/MacOS/ProPresenter"))
|
||||
self.assertTrue(re.search(m, "/Applications/ProPresenter.app/Contents/MacOS/ProPresenter"))
|
||||
self.assertFalse(re.search(m, "/Applications/ProPresenter.app/Contents/MacOS/ProPresenter Helper (Network)"))
|
||||
|
||||
Reference in New Issue
Block a user