@@ -279,7 +279,13 @@ async function downloadToFile(netFetch, url, destPath, onProgress) {
279279 * apparent stalls streaming huge files through Node). Fall back to extract-zip.
280280 * Pulse callback keeps the updater UI moving during large single-file writes (e.g. app.asar).
281281 */
282- async function extractPortableZipToDir ( zipPath , extractDir , logFn , pulse , unpackLo , unpackHi ) {
282+ /**
283+ * @param {object } [opts]
284+ * @param {string } [opts.verifyExeBase] If set, after system tar succeeds we require resolveZipAppContentRoot
285+ * to find the app; otherwise we clear and fall back to extract-zip (tar can exit 0 with a bad tree for some zips).
286+ */
287+ async function extractPortableZipToDir ( zipPath , extractDir , logFn , pulse , unpackLo , unpackHi , opts = { } ) {
288+ const verifyExeBase = opts . verifyExeBase ;
283289 const runExtractZip = async ( ) => {
284290 logUpdater ( "extract" , `extract-zip (yauzl) → ${ extractDir } ` ) ;
285291 const extractZip = require ( "extract-zip" ) ;
@@ -354,6 +360,21 @@ async function extractPortableZipToDir(zipPath, extractDir, logFn, pulse, unpack
354360 clearInterval ( hb ) ;
355361 }
356362 logFn ( `[updater] system tar done in ${ Date . now ( ) - t0 } ms` ) ;
363+ if ( verifyExeBase ) {
364+ const root = resolveZipAppContentRoot ( extractDir , verifyExeBase ) ;
365+ if ( ! root ) {
366+ logFn (
367+ `[updater] system tar left no recognizable main exe (wanted basename like ${ verifyExeBase } ); clearing extract dir and using extract-zip` ,
368+ ) ;
369+ logUpdater ( "extract" , "tar output verification failed → extract-zip" ) ;
370+ try {
371+ fs . rmSync ( extractDir , { recursive : true , force : true } ) ;
372+ } catch ( _ ) { }
373+ fs . mkdirSync ( extractDir , { recursive : true } ) ;
374+ await runExtractZip ( ) ;
375+ return ;
376+ }
377+ }
357378 return ;
358379 } catch ( e ) {
359380 logFn ( `[updater] system tar failed (${ e ?. message || e } ); clearing partial extract, retrying with extract-zip` ) ;
@@ -381,18 +402,49 @@ function sha512Base64OfFile(filePath) {
381402function resolveZipAppContentRoot ( extractDir , exeBaseName ) {
382403 const direct = path . join ( extractDir , exeBaseName ) ;
383404 if ( fs . existsSync ( direct ) ) return extractDir ;
384- let entries = [ ] ;
385- try {
386- entries = fs . readdirSync ( extractDir , { withFileTypes : true } ) ;
387- } catch ( _ ) {
388- return null ;
389- }
390- for ( const ent of entries ) {
391- if ( ! ent . isDirectory ( ) ) continue ;
392- const sub = path . join ( extractDir , ent . name ) ;
393- if ( fs . existsSync ( path . join ( sub , exeBaseName ) ) ) return sub ;
405+
406+ /** Names to treat as the main app exe (portable zip vs running binary name can differ). */
407+ const altNames = new Set ( [ exeBaseName ] ) ;
408+ if ( process . platform === "win32" ) {
409+ altNames . add ( "Hyperlinks Space App.exe" ) ;
410+ altNames . add ( "HyperlinksSpaceApp.exe" ) ;
394411 }
395- return null ;
412+
413+ const matchesMainExe = ( fileName ) => {
414+ const lower = fileName . toLowerCase ( ) ;
415+ for ( const n of altNames ) {
416+ if ( lower === n . toLowerCase ( ) ) return true ;
417+ }
418+ return false ;
419+ } ;
420+
421+ /** Prefer shallowest match; skip common subtrees that are not the main exe. */
422+ const hits = [ ] ;
423+ const MAX_DEPTH = 6 ;
424+ const walk = ( dir , depth ) => {
425+ if ( depth > MAX_DEPTH ) return ;
426+ let entries = [ ] ;
427+ try {
428+ entries = fs . readdirSync ( dir , { withFileTypes : true } ) ;
429+ } catch ( _ ) {
430+ return ;
431+ }
432+ for ( const ent of entries ) {
433+ if ( ! ent . isFile ( ) ) continue ;
434+ if ( ! / \. e x e $ / i. test ( ent . name ) ) continue ;
435+ if ( matchesMainExe ( ent . name ) ) hits . push ( { root : dir , depth } ) ;
436+ }
437+ for ( const ent of entries ) {
438+ if ( ! ent . isDirectory ( ) ) continue ;
439+ const n = ent . name . toLowerCase ( ) ;
440+ if ( n === "resources" || n === "locales" ) continue ;
441+ walk ( path . join ( dir , ent . name ) , depth + 1 ) ;
442+ }
443+ } ;
444+ walk ( extractDir , 0 ) ;
445+ if ( hits . length === 0 ) return null ;
446+ hits . sort ( ( a , b ) => a . depth - b . depth || a . root . length - b . root . length ) ;
447+ return hits [ 0 ] . root ;
396448}
397449
398450/**
@@ -841,6 +893,7 @@ function setupAutoUpdater() {
841893 ...partial ,
842894 } ) ;
843895 } ;
896+ let versionsPrepareOk = false ;
844897 try {
845898 const meta = await resolveWindowsZipSidecarMeta ( ( u ) => net . fetch ( u ) , currentVersion ) ;
846899 if ( meta . version !== remoteV ) {
@@ -909,7 +962,9 @@ function setupAutoUpdater() {
909962 percent : UNPACK_PROGRESS_LO ,
910963 } ) ;
911964
912- await extractPortableZipToDir ( zipPath , extractDir , log , pushUi , UNPACK_PROGRESS_LO , UNPACK_PROGRESS_HI ) ;
965+ await extractPortableZipToDir ( zipPath , extractDir , log , pushUi , UNPACK_PROGRESS_LO , UNPACK_PROGRESS_HI , {
966+ verifyExeBase : exeBase ,
967+ } ) ;
913968
914969 pushUi ( { text : "Finalizing…" , percent : 98 } ) ;
915970
@@ -936,9 +991,13 @@ function setupAutoUpdater() {
936991 } ) . show ( ) ;
937992 } catch ( _ ) { }
938993 }
994+ versionsPrepareOk = true ;
939995 } catch ( e ) {
940- logUpdater ( "prepare" , `FAILED ${ e ?. message || e } ` ) ;
941- log ( `[updater] versions sidecar failed: ${ e ?. message || e } ` ) ;
996+ const errMsg = e ?. message || e ;
997+ const errStack = typeof e ?. stack === "string" ? e . stack : "" ;
998+ logUpdater ( "prepare" , `FAILED ${ errMsg } ` ) ;
999+ log ( `[updater] versions sidecar failed: ${ errMsg } ` ) ;
1000+ if ( errStack ) log ( `[updater] versions sidecar stack: ${ errStack . split ( "\n" ) . slice ( 0 , 8 ) . join ( " | " ) } ` ) ;
9421001 log (
9431002 `[updater] Ensure latest GitHub release includes latest.yml, ${ WIN_PORTABLE_ZIP_PREFIX } <version>.zip (zip build), and optionally zip-latest.yml from cleanup for sha512.` ,
9441003 ) ;
@@ -960,7 +1019,12 @@ function setupAutoUpdater() {
9601019 }
9611020 } finally {
9621021 zipPrepareInFlight = false ;
963- logUpdater ( "prepare" , "zipPrepareInFlight=false" ) ;
1022+ logUpdater (
1023+ "prepare" ,
1024+ versionsPrepareOk
1025+ ? "zipPrepareInFlight=false (success)"
1026+ : "zipPrepareInFlight=false (incomplete — look for prepare FAILED above)" ,
1027+ ) ;
9641028 }
9651029 } ;
9661030
0 commit comments