-
Notifications
You must be signed in to change notification settings - Fork 3
/
IPSW Updater.jxa
4434 lines (3579 loc) · 230 KB
/
IPSW Updater.jxa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// Created by Pico Mitchell (of Free Geek)
//
// https://ipsw.app
// https://github.com/freegeek-pdx/IPSW-Updater
//
// Uses: IPSW Downloads API (https://ipsw.me) by Callum Jones (https://ipsw.me/about)
// Icon: “Phone with Arrow” from Fluent Emoji (https://github.com/microsoft/fluentui-emoji) by Microsoft (https://opensource.microsoft.com) licensed under the MIT License (https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE)
//
// MIT License
//
// Copyright (c) 2022 Free Geek
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// NOTES:
// "use strict" is set and "AppKit" is imported in "main.scpt".
// "LSMinimumSystemVersion" is set to 10.13 in "Build IPSW Updater" (make sure to only use ECMAScript 2018 features, which is what macOS 10.13 High Sierra supports).
// 10.12.2+ Compatible Emoji List: https://emojipedia.org/apple/ios-10.2/ (To make sure that only emoji that are available on at least macOS 10.13 High Sierra are used.)
// Emoji Used: 🛑⚙️🚫🗑⏰🔌👤✅🔄⚖️🔍📲⏳⌛️⏭️🍏↩️↪️🗓💤⚠️❌👉📝⤴️👍⏱️📌📏
const appVersion = '2024.11.20-2'
// @ts-ignore: JXA-ObjC
ObjC.import('unistd') // For "getuid" function.
// @ts-ignore: JXA-ObjC
ObjC.import('Security') // For "SecStaticCodeCheckValidity" and other functions for code signature validation.
// @ts-ignore: "Application.currentApplication()" is set to "a" and "a.includeStandardAdditions" is set to "true" in "main.scpt" which runs this script via "exec()".
const app = a
// @ts-ignore: "$.NSBundle.mainBundle" is set to "ab" in "main.scpt" which runs this script via "exec()".
const appBundle = ab
// @ts-ignore: "ab.bundlePath.js" is set to "ap" in "main.scpt" which runs this script via "exec()".
let appPath = ap
wakeMacUp()
const appName = appBundle.objectForInfoDictionaryKey('CFBundleName').js
const bundleExecutableName = appBundle.objectForInfoDictionaryKey('CFBundleExecutable').js
// @ts-ignore: JXA-ObjC
const fileManager = $.NSFileManager.defaultManager
if (!fileManager.fileExistsAtPath(`${appPath}/Contents/MacOS/${bundleExecutableName}`) || !fileManager.fileExistsAtPath(`${appPath}/Contents/Resources/Scripts/main.scpt`) || !fileManager.fileExistsAtPath(`${appPath}/Contents/Resources/Launch ${bundleExecutableName}`)) {
debugLog('App Check Error', appPath)
while (!macIsAwakeAndUnlocked()) // AppleScript dialogs will timeout early if Mac is asleep when it's displayed. So, wait until Mac is awake to display the dialog.
// @ts-ignore: JXA
delay(15)
try {
app.activate()
app.displayAlert(`${appName}: App Check Error`,
{
message: `${appName} must be run as an application.`,
as: 'critical',
buttons: ['Quit', `Re-Download “${appName}”`],
cancelButton: 1,
defaultButton: 2
}
)
app.doShellScript('/usr/bin/open https://ipsw.app/download/')
} catch (ignoredError) { /* Ignore */ }
app.quit()
}
const bundleIdentifier = appBundle.bundleIdentifier.js
const intendedBundleIdentifier = `org.freegeek.${bundleExecutableName.replace(/ /g, '-')}`
if (bundleIdentifier != intendedBundleIdentifier) {
debugLog('Incorrect Bundle Identifier Error', bundleIdentifier)
while (!macIsAwakeAndUnlocked()) // AppleScript dialogs will timeout early if Mac is asleep when it's displayed. So, wait until Mac is awake to display the dialog.
// @ts-ignore: JXA
delay(15)
try {
app.activate()
app.displayAlert(`${appName}: Bundle Identifier Error`,
{
message: `Intended Bundle Identifier:
${intendedBundleIdentifier}
Actual Bundle Identifier:
${bundleIdentifier}`,
as: 'critical',
buttons: ['Quit', `Re-Download “${appName}”`],
cancelButton: 1,
defaultButton: 2
}
)
app.doShellScript('/usr/bin/open https://ipsw.app/download/')
} catch (ignoredError) { /* Ignore */ }
app.quit()
}
const infoDictVersion = appBundle.objectForInfoDictionaryKey('CFBundleShortVersionString').js
if (infoDictVersion != appVersion) {
debugLog(`Incorrect App Version Error: ${infoDictVersion} != ${appVersion}`)
while (!macIsAwakeAndUnlocked()) // AppleScript dialogs will timeout early if Mac is asleep when it's displayed. So, wait until Mac is awake to display the dialog.
// @ts-ignore: JXA
delay(15)
try {
app.activate()
app.displayAlert(`${appName}: App Version Error`,
{
message: `Intended App Version:
${appVersion}
Actual App Version:
${infoDictVersion}`,
as: 'critical',
buttons: ['Quit', `Re-Download “${appName}”`],
cancelButton: 1,
defaultButton: 2
}
)
app.doShellScript('/usr/bin/open https://ipsw.app/download/')
} catch (ignoredError) { /* Ignore */ }
app.quit()
}
// @ts-ignore: JXA-ObjC
const processInfo = $.NSProcessInfo.processInfo
const isMojaveOrNewer = processInfo.isOperatingSystemAtLeastVersion({majorVersion: 10, minorVersion: 14, patchVersion: 0})
const isCatalinaOrNewer = processInfo.isOperatingSystemAtLeastVersion({majorVersion: 10, minorVersion: 15, patchVersion: 0})
const isBigSurOrNewer = processInfo.isOperatingSystemAtLeastVersion({majorVersion: 11, minorVersion: 0, patchVersion: 0})
const isMontereyOrNewer = processInfo.isOperatingSystemAtLeastVersion({majorVersion: 12, minorVersion: 0, patchVersion: 0})
const isVenturaOrNewer = processInfo.isOperatingSystemAtLeastVersion({majorVersion: 13, minorVersion: 0, patchVersion: 0})
const isSonomaOrNewer = processInfo.isOperatingSystemAtLeastVersion({majorVersion: 14, minorVersion: 0, patchVersion: 0})
const isSequoiaOrNewer = processInfo.isOperatingSystemAtLeastVersion({majorVersion: 15, minorVersion: 0, patchVersion: 0})
try {
// IMPORTANT: On each launch, confirm the running app is properly signed (and notarized, when possible on macOS 10.14 Mojave and newer).
// See comments in the "validateCodeSignatureForAppAtPath" function for more information.
validateCodeSignatureForAppAtPath(appPath) // NOTE: This function with "throw" an error if validation fails which will be caught and displayed below.
} catch (codeSignatureValidationError) {
debugLog('Code Signature Error', codeSignatureValidationError)
while (!macIsAwakeAndUnlocked()) // AppleScript dialogs will timeout early if Mac is asleep when it's displayed. So, wait until Mac is awake to display the dialog.
// @ts-ignore: JXA
delay(15)
try {
app.activate()
app.displayAlert(`${appName}: Code Signature Error`,
{
message: codeSignatureValidationError.message,
as: 'critical',
buttons: ['Quit', `Re-Download “${appName}”`],
cancelButton: 1,
defaultButton: 2
}
)
app.doShellScript('/usr/bin/open https://ipsw.app/download/')
} catch (ignoredError) { /* Ignore */ }
app.quit()
}
if (isVenturaOrNewer) {
// @ts-ignore: JXA-ObjC
ObjC.import('ServiceManagement') // For "SMAppService" to check if the LaunchAgent is enabled on macOS 13 Ventura (see comments in "launchAgentIsDisabledOnVenturaOrNewer()" function for more information).
}
// @ts-ignore: JXA-ObjC
const mainMenu = $.NSApp.mainMenu
try { mainMenu.removeItem(mainMenu.itemWithTitle('File')) } catch (ignoredError) { /* Ignore */ }
try {
const appMenu = mainMenu.itemWithTitle('applet').submenu
try {
const preferencesMenuItem = appMenu.itemWithTitle('Preferences…')
if (!preferencesMenuItem.isNil()) appMenu.removeItem(preferencesMenuItem)
else if (isVenturaOrNewer) appMenu.removeItem(appMenu.itemWithTitle('Settings…')) // This menu item will be "Settings…" instead of "Preferences…" when built with and running on macOS 13 Ventura.
} catch (ignoredError) { /* Ignore */ }
try { appMenu.removeItem(appMenu.itemWithTitle(`Quit ${appName}`)) } catch (ignoredError) { /* Ignore */ }
try {
// NOTE: This "Quit and Keep Windows" menu item (which is the alternate menu item for the "Quit" menu item, accessed by holding the Option key)
// will only be created and therefore only need to be remove IF the app menu was opened BEFORE this code had a chance to run,
// which isn't super likely but can still happen so it should still always be checked and removed if it exists.
const quitAndKeepWindowsMenuItem = appMenu.itemWithTitle('Quit and Keep Windows')
if (!quitAndKeepWindowsMenuItem.isNil()) appMenu.removeItem(quitAndKeepWindowsMenuItem) // Check if isn't nil before removing since it might not exist and "removeItem" will log an error if nil is passed to it.
} catch (ignoredError) { /* Ignore */ }
} catch (ignoredError) { /* Ignore */ }
try {
const editMenu = mainMenu.itemWithTitle('Edit').submenu
try { editMenu.removeItem(editMenu.itemWithTitle('Edit Script')) } catch (ignoredError) { /* Ignore */ }
} catch (ignoredError) { /* Ignore */ }
// @ts-ignore: JXA-ObjC
const currentUserName = $.NSUserName().js
// @ts-ignore: JXA-ObjC
const currentUserID = $.getuid()
// @ts-ignore: JXA-ObjC
const standardUserDefaults = $.NSUserDefaults.standardUserDefaults
standardUserDefaults.registerDefaults({
RefusedMoveToApplications: false,
FirstRun: true,
AppJustAutoUpdated: false,
AppUpdateAvailable: false,
ExcludeProducts: [],
IncludeVersions: 7,
ManagedFirmwares: {},
FirmwareUpdatesAvailable: 0,
AcknowledgedSignificantResults: true,
ImmediatelyDeleteOutdatedFirmwares: false,
ForceDialogWindowStyle: false,
})
let uuid = getPreference('UUID', 'string')
if (!uuid) {
// @ts-ignore: JXA-ObjC
uuid = $.NSUUID.UUID.UUIDString.js
setPreference('UUID', uuid, 'string')
}
// @ts-ignore: JXA-ObjC
const sharedWorkspace = $.NSWorkspace.sharedWorkspace
const settingsOrPreferencesName = (isVenturaOrNewer ? 'Settings' : 'Preferences')
const systemPreferencesOrSettingsName = `System ${settingsOrPreferencesName}`
const curlPIDs = [] // Save CURL PIDs as they are spawned to be able kill them all when an error is caught or before quitting just in case any are left behind before being removed from this list after they are known to not be running anymore.
// @ts-ignore: JXA-ObjC
const applicationSupportFolder = `${fileManager.URLsForDirectoryInDomains($.NSApplicationSupportDirectory, $.NSUserDomainMask).firstObject.path.js}/${appName}`
if (!fileManager.fileExistsAtPath(applicationSupportFolder))
// @ts-ignore: JXA-ObjC
fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(applicationSupportFolder, true, {}, $())
const temporaryFilesFolder = `${applicationSupportFolder}/Temporary Files`
// @ts-ignore: JXA-ObjC
const userTrashPath = fileManager.URLsForDirectoryInDomains($.NSTrashDirectory, $.NSUserDomainMask).firstObject.path.js
const appIsQuarantined = (appPath.includes('/AppTranslocation/') || app.doShellScript(`/usr/bin/xattr ${quoteForShell(appPath)}`).includes('com.apple.quarantine'))
let appPaths = []
try { appPaths = app.doShellScript(`/usr/bin/mdfind kMDItemCFBundleIdentifier = ${bundleIdentifier}`).split(/\n|\r/) } catch (ignoredError) { /* Ignore */ }
// @ts-ignore: JXA-ObjC
let parentDirectoryToMe = $(appPath).stringByDeletingLastPathComponent.js
if (appPath.includes('/AppTranslocation/')) {
let mostRecentlyOpenedAppPath = appPath
let mostRecentlyOpenedAppDate = new Date('1984-01-24')
for (const thisAppPath of appPaths)
try {
if (!thisAppPath.includes('/AppTranslocation/')) {
let appLastUsedDateString = app.doShellScript(`/usr/bin/mdls -raw -name kMDItemLastUsedDate ${quoteForShell(thisAppPath)}`)
// kMDItemLastUsedDate will be "(null)" if it was never launched (which would happen when launching a quarantined app), so lets check the date added.
if (!appLastUsedDateString || (appLastUsedDateString == "(null)"))
appLastUsedDateString = app.doShellScript(`/usr/bin/mdls -raw -name kMDItemDateAdded ${quoteForShell(thisAppPath)}`)
const appLastUsedDate = new Date(appLastUsedDateString.slice(0, -6).replace(' ', 'T'))
if (appLastUsedDate > mostRecentlyOpenedAppDate) {
mostRecentlyOpenedAppDate = appLastUsedDate
mostRecentlyOpenedAppPath = thisAppPath
}
}
} catch (ignoredError) { /* Ignore */ }
if (appPath != mostRecentlyOpenedAppPath) {
appPath = mostRecentlyOpenedAppPath
// @ts-ignore: JXA-ObjC
parentDirectoryToMe = $(appPath).stringByDeletingLastPathComponent.js
}
}
try {
// Set Touch Bar settings to NOT be "App Controls" for this Bundle ID because AppleScript alert and dialog buttons don't update properly on the Touch Bar.
// @ts-ignore: JXA-ObjC
const touchBarPrefs = $.NSUserDefaults.alloc.initWithSuiteName('com.apple.touchbar.agent')
// @ts-ignore: JXA-ObjC
const touchBarPrefsPresentationModePerAppDict = $.NSMutableDictionary.dictionaryWithDictionary(touchBarPrefs.dictionaryForKey('PresentationModePerApp'))
if (touchBarPrefsPresentationModePerAppDict.objectForKey(bundleIdentifier).js != 'fullControlStrip') {
touchBarPrefsPresentationModePerAppDict.setObjectForKey('fullControlStrip', bundleIdentifier)
touchBarPrefs.setObjectForKey(touchBarPrefsPresentationModePerAppDict, 'PresentationModePerApp')
}
} catch (ignoredError) { /* Ignore */ }
// Always "killall ControlStrip" even if PresentationModePerApp was set on a previous launch
// because for some reason the setting won't take effect until the app loses and regains focus,
// or if I just go ahead and "killall ControlStrip" right away.
try { app.doShellScript('/usr/bin/killall ControlStrip') } catch (ignoredError) { /* Ignore */ }
if (!getPreference('RefusedMoveToApplications', 'bool') && !appPath.startsWith('/Applications/')) {
while (!macIsAwakeAndUnlocked()) // AppleScript dialogs will timeout early if Mac is asleep when it's displayed. So, wait until Mac is awake to display the dialog.
// @ts-ignore: JXA
delay(15)
// @ts-ignore: JXA
delay(0.5)
app.activate()
try {
let parentDirectoryName = fileManager.displayNameAtPath(parentDirectoryToMe).js
if (parentDirectoryToMe.endsWith('/Applications')) parentDirectoryName = 'User Applications'
const moveToApplicationsDialogButtons = [
((appPath.includes('/AppTranslocation/') || (parentDirectoryName == 'UNKNOWN')) ? 'No, Run from Current Folder' : `No, Run from “${parentDirectoryName}” Folder`),
'Reveal in Finder & Quit',
'Yes, Move to “Applications” Folder'
]
const moveToApplicationsDialogReply = displayAlertOrDialogDependingOnOS(`${appName} is not properly installed in the “Applications” folder.
Would you like ${appName} to move itself to the “Applications” folder?`,
`${((appPath.includes('/AppTranslocation/') || (parentDirectoryName == 'UNKNOWN')) ? '' : `${appName} is currently running from within the “${parentDirectoryName}” folder: ${parentDirectoryToMe}/
`)}Like all applications, ${appName} should be run from within the “Applications” folder.`,
`${appName} — Install Application`,
'caution',
moveToApplicationsDialogButtons,
2,
3
)
if (moveToApplicationsDialogReply.buttonReturned == moveToApplicationsDialogButtons[2]) {
if (!fileManager.fileExistsAtPath(temporaryFilesFolder))
// @ts-ignore: JXA-ObjC
fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(temporaryFilesFolder, true, {}, $())
// @ts-ignore: JXA-ObjC
const appFileName = $(appPath).lastPathComponent.js
const moveToApplicationsFolderScript = `/usr/bin/osascript -e ${quoteForShell(`
use AppleScript version "2.7"
use scripting additions
use framework "Foundation"
set currentAppFilePath to ${quoteForAppleScript(appPath)}
delay 0.5
repeat while (application currentAppFilePath is running)
delay 0.5
end repeat
${((appIsQuarantined && (appPaths.length > 0)) ? `
try
do shell script "/usr/bin/xattr -drs com.apple.quarantine " & ${appPaths.map(thisAppPath => `(quoted form of ${quoteForAppleScript(thisAppPath)})`).join(' & " " & ')}
end try
` : '')}
set appInTempFolder to ${quoteForAppleScript(`${temporaryFilesFolder}/${appFileName}`)}
set systemApplicationsFolder to (POSIX path of (path to applications folder from system domain))
set appInSystemApplicationsFolder to (systemApplicationsFolder & ${quoteForAppleScript(appFileName)})
set actualInstallFilePath to appInSystemApplicationsFolder
set userApplicationsFolder to (POSIX path of (path to applications folder from user domain))
set appInUserApplicationsFolder to (userApplicationsFolder & ${quoteForAppleScript(appFileName)})
set userDownloadsFolder to (POSIX path of (path to downloads folder from user domain))
set appInUserDownloadsFolder to (userDownloadsFolder & ${quoteForAppleScript(appFileName)})
set adminPermissionPrompt to "“${quoteForAppleScript(appName, true)}” requires Admin Permission to move itself to the “Applications” folder."
try
set appOriginalFileStructure to ""
try
set appOriginalFileStructure to (do shell script ("cd " & (quoted form of currentAppFilePath) & " && /bin/ls -Rsk"))
end try
set fileManager to (defaultManager of NSFileManager of current application)
try
if (fileExistsAtPath_(appInTempFolder) of fileManager) then
trashItemAtURL_resultingItemURL_error_((fileURLWithPath_(appInTempFolder) of NSURL of current application), missing value, missing value) of fileManager
if (fileExistsAtPath_(appInTempFolder) of fileManager) then do shell script "/bin/rm -rf " & (quoted form of appInTempFolder) with prompt adminPermissionPrompt with administrator privileges
end if
end try
try
if ((fileExistsAtPath_(currentAppFilePath) of fileManager) and (not (fileExistsAtPath_(appInTempFolder) of fileManager))) then moveItemAtPath_toPath_error_(currentAppFilePath, appInTempFolder, missing value) of fileManager -- This fails when currentAppFilePath is in /AppTranslocation/
end try
try
if ((fileExistsAtPath_(appInUserDownloadsFolder) of fileManager) and (not (fileExistsAtPath_(appInTempFolder) of fileManager))) then moveItemAtPath_toPath_error_(appInUserDownloadsFolder, appInTempFolder, missing value) of fileManager -- Fall back on this even though its not the best assumption.
end try
try
if ((fileExistsAtPath_(currentAppFilePath) of fileManager) and (not (fileExistsAtPath_(appInTempFolder) of fileManager))) then do shell script "/bin/mv -f " & (quoted form of currentAppFilePath) & " " & (quoted form of appInTempFolder) with prompt adminPermissionPrompt with administrator privileges -- This fails when currentAppFilePath is in /AppTranslocation/
end try
if ((fileExistsAtPath_(appInUserDownloadsFolder) of fileManager) and (not (fileExistsAtPath_(appInTempFolder) of fileManager))) then do shell script "/bin/mv -f " & (quoted form of appInUserDownloadsFolder) & " " & (quoted form of appInTempFolder) with prompt adminPermissionPrompt with administrator privileges -- Fall back on this even though its not the best assumption.
${(appIsQuarantined ? `
try
if (fileExistsAtPath_(appInTempFolder) of fileManager) then do shell script "/usr/bin/xattr -drs com.apple.quarantine " & (quoted form of appInTempFolder)
end try
` : '')}
try
if ((fileExistsAtPath_(appInTempFolder) of fileManager) and (fileExistsAtPath_(appInSystemApplicationsFolder) of fileManager)) then
trashItemAtURL_resultingItemURL_error_((fileURLWithPath_(appInSystemApplicationsFolder) of NSURL of current application), missing value, missing value) of fileManager
if (fileExistsAtPath_(appInSystemApplicationsFolder) of fileManager) then do shell script "/bin/rm -rf " & (quoted form of appInSystemApplicationsFolder) with prompt adminPermissionPrompt with administrator privileges
end if
end try
try
if ((fileExistsAtPath_(appInTempFolder) of fileManager) and (fileExistsAtPath_(appInUserApplicationsFolder) of fileManager)) then
trashItemAtURL_resultingItemURL_error_((fileURLWithPath_(appInUserApplicationsFolder) of NSURL of current application), missing value, missing value) of fileManager
if (fileExistsAtPath_(appInUserApplicationsFolder) of fileManager) then do shell script "/bin/rm -rf " & (quoted form of appInUserApplicationsFolder) with prompt adminPermissionPrompt with administrator privileges
end if
end try
try
if ((fileExistsAtPath_(appInTempFolder) of fileManager) and (fileExistsAtPath_(appInUserDownloadsFolder) of fileManager)) then
trashItemAtURL_resultingItemURL_error_((fileURLWithPath_(appInUserDownloadsFolder) of NSURL of current application), missing value, missing value) of fileManager
if (fileExistsAtPath_(appInUserDownloadsFolder) of fileManager) then do shell script "/bin/rm -rf " & (quoted form of appInUserDownloadsFolder) with prompt adminPermissionPrompt with administrator privileges
end if
end try
try
if ((fileExistsAtPath_(appInTempFolder) of fileManager) and (not (fileExistsAtPath_(appInSystemApplicationsFolder) of fileManager))) then
moveItemAtPath_toPath_error_(appInTempFolder, appInSystemApplicationsFolder, missing value) of fileManager
if ((fileExistsAtPath_(appInTempFolder) of fileManager) and (not (fileExistsAtPath_(appInSystemApplicationsFolder) of fileManager))) then do shell script "/bin/mv -f " & (quoted form of appInTempFolder) & " " & (quoted form of appInSystemApplicationsFolder) with prompt adminPermissionPrompt with administrator privileges
end if
end try
try
if ((fileExistsAtPath_(appInTempFolder) of fileManager) and (not (fileExistsAtPath_(appInUserApplicationsFolder) of fileManager))) then
moveItemAtPath_toPath_error_(appInTempFolder, appInUserApplicationsFolder, missing value) of fileManager
if ((fileExistsAtPath_(appInTempFolder) of fileManager) and (not (fileExistsAtPath_(appInUserApplicationsFolder) of fileManager))) then do shell script "/bin/mv -f " & (quoted form of appInTempFolder) & " " & (quoted form of appInUserApplicationsFolder) with prompt adminPermissionPrompt with administrator privileges
set actualInstallFilePath to appInUserApplicationsFolder
end if
end try
if ((fileExistsAtPath_(appInTempFolder) of fileManager) and (not (fileExistsAtPath_(appInUserDownloadsFolder) of fileManager))) then
moveItemAtPath_toPath_error_(appInTempFolder, appInUserDownloadsFolder, missing value) of fileManager
if ((fileExistsAtPath_(appInTempFolder) of fileManager) and (not (fileExistsAtPath_(appInUserDownloadsFolder) of fileManager))) then do shell script "/bin/mv -f " & (quoted form of appInTempFolder) & " " & (quoted form of appInUserDownloadsFolder) with prompt adminPermissionPrompt with administrator privileges
set actualInstallFilePath to appInUserDownloadsFolder
end if
if (fileExistsAtPath_(actualInstallFilePath) of fileManager) then
-- When working auto-updating for MacLand Scripts, Catalina seems to fail to launch if done too quickly after a move (with an "executable not found" error).
-- Generating the install path file structure alone seems to delay enough to avoid the issue, but this loop makes it extra safe.
repeat 30 times
try
set installAppFileStructure to ""
try
set installAppFileStructure to (do shell script ("cd " & (quoted form of actualInstallFilePath) & " && /bin/ls -Rsk"))
on error
try
set installAppFileStructure to (do shell script ("cd " & (quoted form of actualInstallFilePath) & " && /bin/ls -Rsk") with prompt adminPermissionPrompt with administrator privileges)
on error
exit repeat
end try
end try
if (appOriginalFileStructure is equal to installAppFileStructure) then exit repeat
end try
delay 0.5
end repeat
end if
on error moveError
activate
beep
display alert "Error Moving “${quoteForAppleScript(appName, true)}” to “Applications” Folder" message moveError as critical
end try
do shell script "/bin/rm -rf " & (quoted form of ${quoteForAppleScript(temporaryFilesFolder)})
try
do shell script "/usr/bin/open -na " & (quoted form of actualInstallFilePath)
on error
try
do shell script "/usr/bin/open -na " & (quoted form of currentAppFilePath)
on error
try
do shell script "/usr/bin/open -nb ${bundleIdentifier}"
on error launchError
activate
beep
display alert "Error Re-Launching “${quoteForAppleScript(appName, true)}”\nAfter Moving to “Applications” Folder" message launchError buttons {"Quit", "Re-Download “${quoteForAppleScript(appName, true)}”"} cancel button 1 default button 2 as critical
do shell script "/usr/bin/open 'https://ipsw.app/download/'"
end try
end try
end try
`)} > /dev/null 2>&1 &`
//debugDialog(moveToApplicationsFolderScript.replace(/\/usr\/bin\/osascript -e '|' > \/dev\/null 2>&1 &/g, '').replace(/'\\''/g, "'").trim())
//debugDialog(app.doShellScript(moveToApplicationsFolderScript.replace('/usr/bin/osascript -e ', "printf '%s' ").replace(' > /dev/null 2>&1 &', '')).trim())
app.doShellScript(moveToApplicationsFolderScript)
app.quit()
} else if (moveToApplicationsDialogReply.buttonReturned == moveToApplicationsDialogButtons[0]) {
setPreference('RefusedMoveToApplications', true, 'bool')
if (appIsQuarantined && (appPaths.length > 0)) {
app.doShellScript(`/usr/bin/osascript -e ${quoteForShell(`
delay 0.5
repeat while (application ${quoteForAppleScript(appPath)} is running)
delay 0.5
end repeat
try
do shell script "/usr/bin/xattr -drs com.apple.quarantine " & ${appPaths.map(thisAppPath => `(quoted form of ${quoteForAppleScript(thisAppPath)})`).join(' & " " & ')}
end try
try
do shell script "/usr/bin/open -na " & (quoted form of ${quoteForAppleScript(appPath)})
on error
try
do shell script "/usr/bin/open -nb ${bundleIdentifier}"
end try
end try
`)} > /dev/null 2>&1 &`)
app.quit()
}
}
} catch (moveToApplicationsError) {
if (moveToApplicationsError.errorNumber !== -128)
debugLog('Move to Applications Folder Error', moveToApplicationsError)
if (appPath.includes('/AppTranslocation/'))
app.beep()
else
// @ts-ignore: JXA-ObjC
try { sharedWorkspace.activateFileViewerSelectingURLs($.NSArray.arrayWithObject($.NSURL.fileURLWithPath(appPath))) } catch (ignoredError) { app.beep() }
if (appIsQuarantined && (appPaths.length > 0))
app.doShellScript(`/usr/bin/osascript -e ${quoteForShell(`
delay 0.5
repeat while (application ${quoteForAppleScript(appPath)} is running)
delay 0.5
end repeat
try
do shell script "/usr/bin/xattr -drs com.apple.quarantine " & ${appPaths.map(thisAppPath => `(quoted form of ${quoteForAppleScript(thisAppPath)})`).join(' & " " & ')}
end try
`)} > /dev/null 2>&1 &`)
if (moveToApplicationsError.errorNumber !== -128)
throw moveToApplicationsError
else
app.quit()
}
}
let justInstalledUpdate = getPreference('AppJustAutoUpdated', 'bool')
if (justInstalledUpdate) setPreference('AppJustAutoUpdated', false, 'bool')
let lastInstall = getPreference('LastAppInstall', 'dict')
const versionLastRun = ((lastInstall && lastInstall.NewVersion) ? lastInstall.NewVersion : undefined)
if (versionLastRun != appVersion) {
const lastInstallInfo = {NewVersion: appVersion, LaunchDate: new Date()}
if (versionLastRun) {
lastInstallInfo.OldVersion = versionLastRun
justInstalledUpdate = true
}
setPreference('LastAppInstall', lastInstallInfo, 'dict')
}
const everyProductType = ['iPhone', 'iPad', 'iPod touch', 'Apple TV', 'HomePod mini', 'Apple Vision', 'T2 Mac (iBridge Firmware)', 'Apple Silicon Mac']
const everyDayOfTheWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
const defaultAutoLaunchHour = 18
const defaultAutoLaunchMinute = 30 // NOTE: DO NOT change this to less that 15 since the auto-wake time will always be 15 minutes before and that math only does a basic subtraction of these minutes without worrying about moving to a previous hour.
const daysAsLettersForPMSet = ['M', 'T', 'W', 'R', 'F', 'S', 'U']
const autoWakeTypeTranslation = {wakepoweron: 'Start Up'/* or Wake*/, wake: 'Wake', poweron: 'Start Up', sleep: 'Sleep', restart: 'Restart', shutdown: 'Shut Down'}
/** @type Intl.DateTimeFormatOptions */
const dateFormatOptions = {weekday: 'short', month: 'long', day: 'numeric', year: 'numeric'}
let shouldImmediatelyDeleteOutdatedFirmwares = getPreference('ImmediatelyDeleteOutdatedFirmwares', 'bool')
const isFirstRun = getPreference('FirstRun', 'bool')
let userDidCancel = false
// @ts-ignore: JXA-ObjC
const userLibraryFolder = fileManager.URLsForDirectoryInDomains($.NSLibraryDirectory, $.NSUserDomainMask).firstObject.path.js
const launchAgentsPath = `${userLibraryFolder}/LaunchAgents`
const launchAgentPlistPath = `${launchAgentsPath}/${bundleIdentifier}.plist`
if (fileManager.fileExistsAtPath(launchAgentPlistPath)) { // Check if a LaunchAgent exists and update it if needed. See comments below for more info.
const currentAutoLaunchSchedule = parseLaunchAgentSchedule()
createLaunchAgentWithDaysAndHour(currentAutoLaunchSchedule.days, currentAutoLaunchSchedule.hour)
// NOTES ABOUT CHECKING AND UPDATING LAUNCHAGENT (IF NEEDED) ON EVERY LAUNCH:
// The "createLaunchAgentWithDaysAndHour" function with the same schedule that is already set will check the existing LaunchAgent and make sure it matches the current structure and
// expected values and update it if needed, which is very important for the "Program" path in case the app is moved since the launcher path must point to the current app location to work.
// If the existing LaunchAgent matches the exact expected LaunchAgent structure and values, it WILL NOT be updated or re-bootstrapped.
// Calling the "createLaunchAgentWithDaysAndHour" function will also update any LaunchAgents made by previous versions to the currently used structure
// which would be used when creating a new LaunchAgent, such as the following changes (and fixes):
// LaunchAgents created prior to version 2022.10.21-1 specified "ProgramArguments" instead of a single "Program" path to the signed launcher script
// and also didn't have "AssociatedBundleIdentifiers" specified for compatibility with macOS 13 Ventura.
// LaunchAgents updated on launch by version 2022.11.17-1 and 2022.11.23-1 would accidentally/incorrectly have their "integer" values converted to "real" values
// (because of running "deepUnwrap" on the NSDictionary of the plist, see comments in "createLaunchAgentWithDaysAndHour" function for more info about this JS int conversion issue)
// which would not get parsed correctly by "launchd" and cause the app to incorrectly be automatically launched every minute or every hour.
}
const libraryITunesFolder = `${userLibraryFolder}/iTunes`
if (!fileManager.fileExistsAtPath(libraryITunesFolder))
// @ts-ignore: JXA-ObjC
fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(libraryITunesFolder, true, {}, $())
const appleConfiguratorFirmwareFolder = `${userLibraryFolder}/Group Containers/K36BKF7T3D.group.com.apple.configurator/Library/Caches/Firmware`
if (!fileManager.fileExistsAtPath(appleConfiguratorFirmwareFolder) && !needsFullDiskAccess()) // On macOS 15 Sequoia, there is a new "would like to access data from other apps" prompt that comes up when accessing the Apple Configurator Group Container each run unless the app has Full Disk Access.
// @ts-ignore: JXA-ObjC
fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(appleConfiguratorFirmwareFolder, true, {}, $())
// @ts-ignore: JXA-ObjC
try { fileManager.removeItemAtPathError(temporaryFilesFolder, $()) } catch (ignoredError) { /* Ignore */ }
if (isFirstRun) {
for ( ; ; ) {
while (!macIsAwakeAndUnlocked()) // AppleScript dialogs will timeout early if Mac is asleep when it's displayed. So, wait until Mac is awake to display the dialog.
// @ts-ignore: JXA
delay(15)
app.activate()
try {
const welcomeDialogReply = displayAlertOrDialogDependingOnOS(`Welcome to ${appName}!
Made by Pico Mitchell of Free Geek (freegeek.org) using the IPSW Downloads API (ipsw.me) by Callum Jones.
${appName} can batch download all the latest IPSW Firmware files from Apple and place them in the correct iTunes Software Updates or Apple Configurator Firmware folders so that they are automatically found by ${isCatalinaOrNewer ? 'Finder' : 'iTunes'} and/or Apple Configurator.
You can set which IPSW Firmware files to download based on versions as well as product types, such as ${everyProductType.slice(0, -1).join(', ')}, and ${everyProductType.slice(-1)[0]}. You can also set ${appName} to run automatically at a scheduled time so that all of your IPSW Firmware files will always be kept up-to-date.`,
`If you agree, ${appName} will take over management of your iTunes Software Updates and Apple Configurator Firmware folders. You can use the button below to view these folders in Finder.
iPhone, iPad, and iPod touch IPSW Firmware files will be stored within iTunes Software Updates folders at “${libraryITunesFolder}” since they will be found by both ${isCatalinaOrNewer ? 'Finder' : 'iTunes'} and Apple Configurator at that location.
Apple TV, HomePod mini, Apple Vision, and T2 and Apple Silicon Mac IPSW Firmware files will be stored within the Apple Configurator Firmware folder at “${appleConfiguratorFirmwareFolder}”. This is because unlike iPhone, iPad, and iPod touch IPSW Firmware files, Apple Configurator will not detect or use IPSW Firmware files in the iTunes Software Updates folders for any other device types.${(isSonomaOrNewer ? '' : ` Also, T2 and Apple Silicon Macs cannot be restored by ${isCatalinaOrNewer ? 'Finder' : 'iTunes'} and can only be restored by Apple Configurator when they are put into DFU mode.`)}
Any IPSW Firmware files that are already in these folders will become managed by ${appName}. That means that IPSW Firmware files in these locations will be moved to the Trash (or deleted, based on your ${settingsOrPreferencesName.toLowerCase()}) when they become out-of-date after an update for them has been downloaded by ${appName}. Also, as described above, any iPhone, iPad, and iPod touch IPSW Firmware files that are currently in the Apple Configurator Firmware folder will be moved into their correct iTunes Software Updates folder and any IPSW Firmware files in the iTunes Software Updates folders for any other device types will be moved into the Apple Configurator Firmware folder. Finally, any IPSW Firmware files currently in these folders that are out-of-date but are still signed by Apple will be left alone and any that are no longer signed by Apple will be moved to the Trash (or deleted). ${appName} will check for unsigned IPSW Firmware files each time it runs, so existing files may be trashed (or deleted) in the future if and when Apple no longer signs them.
To use ${appName}, you must agree to let it manage your iTunes Software Updates and Apple Configurator Firmware folders and to update itself (app updates are checked on each launch). Once you agree, you'll be prompted to choose your desired ${settingsOrPreferencesName.toLowerCase()}, such as Excluded Product Types, Included iOS / iPadOS / tvOS Versions, Trash or Delete Outdated Files, Auto-Launch Schedule, and Start Up Schedule.`,
`${appName} — Welcome`,
'note',
['View iTunes Software Updates and Apple Configurator Firmware Folders', 'Quit', `Agree & Choose ${settingsOrPreferencesName}`],
2,
3
)
if (welcomeDialogReply.buttonReturned == `Agree & Choose ${settingsOrPreferencesName}`)
break
else if (welcomeDialogReply.buttonReturned == 'View iTunes Software Updates and Apple Configurator Firmware Folders') {
// @ts-ignore: JXA-ObjC
sharedWorkspace.openURL($.NSURL.fileURLWithPath(libraryITunesFolder))
// @ts-ignore: JXA-ObjC
sharedWorkspace.openURL($.NSURL.fileURLWithPath(appleConfiguratorFirmwareFolder))
}
} catch (welcomeDialogError) {
if (welcomeDialogError.errorNumber === -128)
cleanUpAndQuitEarly()
else {
debugLog('Welcome Dialog Error', welcomeDialogError)
break // Just start if somehow times out or other error
}
}
}
promptForFullDiskAccessIfNeeded()
promptForPreferences()
setPreference('FirstRun', false, 'bool')
} else
promptForFullDiskAccessIfNeeded()
if (!fileManager.fileExistsAtPath(appleConfiguratorFirmwareFolder))
// @ts-ignore: JXA-ObjC
fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(appleConfiguratorFirmwareFolder, true, {}, $())
Progress.description = `
${(justInstalledUpdate ? `✅ Updated to Version ${appVersion}!` : (isFirstRun ? `📲 Welcome to ${appName}!` : '🔄 Checking for App Updates'))}`
Progress.additionalDescription = ''
Progress.completedUnitCount = 0
Progress.totalUnitCount = -1
let progressWindow = undefined
// @ts-ignore: JXA-ObjC
for (const thisWindow of $.NSApp.windows.js)
if (thisWindow.title.js == appName) {
for (const thisSubview of thisWindow.contentView.subviews.js)
// @ts-ignore: JXA-ObjC
if ($.NSStringFromClass(thisSubview.class).js == 'NSProgressIndicator') { // Check for NSProgressIndicator to not accidentally choose the "startup screen" window in case the app was launched when the Control key was being held.
progressWindow = thisWindow
// @ts-ignore: JXA-ObjC
progressWindow.standardWindowButton($.NSWindowZoomButton).enabled = false
break
}
if (progressWindow) break
}
const releaseNotesURL = 'https://ipsw.app/download/updates.php'
const appUpdatesJsonURL = `${releaseNotesURL}?current_version=${appVersion}&os=${app.systemInfo().systemVersion}&uuid=${uuid}`
if (!justInstalledUpdate && !isFirstRun)
checkAndInstallAppUpdates()
else {
setProgressStopButtonEnabled(false)
app.activate()
if (justInstalledUpdate) {
if (macIsAwakeAndUnlocked()) { // Skip update installed confirmation prompt if Mac IS NOT awake and unlocked.
const appUpdatesJsonFilePath = `${temporaryFilesFolder}/app-updates.json`
const appUpdatesJsonProgressOutputFile = `${temporaryFilesFolder}/app-updates.json-progress`
// @ts-ignore: JXA-ObjC
const readAppUpdatesFileError = $()
try {
if (!fileManager.fileExistsAtPath(temporaryFilesFolder))
// @ts-ignore: JXA-ObjC
fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(temporaryFilesFolder, true, {}, $())
const downloadAppUpdatesJsonPID = app.doShellScript(`/usr/bin/curl --connect-timeout 5 -fL ${quoteForShell(`${appUpdatesJsonURL}&just_updated=1`)} -o ${quoteForShell(appUpdatesJsonFilePath)} > ${quoteForShell(appUpdatesJsonProgressOutputFile)} 2>&1 & echo $!`)
curlPIDs.push(downloadAppUpdatesJsonPID)
try {
while (app.doShellScript(`kill -0 ${downloadAppUpdatesJsonPID}; echo $?`) == 0)
// @ts-ignore: JXA
delay(0.25)
} catch (waitingForAppUpdatesJsonError) {
debugLog('Waiting for App Updates JSON Error', waitingForAppUpdatesJsonError)
try { app.doShellScript(`kill ${downloadAppUpdatesJsonPID}`) } catch (ignoredError) { /* Ignore */ }
throw waitingForAppUpdatesJsonError
}
curlPIDs.splice(curlPIDs.indexOf(downloadAppUpdatesJsonPID), 1)
// @ts-ignore: JXA-ObjC
const appUpdatesInfo = JSON.parse($.NSString.stringWithContentsOfFileEncodingError(appUpdatesJsonFilePath, $.NSUTF8StringEncoding, readAppUpdatesFileError).js)
// NOTE: NOT using "ObjC.deepUnwrap($.NSJSONSerialization.JSONObjectWithDataOptionsError(...))" for consistency with other JSON parsing which don't use it for other reasons which aren't really important here (this JSON parse really would be fine with either technique).
if (!Array.isArray(appUpdatesInfo)) throw new Error('App Updates JSON Is NOT an Array')
// @ts-ignore: JXA-ObjC
try { fileManager.removeItemAtPathError(temporaryFilesFolder, $()) } catch (ignoredError) { /* Ignore */ }
let newVersionReleaseNotes = 'No Release Notes'
if (appUpdatesInfo.length > 0)
for (const thisAppUpdate of appUpdatesInfo)
if (thisAppUpdate.version == appVersion) {
if (thisAppUpdate.releasenotes)
newVersionReleaseNotes = thisAppUpdate.releasenotes
break
}
closeProgressWindow()
if (macIsAwakeAndUnlocked()) {
for ( ; ; ) {
app.activate()
try {
displayAlertOrDialogDependingOnOS(`Welcome to ${appName} Version ${appVersion}!`,
`Version ${appVersion} Release Notes:
${newVersionReleaseNotes}`,
`${appName} — Updated to ${appVersion}`,
'note',
['View All Release Notes', 'Continue'],
1,
2,
60
)
break
} catch (appUpdatedDialogError) {
if (appUpdatedDialogError.errorNumber === -128)
// @ts-ignore: JXA-ObjC
sharedWorkspace.openURL($.NSURL.URLWithString(releaseNotesURL))
else
break
}
}
}
} catch (appUpdateError) {
closeProgressWindow()
let appUpdateErrorMessage = appUpdateError.message
if (!readAppUpdatesFileError.isNil()) appUpdateError = readAppUpdatesFileError.description.js
try {
// @ts-ignore: JXA-ObjC
const progressOutputParts = $.NSString.stringWithContentsOfFileEncodingError(appUpdatesJsonProgressOutputFile, $.NSUTF8StringEncoding, $()).js.split('curl: ')
if (progressOutputParts.length > 1)
appUpdateErrorMessage = `CURL Error: ${progressOutputParts.slice(-1)[0].trim()}`
} catch (ignoredError) { /* Ignore */ }
// @ts-ignore: JXA-ObjC
try { fileManager.removeItemAtPathError(temporaryFilesFolder, $()) } catch (ignoredError) { /* Ignore */ }
debugLog(`Confirm App Update Error: ${appUpdateErrorMessage}`, appUpdateError)
}
} else
try {
// But, even if Mac IS NOT awake and unlocked still do the "curl" call to "appUpdatesJsonURL" with the new version parameters,
// (but do not wait for it to finish or save the output) just so that the successful app update is still saved in the server log.
app.doShellScript(`/usr/bin/curl --connect-timeout 5 -sfL ${quoteForShell(`${appUpdatesJsonURL}&just_updated=2`)} > /dev/null 2>&1 &`) // "just_updated=2" indicates an update when the Mac is asleep or locked (while "just_updated=1" indicates updated while awake an unlocked).
} catch (ignoredError) { /* Ignore */ }
} else {
// @ts-ignore: JXA
delay(0.5)
closeProgressWindow()
}
}
let lastAppUpdatedNote = ''
try {
lastInstall = getPreference('LastAppInstall', 'dict')
if (lastInstall && lastInstall.OldVersion && lastInstall.NewVersion && lastInstall.LaunchDate) {
const oneWeekAgoDate = new Date()
oneWeekAgoDate.setDate(oneWeekAgoDate.getDate() - 7)
if (lastInstall.LaunchDate.getTime() >= oneWeekAgoDate.getTime())
lastAppUpdatedNote = `
⤴️ App Last Updated:
🗓 ${lastInstall.LaunchDate.toLocaleDateString('en-US', dateFormatOptions)} @ ${lastInstall.LaunchDate.toLocaleTimeString('en-US')}`
}
} catch (ignoredError) { /* Ignore */ }
const updatesLogPath = `${applicationSupportFolder}/Updates Log.json`
let updatesLog = []
if (fileManager.fileExistsAtPath(updatesLogPath)) {
// @ts-ignore: JXA-ObjC
const readUpdateLogFileError = $()
try {
// @ts-ignore: JXA-ObjC
updatesLog = JSON.parse($.NSString.stringWithContentsOfFileEncodingError(updatesLogPath, $.NSUTF8StringEncoding, readUpdateLogFileError).js)
// NOTE: NOT using "ObjC.deepUnwrap($.NSJSONSerialization.JSONObjectWithDataOptionsError(...))" to read this JSON to the file since it alters the order of the keys within dicts when written later, which should be preserved since it's the desired dispay order of this human readable "Updates Log.json" file.
if (!Array.isArray(updatesLog)) throw new Error('Parsed JSON Is NOT an Array')
} catch (readUpdatesLogError) {
let readUpdatesLogErrorMessage = readUpdatesLogError.message
if (!readUpdateLogFileError.isNil()) readUpdatesLogError = readUpdateLogFileError.description.js
// @ts-ignore: JXA-ObjC
try { fileManager.removeItemAtPathError(updatesLogPath, $()) } catch (ignoredError) { /* Ignore */ } // Delete any invalid Updates Log since it would just get overwritten anyways.
debugLog(`Read "Updates Log.json" Error: ${readUpdatesLogErrorMessage}`, readUpdatesLogError)
updatesLog = []
}
}
let lastFirmwareUpdateCheck = `
🔍 Last Checked:`
const lastFirmwareUpdateCheckDate = getPreference('LastFirmwareUpdateCheck', 'date')
if (lastFirmwareUpdateCheckDate) {
try {
const firmwareUpdatesAvailable = getPreference('FirmwareUpdatesAvailable', 'int')
lastFirmwareUpdateCheck += `
🗓 ${lastFirmwareUpdateCheckDate.toLocaleDateString('en-US', dateFormatOptions)} @ ${lastFirmwareUpdateCheckDate.toLocaleTimeString('en-US')}${(
(firmwareUpdatesAvailable && (firmwareUpdatesAvailable > 0)) ? `
⤴️ ${firmwareUpdatesAvailable} Firmware Update${(firmwareUpdatesAvailable == 1) ? '' : 's'} Available` : '')}`
} catch (ignoredError) {
lastFirmwareUpdateCheck += `
UNKNOWN`
}
} else
lastFirmwareUpdateCheck += `
Never`
let lastUpdateSummary = ''
let lastErrorSummary = ''
for (const thisUpdate of updatesLog)
if (thisUpdate.Status)
if (thisUpdate.Status.includes('Updated') || thisUpdate.Status.includes('Initial')) {
try {
const lastDownloadedFilesCount = Object.keys(thisUpdate.Downloaded.Files).length
if (lastDownloadedFilesCount > 0) {
if (lastFirmwareUpdateCheck.includes(thisUpdate.Times.Start)) lastFirmwareUpdateCheck = ''
const lastSkippedDownloadsCount = (thisUpdate.Downloaded.Skipped ? parseInt(thisUpdate.Downloaded.Skipped) : 0)
const lastDownloadErrorsCount = Object.keys(thisUpdate.Errors).length
lastUpdateSummary = `
↩️ ${(thisUpdate.Status.includes('Updated') ? `Last ${thisUpdate.Status}` : thisUpdate.Status)}:
🗓 ${thisUpdate.Times.Start}
📲 Updated ${lastDownloadedFilesCount} IPSW File${((lastDownloadedFilesCount == 1) ? '' : 's')} (${thisUpdate.Downloaded['Total Size']})${createVersionsDownloadedSummary(thisUpdate.Downloaded.Files, 1)}${((lastSkippedDownloadsCount > 0) ? `
⏭️ Skipped ${lastSkippedDownloadsCount} Update${((lastSkippedDownloadsCount == 1) ? '' : 's')}` : '')}${((lastDownloadErrorsCount > 0) ? `
⚠️ ${lastDownloadErrorsCount} Download Error${((lastDownloadErrorsCount == 1) ? '' : 's')} Occurred` : '')}`
}
} catch (ignoredError) { /* Ignore */ }
break
} else if (!lastErrorSummary && thisUpdate.Status.includes('Error')) {
try {
const lastDownloadErrorKeys = Object.keys(thisUpdate.Errors)
const lastDownloadErrorsCount = lastDownloadErrorKeys.length
if (lastDownloadErrorsCount > 0) {
if (lastFirmwareUpdateCheck.includes(thisUpdate.Times.Start)) lastFirmwareUpdateCheck = ''
const firstLastDownloadErrorObject = thisUpdate.Errors[lastDownloadErrorKeys[0]]
lastErrorSummary = `
↪️ Last ${thisUpdate.Status}:
🗓 ${thisUpdate.Times.Start}
${((thisUpdate.Status == 'Load Error') ?
`⚠️ ${(Array.isArray(firstLastDownloadErrorObject) ? firstLastDownloadErrorObject[0] : firstLastDownloadErrorObject).replace('CURL Error: (6) ', '').trim()}` :
`⚠️ ${lastDownloadErrorsCount} Download Error${((lastDownloadErrorsCount == 1) ? '' : 's')} Occurred`)}`
}
} catch (ignoredError) { /* Ignore */ }
}
if (macIsAwakeAndUnlocked()) { // Skip launch window if asleep or locked (just start checking for updates immediately).
const autoStartSeconds = 45
for ( ; ; ) {
const currentExcludedProducts = getPreference('ExcludeProducts', 'string array') // This returned excluded product will always be sanitized to a valid value by "getPreference()".
const currentIncludedVersions = getPreference('IncludeVersions', 'int') // This returned version will always be sanitized to a valid value by "getPreference()".
let currentAutoLaunchScheduleDisplay = createDisplayScheduleFor(parseLaunchAgentSchedule())
if ((currentAutoLaunchScheduleDisplay != 'Never') && launchAgentIsDisabledOnVenturaOrNewer())
currentAutoLaunchScheduleDisplay += `
⚠️\tManually Disabled in System Settings`
const currentAutoWakeSchedules = parseAutoWakePowerEventPreferences()
let currentAutoWakeOnScheduleDisplay = createDisplayScheduleFor(currentAutoWakeSchedules.On)
// @ts-ignore: JXA-ObjC
if ((currentAutoWakeOnScheduleDisplay != 'Never') && ($.NSUserDefaults.alloc.initWithSuiteName('/Library/Preferences/com.apple.loginwindow').stringForKey('autoLoginUser').js != currentUserName))
currentAutoWakeOnScheduleDisplay += `
⚠️\tAuto-Login Must Be Enabled\n\t\t\tto Auto-Launch on Start Up`
let currentAutoWakeOffScheduleDisplay = createDisplayScheduleFor(currentAutoWakeSchedules.Off)
if ((currentAutoWakeOffScheduleDisplay != 'Never') && hasConflictingPowerSchedules(currentAutoWakeSchedules))
currentAutoWakeOffScheduleDisplay += `
⚠️\tShut Down Schedule May Conflict\n\t\t\twith Start Up Schedule`
app.activate()
let includedVersionsDisplay = ''
const iOSincludedVersions = []
if (!currentExcludedProducts.includes('iPhone') || !currentExcludedProducts.includes('iPod touch'))
iOSincludedVersions.push('iOS')
if (!currentExcludedProducts.includes('iPad'))
iOSincludedVersions.push('iPadOS')
if (!currentExcludedProducts.includes('Apple TV'))
iOSincludedVersions.push('tvOS')
if (iOSincludedVersions.length > 0)
includedVersionsDisplay += `${iOSincludedVersions.join(' / ')} ${currentIncludedVersions} and Newer`
if (!currentExcludedProducts.includes('HomePod mini') || !currentExcludedProducts.includes('Apple Vision') || !currentExcludedProducts.includes('T2 Mac (iBridge Firmware)') || !currentExcludedProducts.includes('Apple Silicon Mac')) {
const otherOSincludedVersions = []
if (!currentExcludedProducts.includes('HomePod mini'))
otherOSincludedVersions.push('audioOS')
if (!currentExcludedProducts.includes('Apple Vision'))
otherOSincludedVersions.push('visionOS')
if (!currentExcludedProducts.includes('T2 Mac (iBridge Firmware)'))
otherOSincludedVersions.push('bridgeOS')
if (!currentExcludedProducts.includes('Apple Silicon Mac'))
otherOSincludedVersions.push('macOS')
if (includedVersionsDisplay) includedVersionsDisplay += '\n\t\t'
includedVersionsDisplay += `Latest ${((otherOSincludedVersions.length <= 2) ? otherOSincludedVersions.join(' and ') : `${otherOSincludedVersions.slice(0, -1).join(', ')},\n\t\tand ${otherOSincludedVersions.slice(-1)[0]}`)}`
}
try {
const countDownDialogReply = displayAlertOrDialogDependingOnOS(`${appName} will start in ${autoStartSeconds} seconds…`,
`Version ${appVersion}${lastAppUpdatedNote}
📲 Firmware Updates Overview:${lastFirmwareUpdateCheck}${lastErrorSummary.replace(/\n|\r/g, '\n\t')}${lastUpdateSummary.replace(/\n|\r/g, '\n\t')}
⚙️ ${settingsOrPreferencesName} Overview:
🚫 Excluded Product Types:
${((currentExcludedProducts.length == 0) ? 'None' : currentExcludedProducts.join(', ').replace(', HomePod', ',\n\t\tHomePod').replace(', T2', ',\n\t\tT2').replace(', Apple Silicon', ',\n\t\tApple Silicon'))}
🍏 Included Versions:
${includedVersionsDisplay}
🗑 Outdated IPSW Files:
${shouldImmediatelyDeleteOutdatedFirmwares ? 'Immediately Deleted' : 'Trashed'}
⏰ Auto-Launch Schedule:
${currentAutoLaunchScheduleDisplay}