如何 cron 访问提醒的 AppleScript(带参数)

How to cron an AppleScript (with arguments) that accesses Reminders

我写了一个 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 运行 访问提醒?我可以对命令行可执行文件进行代码签名吗?如果我用另一种语言写这个,会不会有同样的问题?

我正在 运行宁卡塔琳娜 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 访问提醒的效率有关。

我也开始注意到,当从脚本编辑器 运行 时,我上面的第一个玩具示例有时会失败。我一直在重试它以获得上面粘贴的 运行ning 时间,我开始感觉到一种模式。我 认为 当我 select 在 Reminders GUI 中查看一个新列表时(哪个并不重要),然后 运行 脚本(来自编辑器),它的工作原理。但是,如果我不 select 要查看的新列表并再次 运行 编辑器中的脚本,它将因该超时而失败。

...但这似乎很疯狂。谁能解释一下这是怎么回事?

注意:我正在编写的脚本实际上是对我编写的 Siri 快捷方式的 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(玩具 4)

I 运行 TOY 4,从 7:25:24 开始,持续 10 分钟,然后控制它。我立即在控制台中看到许多错误。我在控制台中搜索了“提醒”和 here's what I go during the run.

尝试 2(玩具 4)

然后,根据我在提醒 GUI 中单击列表名称时获得的轶事成功的见解,我尝试单击 运行dom 列表,然后立即再次单击 运行 TOY 4。我在 7:38:23 开始玩 TOY 4。在7:44:22,成功了!大约 6 分钟!

控制台中的消息少了很多,none 其中标记为错误。为了便于比较,here are the console results from searching for "Reminders".

讨论

我修改了我对正在发生的事情的理论。鉴于控制台消息,我推断当您从命令行通过 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 秒,就像脚本编辑器中的 运行 一样。当我有时间时,我会再试一次,但手动 select 提醒 GUI 中的列表。

更新 4

我运行刚才又是更新3的那个玩具。在超时前 运行 的 2 分钟内,控制台充满了 52,349 行 mostly this,一遍又一遍地重复,这只是该时间跨度中与搜索词匹配的部分 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)

脚注

* 我一直在有意在不同的桌面上使用 Reminders 应用程序测试我的脚本,因为我在努力中注意到 GUI 脚本总是比通过 Reminders 字典访问更快。所以我写了 2 个方法:GUI 和 Reminders Dict。如果打开的提醒应用程序在桌面上(我一直藏在扩展坞下面),则 GUI 将 运行。如果我们正在全屏观看 Netflix,我有一个 try/catch 当 GUI 在不同的桌面上时使用较慢的 Reminders Dict 访问方法。

用 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'

我没有弄清楚到底是什么导致了问题,但我反复尝试使用不同 results/behaviors 的完全相同的代码,显然取决于各种情况。这是我的观察。

使用任何玩具示例,有 2 个 运行宁行为似乎发生了变化:

  • 运行时间(我能得到的最快时间是将近半分钟,但在某些情况下相同的代码可能需要 10 分钟以上 - 我控制了它们,所以我不知道如何只要他们有 运行)
  • 控制台中的 tccd 和其他错误(与 Apple 的“t运行sparency、consent 和 control”机制有关——即导致这些访问请求弹出窗口的原因)

我尝试运行通过以下方式使用上面的玩具示例:

  • 来自脚本编辑器
  • 从命令行通过 osascript
  • 重写为 Javascript 用于自动化(又名“JXA”)(来自脚本编辑器)
  • 作为应用程序,双击
  • 作为从命令行打开的应用程序

而我运行在以下各种情况下(如果可能)使用那些不同的方法:

  • 解锁屏幕后立即
  • 在当前桌面上打开提醒应用程序
  • 在当前桌面上打开提醒应用程序
  • 在运行宁
  • 之前没有手动与提醒 GUI 交互
  • 在运行宁
  • 之前手动与提醒 GUI 交互
  • 包括一个 applescript 指令以在提醒 GUI 中显示列表
  • 不包含在提醒 GUI 中显示列表的 applescript 指令

还有一个重要因素需要考虑:

  • 提醒数据库大小

Apple 实际上并没有从提醒数据库中删除任何内容。我目前有 9,604 个已完成的提醒和 193 个未完成的提醒。在探索这个问题时,我在我的提醒数据库中发现了十年前的提醒。

我怀疑这些问题更多地与数据库的大小有关,而不是 tccd 错误,因为我在 Apple Developers 论坛上发现将这些错误描述为仅仅是日志噪音的帖子。我还发现开发人员的帖子指出,提醒数据库的大小不断增加会导致性能问题不断增加,并指出无法 really 删除条目。删除的条目只是标记为已删除。

我发现没有可靠的 运行ning 上下文 运行 在任何情况下(当您有一个大型提醒数据库时)都快速且没有错误。在某些情况下,所有执行方式都会失败。有些情况 运行 比其他情况快,但 none 曾经 运行 在我认为合理的 运行 宁时间。

我尝试对玩具脚本的应用程序版本进行代码签名,明确地 g运行 对提醒数据授予权利,但根据名为 Taccy 的应用程序,我可以检索这些权利从应用它们的文件来看,它们并没有阻止 tccd 错误或使任何情况 运行 更快。我什至尝试对 osascript 可执行文件的副本进行代码签名,但显然它只适用于应用程序包。

虽然我可以在某些情况下看到 运行time 的差异,并且可以通过在 some 情况下以某种方式做事来避免 tccd 错误(所有这些似乎都需要真正的手动操作),运行时间从未显着改善,并且 errors/failures 在屏幕被锁定等情况下似乎是不可避免的。

所以我得出结论,鉴于我的提醒数据库的大小以及我想 运行 这个脚本锁定屏幕的事实(例如在 cron 作业上),我不得不放弃 AppleScript 解决方案。不可能以可预测和可靠的方式做到这一点。 (我曾在 iOS 设备上简要探索过 Siri Automation,但发现每天不止一次要跳到 运行 的圈套太烦人了。)

因此请记住,提醒曾(/曾经)作为 ics 文件存储在 Library 文件夹中。我了解到,随着 iOS 13 和 macOS Catalina 中的提醒更新,提醒的存储已移至 ~/Library/Reminders/Container_v1/Stores.

下的 sqlite 数据库

我昨晚在数据库里翻来覆去,开始想办法。我 googled 了一些我发现的东西,发现一个 google 命中 github repo that had already worked the difficult sqlite stuff out。我最终得到了一个 shell 脚本,该脚本 可靠地 在大约 1 秒内检索所有提醒数据(近 10k 条记录)!

我还没有完善它以将其转换为 JSON 并额外检索在特定日期之后修改的任何内容,但到目前为止我所拥有的足以回答这个问题。

我用 tcsh 的(不受欢迎的)shell 语言编写了 shell 脚本。随意在 bash 中重写它或从 repo I found 开始,它已经在 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')