如何创建访问提醒事项的 AppleScript(带参数)
我编写了一个 AppleScript 来同步我的提醒事项(通过导出到 JSON)。它运行良好...来自脚本编辑器。当我尝试通过
osascript
在命令行上运行它时,我发现它在尝试访问提醒事项时会遇到障碍。大约一分半钟后,我收到此错误:
/Users/robleach/Temporary/synchRemindersTest.scpt: execution error: Reminders got an error: AppleEvent timed out. (-1712)
我还在控制台中注意到了这些错误:
error 19:33:49.628309-0400 tccd Refusing client without path (from responsibility_get_responsible_audit_token_for_audit_token) PID[1422]: (#3) No such process
error 19:33:49.628370-0400 tccd Refusing TCCAccessRequest for service kTCCServiceReminders from invalid client with pid 1422
假设这是一个权限问题,我查看了系统偏好设置>安全和隐私>提醒,并注意到
osascript
不存在,也没有 ± 按钮来添加它,即使在身份验证之后也是如此。
我想知道将脚本保存为应用程序是否会提示安全内容提示我启用它 - 并且我可以 cron 启动该应用程序,但如果我这样做,我将无法将参数传递给脚本(或者至少,我不知道如何做到这一点)。另外,我宁愿一切都在后台发生,没有停靠图标或任何东西(除了需要打开提醒应用程序)。
我编写了一个产生超时错误的脚本的玩具示例:
玩具 1
tell application "Reminders"
return (properties of every reminder whose completed is false)
end tell
我这样称呼它:
> osascript /Users/robleach/Temporary/synchRemindersTest.scpt
有没有办法允许脚本的 osascript 运行访问提醒?我可以对命令行可执行文件进行代码签名吗?如果我用另一种语言编写它,它会出现同样的问题吗?
我正在运行 Catalina 10.15.7。
更新 1
我在控制台中挖掘了更多内容。还有许多其他可能相关的错误。我认为这实际上是 超时。当我在脚本编辑器中运行它时,它大约需要 40 秒,但当我通过 osascript 运行它时它会超时(大约一分半钟)。
但是,我记得我在 cron 作业上有另一个访问提醒的脚本,我不记得它有问题。所以我测试了它,无论出于什么原因,它执行了一个非常相似的命令,但成功了。而且它在编辑器中的运行速度比在 osascript 的命令行中运行快得多。我从该脚本中抽出一行成功的内容,并将其包装在一个玩具脚本中:
玩具 2
tell application "Reminders"
set theList to "ToDo Home Recurring"
set namesDates to {name, due date} of (every reminder in list theList whose completed is false)
display dialog "Got " & (count of namesDates) & " reminder names & dates"
end tell
,它通过 osascript 失败,并出现相同的超时错误。然后我抽出另一行并将其添加到玩具中:
玩具 3
tell application "Reminders"
set theList to "ToDo Home Recurring"
show list theList
delay 0.25
set namesDates to {name, due date} of (every reminder in list theList whose completed is false)
tell application "System Events" to display dialog "Got " & (count of namesDates) & " reminder names & dates"
end tell
...它 成功 了。所以我认为这不再是权限问题。这感觉可能与 osascript 访问提醒的效率有关。
我还开始注意到,上面的第一个玩具示例有时会从脚本编辑器运行失败。我不断重试以获取上面粘贴的运行时间,并开始感觉到一种模式。我 认为 ,当我在提醒 GUI 中选择要查看的新列表(无论哪个列表),然后运行脚本(从编辑器中)时,它会起作用。但如果我没有选择要查看的新列表并再次从编辑器中运行脚本,它会因超时而失败。
...但这似乎很疯狂。有人能解释一下这里发生了什么吗?
注意:我编写的脚本实际上是我编写的 Siri Shortcut 的 AppleScript 重写版(运行时间大约为 25 秒)。由于我想使其自动化并每天运行多次,因此我决定使用 AppleScript。
更新 2
我尝试了 @Robert Kniazidis 建议的答案。
TOY 4(TOY 1 的修改版)
with timeout of 3600 seconds
tell application "Reminders"
set allRems to (properties of every reminder whose completed is false)
display dialog "Got " & (count of allRems) & " reminders"
end tell
end timeout
...并密切关注控制台。
尝试 1(TOY 4)
我从 7:25:24 开始运行 TOY 4,持续了 10 分钟,然后按 control-c 将其关闭。我立即在控制台中看到大量错误。我在控制台中搜索了“提醒”, 以下是我在运行过程中所做的操作 。
尝试 2(TOY 4)
然后,考虑到我在提醒 GUI 中单击列表名称时获得的轶事成功经验,我尝试单击一个随机列表,然后立即再次运行 TOY 4。我在 7:38:23 启动了 TOY 4。在 7:44:22,它成功了!大约 6 分钟!
控制台中的消息少得多,没有一条被标记为错误。为了便于比较, 以下是搜索“提醒”的控制台结果 。
讨论
我修改了关于正在发生的事情的理论。根据控制台消息,我推断当您从命令行运行 osascript 时,该脚本被标识为“间接访问”,因此受到更高级别的安全审查,因此执行时间
要长得多
。也许当我“在 GUI 中单击”时(或者甚至通过 AppleScript,
show list theList
),安全问题仍被视为“间接”,但对用户来说并非完全未知,因为 GUI 正在发生变化,因此受到的审查较少,因此需要 6 分钟,而不是超过 10 分钟。
如果这是真的,值得注意的是,即使提醒事项 GUI 位于不同的桌面上(就像我的测试中的情况一样*),也会应用较低级别的审查。
更新 3
我今天早上尝试使用以下代码进行临时代码签名:
codesign --force -v -s - synchRemindersTest.app/Contents/Info.plist synchRemindersTest.app/Contents/PkgInfo synchRemindersTest.app/Contents/Resources/applet.rsrc synchRemindersTest.app/Contents/Resources/Scripts/main.scpt synchRemindersTest.app/Contents/Resources/applet.icns
...并再次运行该应用程序,它是 TOY 1 的一个版本。仍然出现超时错误。我希望它会像从脚本编辑器运行时一样需要 40 秒。等我有时间了,我会再试一次,但要手动选择提醒器 GUI 中的列表。
更新 4
我刚刚再次运行了与更新 3 中相同的玩具。在超时前运行的 2 分钟内,控制台中充满了 52,349 行
大部分是这个
,一遍又一遍地重复,这只是该时间跨度中与搜索词
tccd
匹配的部分。
我还注意到,在不同时间运行的相同未修改脚本会在某些运行中成功,而在其他运行中失败。例如:
玩具 5 (synchRemindersTest5.scpt)
with timeout of 600 seconds
tell application "Reminders"
show list "ToDo Home"
set startt to (get current date)
set allRems to (properties of every reminder whose completed is false)
set endt to (get current date)
set dur to (endt - startt)
set msg to "Got " & (count of allRems) & " reminders in " & dur & " seconds"
tell application "System Events" to display dialog msg giving up after 5
return msg
end tell
end timeout
我昨天多次运行它,成功了,但今天运行时却超时了:
[Jun 08 22:59:51]:~/GoogleDrive/Scripts>osascript synchRemindersTest5.scpt
Got 166 reminders in 287 seconds
[Jun 08 23:06:17]:~/GoogleDrive/Scripts>osascript synchRemindersTest5.scpt
Got 166 reminders in 291 seconds
[Jun 08 23:11:45]:~/GoogleDrive/Scripts>osascript synchRemindersTest5.scpt
Got 166 reminders in 293 seconds
[Jun 08 23:17:46]:~/GoogleDrive/Scripts>osascript synchRemindersTest5.scpt
Got 166 reminders in 300 seconds
[Jun 09 8:23:28]:~/GoogleDrive/Scripts>osascript synchRemindersTest5.scpt
synchRemindersTest5.scpt: execution error: Reminders got an error: AppleEvent timed out. (-1712)
脚注
* 我一直在不同桌面上用提醒应用特意测试我的脚本,因为我注意到 GUI 脚本总是比通过提醒词典访问更快。所以我写了 2 个方法:GUI 和提醒词典。如果打开的提醒应用在桌面上(我把它放在 dock 下),GUI 方法就会运行。如果我们全屏观看 Netflix,我会使用 try/catch 来使用较慢的提醒词典访问方法,当 GUI 在另一个桌面上时(在这种情况下)。
用 3600 秒(1 小时)的超时时间包装您的脚本。您的脚本超时默认时间 = 每个命令 2 分钟(120 秒)。因此:
with timeout of 3600 seconds -- or 600 seconds, or as you want
tell application "Reminders"
return (properties of every reminder whose completed is false)
end tell
end timeout
再次查看您的 TOY 4。osascript 的手册页显示:脚本后面的任何参数都将作为字符串列表传递给 “运行”处理程序的直接参数 。因此,您的 TOY 4 应该是这样的:
on run argv -- THIS
with timeout of 3600 seconds
tell application "Reminders"
set allRems to (properties of every reminder whose completed is false)
display dialog "Got " & (count of allRems) & " reminders"
end tell
end timeout
end run -- and THIS
我在终端中尝试了这个脚本,使用以下命令,它成功请求访问提醒,并且 在授予访问权限后工作 。还请注意引号:
osascript '/Users/123/Desktop/synchRemindersTest.scpt' 'output.json' 'Reminders' 'ToDo'
我没有弄清楚究竟是什么导致了这些问题,但我反复尝试了完全相同的代码,但结果/行为却有所不同,显然取决于各种情况。以下是我的观察结果。
使用任何玩具示例,似乎有 2 个运行行为发生了变化:
- 运行时(我能得到的最快时间接近半分钟,但在某些情况下,相同的代码可能需要 10 分钟以上 - 我控制了它们,所以我不知道它们会运行多长时间)
- 控制台中的 tccd 和其他错误(与 Apple 的“透明度、同意和控制”机制有关 - 即导致这些访问请求弹出窗口的事情)
我尝试通过以下方式运行上述玩具示例:
- 从脚本编辑器
- 通过命令行中的 osascript
- 重写为 Javascript for Automation(又名“JXA”)(来自脚本编辑器)
- 作为应用程序,双击
- 作为从命令行打开的应用程序
我在以下各种情况下(尽可能)运行了这些各种方法:
- 解锁屏幕后立即
- 在当前桌面上打开提醒应用程序
- 在当前桌面外打开提醒应用程序
- 在运行前未手动与提醒 GUI 交互
- 在运行前手动与提醒 GUI 交互
- 在提醒 GUI 中包含显示列表的 applescript 指令
- 在提醒 GUI 中包含显示列表的 applescript 指令
还有一个重要因素需要考虑:
- 提醒数据库大小
Apple 不实际上从未从提醒数据库中删除任何内容。我目前有 9,604 个已完成的提醒和 193 个未完成的提醒。在探索此问题时,我发现我的提醒数据库中有十多年的提醒。
我怀疑这些问题与数据库的大小有关,而不是 tccd 错误,因为我在 Apple 开发者论坛上发现将这些错误描述为日志噪音的帖子。我还发现开发人员的帖子指出,提醒数据库的不断增长会导致性能问题日益严重,并指出没有办法 真正 删除条目。已删除的条目只是标记为已删除。
我发现没有可靠的运行环境可以在任何情况下(当您拥有大型提醒数据库时)快速运行且没有错误。在某些情况下,所有执行方式都会失败。有些案例的运行速度比其他案例快,但没有一个案例的运行时间达到我认为合理的水平。
我尝试对玩具脚本的应用版本进行代码签名,明确授予提醒数据的权利,但根据一款名为 Taccy 的应用,虽然我可以从应用这些权利的文件中检索这些权利,但它们并没有阻止 tccd 错误或使任何案例运行得更快。我甚至尝试对 osascript 可执行文件的副本进行代码签名,但显然它只适用于应用程序包。
虽然我在某些情况下可以看到运行时间的差异,并且可以通过在 某些 情况下以某种方式执行操作来避免 tccd 错误(所有这些似乎都需要真正的手动操作),但运行时间从未显着改善,并且在例如屏幕被锁定的情况下似乎无法避免错误/故障。
因此,我得出结论,考虑到我的提醒数据库的大小以及我想在屏幕锁定的情况下运行此脚本的事实(例如在 cron 作业上),我不得不放弃 AppleScript 解决方案。不可能可预测且可靠地做到这一点。 (我曾在 iOS 设备上简要探索过 Siri 自动化,但发现让它每天运行多次的麻烦太烦人了。)
所以记住提醒曾经是(/曾经是)作为 ics 文件存储在 Library 文件夹中。我了解到,通过 iOS 13 中的提醒更新 & macOS Catalina,提醒存储已移至
~/Library/Reminders/Container_v1/Stores
下的 sqlite 数据库。
昨晚我在数据库中搜索了一下,开始弄清楚。我用谷歌搜索了一些我发现的内容,发现一个谷歌搜索结果是 github repo ,它 已经解决了棘手的 sqlite 问题 。我最终得到了一个 shell 脚本,它可以在大约 1 秒内 可靠地 检索所有提醒数据(近 10k 条记录)!
我尚未完善它以将其转换为 JSON 并另外检索在某个日期之后修改的任何内容,但我目前所拥有的足以回答这个问题。
我用(不流行的)shell 语言
tcsh
编写了 shell 脚本。请随意在
bash
中重写它,或者从
我找到的 repo
开始,它已经在 bash 中(但不会检索所有提醒数据):
set REMINDERS_STORES="$HOME/Library/Reminders/Container_v1/Stores";
set SQL_GET_Z_ENT="SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = 'REMCDList'";
foreach DBFILE ( "$REMINDERS_STORES"/Data-*-*.sqlite )
set DB="file:${DBFILE}?mode=ro"
set COUNT=`sqlite3 "$DB" "SELECT COUNT(*) FROM ZREMCDOBJECT WHERE Z_ENT = ($SQL_GET_Z_ENT) AND ZCKIDENTIFIER IS NOT NULL;"`
if ( "$COUNT" > 0 ) then
set REMINDERS_DB="$DB"
endif
end
set Z_ENT_LISTS=`sqlite3 "$REMINDERS_DB" "$SQL_GET_Z_ENT;"`
set YEARZERO=`date -j -f "%Y-%m-%d %H:%M:%S %z" "2001-01-01 0:0:0 +0000" "+%s"`
set NOW=`date "+%s"`
sqlite3 "$REMINDERS_DB" "SELECT strftime('%Y-%m-%dT%H:%M:%S',($YEARZERO + TASK.ZDUEDATE),'unixepoch') as dueDate, TASK.ZPRIORITY AS priority, TASK.ZTITLE1 AS title, LIST.ZNAME1 AS list, TASK.ZNOTES AS notes, TASK.ZCOMPLETED as completed, strftime('%Y-%m-%dT%H:%M:%S',($YEARZERO + TASK.ZCOMPLETIONDATE),'unixepoch') as completionDate, strftime('%Y-%m-%dT%H:%M:%S',($YEARZERO + TASK.ZCREATIONDATE),'unixepoch') as creationDate, TASK.ZDISPLAYDATEISALLDAY as isAllday, strftime('%Y-%m-%dT%H:%M:%S',($YEARZERO + TASK.ZDISPLAYDATEDATE),'unixepoch') as alldayDate, strftime('%Y-%m-%dT%H:%M:%S',($YEARZERO + TASK.ZLASTMODIFIEDDATE),'unixepoch') as modificationDate, TASK.ZFLAGGED as flagged FROM ZREMCDOBJECT TASK LEFT JOIN ZREMCDOBJECT LIST on TASK.ZLIST = LIST.Z_PK WHERE LIST.Z_ENT = $Z_ENT_LISTS AND LIST.ZMARKEDFORDELETION = 0 AND TASK.ZMARKEDFORDELETION = 0 ORDER BY CASE WHEN TASK.ZDUEDATE IS NULL THEN 1 ELSE 0 END, TASK.ZDUEDATE, TASK.ZPRIORITY;"
这是输出示例:
2011-11-01T18:30:00|0|Pay the rent|ToDo Home Recurring||1|2011-11-03T13:21:00|2017-09-18T16:59:00|0|2011-11-01T22:30:00|2020-01-04T20:40:00|0
2011-11-05T15:45:00|0|Feed meter|Reminders||1|2011-11-06T15:39:00|2017-09-18T16:59:00|0|2011-11-05T19:45:00|2020-01-04T20:36:00|0
请注意,要获取您所在时区的日期,而不是 GMT(/UTC?),请附加“localtime”,例如:
strftime('%Y-%m-%dT%H:%M:%S',($YEARZERO + TASK.ZDUEDATE),'unixepoch', 'localtime')