@@ -164,6 +164,113 @@ type FileValidationFailure = {
164164
165165type FileValidationResult = FileValidationSuccess | FileValidationFailure ;
166166
167+ type UpdaterArch = 'x64' | 'ia32' | 'arm64' | 'armv7l' ;
168+
169+ const mapProcessArchToUpdaterArch = ( ) : UpdaterArch | null => {
170+ switch ( process . arch ) {
171+ case 'x64' :
172+ case 'arm64' :
173+ case 'ia32' :
174+ return process . arch ;
175+ case 'arm' :
176+ return 'armv7l' ;
177+ default :
178+ return null ;
179+ }
180+ } ;
181+
182+ const detectArchFromFileName = ( name : string | null | undefined ) : UpdaterArch | null => {
183+ if ( ! name ) {
184+ return null ;
185+ }
186+ const normalized = name . toLowerCase ( ) ;
187+ const tokens = normalized . split ( / [ ^ a - z 0 - 9 ] + / ) . filter ( Boolean ) ;
188+ const hasToken = ( token : string ) => tokens . includes ( token ) ;
189+
190+ if ( hasToken ( 'arm64' ) || hasToken ( 'aarch64' ) ) {
191+ return 'arm64' ;
192+ }
193+ if ( hasToken ( 'armv7l' ) || hasToken ( 'armhf' ) ) {
194+ return 'armv7l' ;
195+ }
196+ if ( hasToken ( 'ia32' ) || hasToken ( 'x86' ) || hasToken ( 'win32' ) ) {
197+ return 'ia32' ;
198+ }
199+ if ( hasToken ( 'x64' ) || hasToken ( 'amd64' ) || hasToken ( 'win64' ) ) {
200+ return 'x64' ;
201+ }
202+
203+ const endsWith32 = / ( ^ | [ ^ 0 - 9 ] ) 3 2 ( $ | [ ^ 0 - 9 ] ) / . test ( normalized ) ;
204+ const endsWith64 = / ( ^ | [ ^ 0 - 9 ] ) 6 4 ( $ | [ ^ 0 - 9 ] ) / . test ( normalized ) ;
205+ if ( endsWith32 && ! endsWith64 ) {
206+ return 'ia32' ;
207+ }
208+ if ( endsWith64 && ! endsWith32 ) {
209+ return 'x64' ;
210+ }
211+ return null ;
212+ } ;
213+
214+ const normalizeNameForComparison = ( name : string ) : string => {
215+ return name . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ] + / g, '-' ) ;
216+ } ;
217+
218+ const determineDownloadedUpdateArch = ( info : UpdateDownloadedEvent , downloadedName : string ) => {
219+ type ArchCandidate = { priority : number ; sources : string [ ] } ;
220+ const candidates = new Map < UpdaterArch , ArchCandidate > ( ) ;
221+ const register = ( arch : UpdaterArch | null , priority : number , source : string ) => {
222+ if ( ! arch ) {
223+ return ;
224+ }
225+ const existing = candidates . get ( arch ) ;
226+ if ( ! existing || priority > existing . priority ) {
227+ candidates . set ( arch , { priority, sources : [ source ] } ) ;
228+ return ;
229+ }
230+ if ( priority === existing . priority ) {
231+ existing . sources . push ( source ) ;
232+ return ;
233+ }
234+ existing . sources . push ( source ) ;
235+ } ;
236+
237+ register ( mapProcessArchToUpdaterArch ( ) , 10 , `process.arch:${ process . arch } ` ) ;
238+ register ( detectArchFromFileName ( downloadedName ) , 40 , `downloadedName:${ downloadedName } ` ) ;
239+
240+ if ( typeof ( info as any ) . path === 'string' ) {
241+ const legacyName = path . basename ( ( info as any ) . path ) ;
242+ register ( detectArchFromFileName ( legacyName ) , 50 , `info.path:${ legacyName } ` ) ;
243+ }
244+
245+ if ( Array . isArray ( info . files ) ) {
246+ for ( const file of info . files ) {
247+ if ( typeof file ?. url !== 'string' ) {
248+ continue ;
249+ }
250+ const urlName = getFileNameFromUrlLike ( file . url ) ;
251+ const arch = detectArchFromFileName ( urlName ) ;
252+ const matchesDownloaded = urlName && normalizeNameForComparison ( urlName ) === normalizeNameForComparison ( downloadedName ) ;
253+ register ( arch , matchesDownloaded ? 100 : 80 , urlName ? `info.files:${ urlName } ` : 'info.files' ) ;
254+ }
255+ }
256+
257+ let selected : { arch : UpdaterArch | null ; sources : string [ ] } = { arch : null , sources : [ ] } ;
258+ for ( const [ arch , candidate ] of candidates . entries ( ) ) {
259+ if ( ! selected . arch || candidate . priority > ( candidates . get ( selected . arch ) ?. priority ?? - Infinity ) ) {
260+ selected = { arch, sources : [ ...candidate . sources ] } ;
261+ }
262+ }
263+ return selected ;
264+ } ;
265+
266+ const filterNamesByArch = ( names : string [ ] , arch : UpdaterArch | null ) : string [ ] => {
267+ if ( ! arch ) {
268+ return names ;
269+ }
270+ const filtered = names . filter ( name => detectArchFromFileName ( name ) === arch ) ;
271+ return filtered . length > 0 ? filtered : names ;
272+ } ;
273+
167274const buildGitHubApiHeaders = async ( ) : Promise < Record < string , string > > => {
168275 const headers : Record < string , string > = {
169276 'Accept' : 'application/vnd.github+json' ,
@@ -401,23 +508,49 @@ const ensureDownloadedFileMatchesOfficialRelease = async (info: UpdateDownloaded
401508 return { success : false , error : 'No official release filenames available for comparison.' , downloadedName, officialNames : [ ] } ;
402509 }
403510
404- if ( officialNames . includes ( downloadedName ) ) {
511+ const { arch : inferredArch , sources : archSources } = determineDownloadedUpdateArch ( info , downloadedName ) ;
512+ const candidateNames = filterNamesByArch ( officialNames , inferredArch ) ;
513+ if ( inferredArch ) {
514+ mainLogger . info ( '[AutoUpdate] Inferred update architecture.' , {
515+ version : info . version ,
516+ inferredArch,
517+ evidence : archSources ,
518+ candidateCount : candidateNames . length ,
519+ } ) ;
520+ } else {
521+ mainLogger . info ( '[AutoUpdate] Unable to infer update architecture from metadata.' , {
522+ version : info . version ,
523+ availableCandidates : officialNames . length ,
524+ } ) ;
525+ }
526+
527+ if ( candidateNames . includes ( downloadedName ) ) {
405528 mainLogger . info ( '[AutoUpdate] Downloaded filename already matches official release asset.' , {
406529 downloadedName,
407530 } ) ;
408- return { success : true , filePath : downloadedPath , expectedName : downloadedName , officialNames } ;
531+ return { success : true , filePath : downloadedPath , expectedName : downloadedName , officialNames : candidateNames } ;
409532 }
410533
411- const caseInsensitiveMatch = officialNames . find ( name => name . toLowerCase ( ) === downloadedName . toLowerCase ( ) ) ;
534+ const caseInsensitiveMatch = candidateNames . find ( name => name . toLowerCase ( ) === downloadedName . toLowerCase ( ) ) ;
412535 if ( caseInsensitiveMatch ) {
413536 mainLogger . info ( '[AutoUpdate] Downloaded filename matches an official asset ignoring case.' , {
414537 downloadedName,
415538 expectedName : caseInsensitiveMatch ,
416539 } ) ;
417- return { success : true , filePath : downloadedPath , expectedName : caseInsensitiveMatch , officialNames } ;
540+ return { success : true , filePath : downloadedPath , expectedName : caseInsensitiveMatch , officialNames : candidateNames } ;
418541 }
419542
420- const expectedName = officialNames [ 0 ] ;
543+ const normalizedDownloaded = normalizeNameForComparison ( downloadedName ) ;
544+ const normalizedMatch = candidateNames . find ( name => normalizeNameForComparison ( name ) === normalizedDownloaded ) ;
545+ if ( normalizedMatch ) {
546+ mainLogger . info ( '[AutoUpdate] Downloaded filename matches official asset after normalization.' , {
547+ downloadedName,
548+ expectedName : normalizedMatch ,
549+ } ) ;
550+ return { success : true , filePath : downloadedPath , expectedName : normalizedMatch , officialNames : candidateNames } ;
551+ }
552+
553+ const expectedName = candidateNames [ 0 ] ;
421554 const expectedPath = path . join ( path . dirname ( downloadedPath ) , expectedName ) ;
422555 try {
423556 await safeRenameDownloadedUpdate ( downloadedPath , expectedPath ) ;
@@ -433,16 +566,17 @@ const ensureDownloadedFileMatchesOfficialRelease = async (info: UpdateDownloaded
433566 previousName : downloadedName ,
434567 expectedName,
435568 helperAdjusted : Boolean ( helper ) ,
569+ normalizedAttempted : Boolean ( normalizedMatch ) ,
436570 } ) ;
437- return { success : true , filePath : expectedPath , expectedName, renamed : true , officialNames } ;
571+ return { success : true , filePath : expectedPath , expectedName, renamed : true , officialNames : candidateNames } ;
438572 } catch ( error : any ) {
439573 mainLogger . error ( '[AutoUpdate] Failed to align downloaded filename with official asset.' , {
440574 version : info . version ,
441575 downloadedName,
442576 expectedName,
443577 error : error instanceof Error ? { message : error . message , stack : error . stack } : { message : String ( error ) } ,
444578 } ) ;
445- return { success : false , error : error ?. message || String ( error ) , downloadedName, officialNames } ;
579+ return { success : false , error : error ?. message || String ( error ) , downloadedName, officialNames : candidateNames } ;
446580 }
447581} ;
448582
@@ -497,16 +631,19 @@ app.on('ready', async () => {
497631
498632 // --- Auto-updater logic ---
499633 if ( app . isPackaged ) {
500- mainLogger . info ( `Configuring auto-updater. allowPrerelease: ${ settings . allowPrerelease } ` ) ;
501- autoUpdater . allowPrerelease = settings . allowPrerelease ?? true ;
634+ const allowPrerelease = settings . allowPrerelease ?? true ;
635+ mainLogger . info ( '[AutoUpdate] Configuring auto-updater.' , {
636+ allowPrerelease,
637+ } ) ;
638+ autoUpdater . allowPrerelease = allowPrerelease ;
502639
503640 autoUpdater . on ( 'checking-for-update' , ( ) => {
504- mainLogger . info ( 'Checking for update.. .' ) ;
641+ mainLogger . info ( '[AutoUpdate] Checking for update.' ) ;
505642 mainWindow ?. webContents . send ( 'update-status-change' , { status : 'checking' , message : 'Checking for updates...' } ) ;
506643 } ) ;
507644 autoUpdater . on ( 'update-available' , ( info ) => {
508645 lastDownloadedUpdateValidation = null ;
509- mainLogger . info ( 'Update available.' , {
646+ mainLogger . info ( '[AutoUpdate] Update available.' , {
510647 version : info . version ,
511648 files : Array . isArray ( info . files ) ? info . files . map ( file => ( {
512649 url : typeof file ?. url === 'string' ? getFileNameFromUrlLike ( file . url ) : undefined ,
@@ -517,14 +654,14 @@ app.on('ready', async () => {
517654 mainWindow ?. webContents . send ( 'update-status-change' , { status : 'available' , message : `Update v${ info . version } available. Downloading...` } ) ;
518655 } ) ;
519656 autoUpdater . on ( 'update-not-available' , ( info ) => {
520- mainLogger . info ( 'Update not available.' , {
657+ mainLogger . info ( '[AutoUpdate] No update available.' , {
521658 version : info ?. version ,
522659 downloadedFile : info ?. downloadedFile ,
523660 } ) ;
524661 } ) ;
525662 autoUpdater . on ( 'error' , ( err ) => {
526663 lastDownloadedUpdateValidation = null ;
527- mainLogger . error ( 'Error in auto-updater.' , {
664+ mainLogger . error ( '[AutoUpdate] Error from auto-updater.' , {
528665 message : err ?. message ,
529666 stack : err ?. stack ,
530667 name : err ?. name ,
@@ -534,11 +671,15 @@ app.on('ready', async () => {
534671 mainWindow ?. webContents . send ( 'update-status-change' , { status : 'error' , message : `Error in auto-updater: ${ err . message } ` } ) ;
535672 } ) ;
536673 autoUpdater . on ( 'download-progress' , ( progressObj ) => {
537- const log_message = `Downloaded ${ progressObj . percent . toFixed ( 2 ) } %` ;
538- mainLogger . debug ( log_message ) ;
674+ mainLogger . debug ( '[AutoUpdate] Download progress update.' , {
675+ percent : Number . isFinite ( progressObj . percent ) ? Number ( progressObj . percent . toFixed ( 2 ) ) : progressObj . percent ,
676+ transferred : ( progressObj as any ) ?. transferred ,
677+ total : ( progressObj as any ) ?. total ,
678+ bytesPerSecond : ( progressObj as any ) ?. bytesPerSecond ,
679+ } ) ;
539680 } ) ;
540681 autoUpdater . on ( 'update-downloaded' , ( info ) => {
541- mainLogger . info ( 'Update downloaded event received. Validating filename.' , {
682+ mainLogger . info ( '[AutoUpdate] Update downloaded event received. Validating filename.' , {
542683 version : info . version ,
543684 downloadedFile : info . downloadedFile ,
544685 files : Array . isArray ( info . files ) ? info . files . map ( file => ( {
@@ -573,7 +714,7 @@ app.on('ready', async () => {
573714 validated : true ,
574715 } ;
575716
576- mainLogger . info ( 'Update downloaded .' , {
717+ mainLogger . info ( '[AutoUpdate] Update validated and ready to install .' , {
577718 version : info . version ,
578719 filePath : validationResult . filePath ,
579720 alignedWithOfficialName : validationResult . renamed === true ,
@@ -594,14 +735,14 @@ app.on('ready', async () => {
594735 } ) ;
595736
596737 // Check for updates
597- mainLogger . info ( 'Triggering auto-updater checkForUpdatesAndNotify call .' ) ;
738+ mainLogger . info ( '[AutoUpdate] Triggering checkForUpdatesAndNotify.' ) ;
598739 autoUpdater . checkForUpdatesAndNotify ( )
599740 . then ( result => {
600741 if ( ! result ) {
601- mainLogger . info ( 'Auto-updater checkForUpdatesAndNotify resolved without update info.' ) ;
742+ mainLogger . info ( '[AutoUpdate] checkForUpdatesAndNotify completed without update info.' ) ;
602743 return ;
603744 }
604- mainLogger . info ( 'Auto-updater checkForUpdatesAndNotify resolved.' , {
745+ mainLogger . info ( '[AutoUpdate] checkForUpdatesAndNotify resolved.' , {
605746 updateInfo : result . updateInfo ? {
606747 version : result . updateInfo . version ,
607748 files : Array . isArray ( result . updateInfo . files ) ? result . updateInfo . files . map ( file => ( {
@@ -614,15 +755,15 @@ app.on('ready', async () => {
614755 } ) ;
615756 } )
616757 . catch ( error => {
617- mainLogger . error ( 'checkForUpdatesAndNotify rejected.' , {
758+ mainLogger . error ( '[AutoUpdate] checkForUpdatesAndNotify rejected.' , {
618759 message : error ?. message ,
619760 stack : error ?. stack ,
620761 name : error ?. name ,
621762 } ) ;
622763 mainWindow ?. webContents . send ( 'update-status-change' , { status : 'error' , message : `Failed to check for updates: ${ error ?. message || error } ` } ) ;
623764 } ) ;
624765 } else {
625- mainLogger . info ( 'App is not packaged, skipping auto-updater.' ) ;
766+ mainLogger . info ( '[AutoUpdate] App is not packaged; skipping auto-updater.' ) ;
626767 }
627768} ) ;
628769
0 commit comments