install-direct-sync-launchagent.mjs 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738
  1. #!/usr/bin/env node
  2. import {mkdir,copyFile,chmod,readFile,writeFile,stat,realpath} from 'node:fs/promises';
  3. import {homedir} from 'node:os';
  4. import {join} from 'node:path';
  5. import {spawnSync} from 'node:child_process';
  6. const label='cn.xiaoshu.legacy-direct-sync-nightly',home=homedir();
  7. const root=join(home,'Library/Application Support/XiaoshuSync'),env=join(home,'.config/xiaoshu/legacy-sync-nightly.env'),state=join(home,'.local/state/xiaoshu/legacy-sync');
  8. if(process.platform!=='darwin')throw Error('This installer requires macOS');
  9. if(new Intl.DateTimeFormat().resolvedOptions().timeZone!=='Asia/Shanghai')throw Error('macOS timezone must be Asia/Shanghai for the 23:00 calendar schedule');
  10. if((await stat(env)).mode%512!==384)throw Error('Credential environment must have mode 0600');
  11. await mkdir(join(root,'scripts/lib'),{recursive:true,mode:0o700});await mkdir(state,{recursive:true,mode:0o700});
  12. for(const file of ['run-nightly-direct-sync.mjs','lib/legacy-direct-projection.mjs','lib/legacy-direct-specs.mjs']){const dest=join(root,'scripts',file);await copyFile(new URL(file,import.meta.url),dest);await chmod(dest,0o600);}
  13. const node=await realpath(process.execPath),script=join(root,'scripts/run-nightly-direct-sync.mjs');
  14. const escape=s=>s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;');
  15. const xml=`<?xml version="1.0" encoding="UTF-8"?>
  16. <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  17. <plist version="1.0"><dict>
  18. <key>Label</key><string>${label}</string>
  19. <key>ProgramArguments</key><array><string>${escape(node)}</string><string>--env-file=${escape(env)}</string><string>${escape(script)}</string><string>--scheduled</string></array>
  20. <key>WorkingDirectory</key><string>${escape(root)}</string>
  21. <key>StartCalendarInterval</key><dict><key>Hour</key><integer>23</integer><key>Minute</key><integer>0</integer></dict>
  22. <key>RunAtLoad</key><true/>
  23. <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
  24. <key>ThrottleInterval</key><integer>300</integer>
  25. <key>ProcessType</key><string>Background</string>
  26. <key>StandardOutPath</key><string>${escape(join(state,'launchagent.log'))}</string>
  27. <key>StandardErrorPath</key><string>${escape(join(state,'launchagent-error.log'))}</string>
  28. <key>EnvironmentVariables</key><dict><key>TZ</key><string>Asia/Shanghai</string><key>NO_PROXY</key><string>server.xiaoshu.pro,localhost,127.0.0.1,::1</string></dict>
  29. <key>Umask</key><integer>63</integer>
  30. </dict></plist>`;
  31. const agents=join(home,'Library/LaunchAgents');await mkdir(agents,{recursive:true});const path=join(agents,label+'.plist');await writeFile(path,xml,{mode:0o600});
  32. const lint=spawnSync('/usr/bin/plutil',['-lint',path],{encoding:'utf8'});if(lint.status!==0)throw Error('LaunchAgent plist validation failed');
  33. const domain='gui/'+process.getuid(),target=domain+'/'+label;
  34. spawnSync('/bin/launchctl',['bootout',target],{encoding:'utf8'});
  35. const installed=spawnSync('/bin/launchctl',['bootstrap',domain,path],{encoding:'utf8'});if(installed.status!==0)throw Error('LaunchAgent bootstrap failed: '+installed.stderr);
  36. const check=spawnSync('/bin/launchctl',['print',target],{encoding:'utf8'});if(check.status!==0||!check.stdout.includes(label)||!check.stdout.includes(script))throw Error('LaunchAgent readback failed');
  37. await writeFile(join(state,'launchagent-install.json'),JSON.stringify({label,path,installedAt:new Date().toISOString(),timeZone:'Asia/Shanghai',hour:23,minute:0,node,script,credentialMode:'0600',readbackVerified:true},null,2),{mode:0o600});
  38. console.log(JSON.stringify({label,installed:true,timeZone:'Asia/Shanghai',schedule:'23:00',runtime:root,systemReadbackVerified:true}));