Skip to content

Commit 689fc0d

Browse files
committed
refactor(scanner): type the worker path and align app-test results
Move the worker scanner surface into TypeScript, add a direct worker regression, and make the version=2 app-test path populate the same visible result data and final status as the legacy scanner. This keeps the refactor bounded while making the worker route safe to exercise. Constraint: Must preserve the existing Apple Silicon app-test behavior while changing the worker internals Rejected: Flip production to the worker path immediately | still needs the normal deploy path and broader production soak Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Keep the version=2 adapter using the shared finishFileScan path until the legacy scanner can be removed entirely Tested: pnpm run typecheck; pnpm exec vitest run test/scanner/client.test.ts; pnpm run test:browser (original workspace); netlify build --context deploy-preview (original workspace) Not-tested: Browser suite from the clean clone environment (local Astro dev server startup timed out there)
1 parent 0480c47 commit 689fc0d

File tree

10 files changed

+869
-657
lines changed

10 files changed

+869
-657
lines changed

helpers/app-files-scanner.js

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { isString } from './check-types.js'
77
import parseMacho from './macho/index.js'
88

99
// Vite Web Workers - https://vitejs.dev/guide/features.html#web-workers
10-
import { runScanWorker } from '~/helpers/scanner/client.mjs'
10+
import { runScanWorker } from '~/helpers/scanner/client'
1111

1212
const scannerVersion = (() => {
1313
// If there's no window
@@ -341,13 +341,58 @@ export default class AppFilesScanner {
341341
.then( response => response.data )
342342
.catch(function (error) {
343343
console.error(error)
344+
345+
return {
346+
supportedVersionNumber: null
347+
}
344348
})
345349

346350
return {
347351
supportedVersionNumber
348352
}
349353
}
350354

355+
getFinishedStatusMessage ({
356+
binarySupportsNative,
357+
supportedVersionNumber
358+
}) {
359+
if ( binarySupportsNative ) {
360+
return '✅ This app is natively compatible with Apple Silicon!'
361+
}
362+
363+
if ( supportedVersionNumber != null ) {
364+
return [
365+
'✅ A native version of this has been reported',
366+
( isString( supportedVersionNumber ) && supportedVersionNumber.length > 0 ) ? `as of v${ supportedVersionNumber }` : null
367+
].filter( Boolean ).join(' ')
368+
}
369+
370+
return `🔶 This app file is not natively compatible with Apple Silicon and may only run via Rosetta 2 translation, however, software vendors will sometimes will ship separate install files for Intel and ARM instead of a single one. `
371+
}
372+
373+
finishFileScan ( file, scanIndex, {
374+
binarySupportsNative,
375+
supportedVersionNumber
376+
} ) {
377+
file.statusMessage = this.getFinishedStatusMessage({
378+
binarySupportsNative,
379+
supportedVersionNumber
380+
})
381+
file.status = 'finished'
382+
383+
if ( binarySupportsNative ) {
384+
this.files.unshift( this.files.splice( scanIndex, 1 )[0] )
385+
}
386+
}
387+
388+
applyWorkerScanData ( file, scan ) {
389+
file.appVersion = scan.appVersion || null
390+
file.displayName = scan.displayName || file.displayName
391+
file.details = Array.isArray( scan.details ) ? scan.details : []
392+
file.displayBinarySize = scan.displayBinarySize || null
393+
file.binarySize = typeof scan.binarySize === 'number' ? scan.binarySize : null
394+
}
395+
351396
async scanFile ( file, scanIndex ) {
352397

353398
// If we've already scanned this
@@ -553,26 +598,10 @@ export default class AppFilesScanner {
553598

554599
console.log('supportedVersionNumber', supportedVersionNumber)
555600

556-
let finishedStatusMessage = ''
557-
558-
if ( binarySupportsNative ) {
559-
finishedStatusMessage = '✅ This app is natively compatible with Apple Silicon!'
560-
561-
// Shift this scan to the top
562-
this.files.unshift( this.files.splice( scanIndex, 1 )[0] )
563-
} else if ( supportedVersionNumber !== null ) {
564-
565-
finishedStatusMessage = [
566-
'✅ A native version of this has been reported',
567-
(supportedVersionNumber.length > 0) ? `as of v${supportedVersionNumber}` : null
568-
].join(' ')
569-
570-
} else {
571-
finishedStatusMessage = `🔶 This app file is not natively compatible with Apple Silicon and may only run via Rosetta 2 translation, however, software vendors will sometimes will ship separate install files for Intel and ARM instead of a single one. `
572-
}
573-
574-
file.statusMessage = finishedStatusMessage
575-
file.status = 'finished'
601+
this.finishFileScan( file, scanIndex, {
602+
binarySupportsNative,
603+
supportedVersionNumber
604+
} )
576605

577606
return
578607
}
@@ -618,20 +647,45 @@ export default class AppFilesScanner {
618647
console.log( 'scannerVersion', scannerVersion )
619648

620649
if ( scannerVersion === '2' ) {
650+
try {
651+
const { scan } = await runScanWorker( file.instance, messageDetails => {
652+
console.log( 'messageDetails', messageDetails )
621653

654+
if ( isString( messageDetails.message ) ) {
655+
file.statusMessage = messageDetails.message
656+
}
622657

623-
const { scan } = await runScanWorker( file.instance, messageDetails => {
624-
console.log( 'messageDetails', messageDetails )
658+
if ( isString( messageDetails.status ) ) {
659+
file.status = messageDetails.status
660+
}
661+
} )
625662

626-
file.statusMessage = messageDetails.message
627-
file.status = messageDetails.status
628-
} )
663+
this.applyWorkerScanData( file, scan )
629664

630-
console.log('scan', scan)
665+
const { supportedVersionNumber } = await this.submitScanInfo({
666+
filename: scan.info?.filename || file.name,
667+
appVersion: scan.info?.appVersion || file.appVersion,
668+
result: scan.info?.result || ( scan.binarySupportsNative ? '✅' : '🔶' ),
669+
machoMeta: scan.info?.machoMeta || null,
670+
infoPlist: scan.info?.infoPlist || null
671+
})
631672

632-
clearTimeout(timer)
673+
this.finishFileScan( file, scanIndex, {
674+
binarySupportsNative: Boolean( scan.binarySupportsNative ),
675+
supportedVersionNumber
676+
} )
633677

634-
resolve()
678+
clearTimeout(timer)
679+
680+
resolve()
681+
} catch ( error ) {
682+
file.statusMessage = `❔ ${ error.message }`
683+
file.status = 'finished'
684+
685+
clearTimeout(timer)
686+
687+
resolve()
688+
}
635689
return
636690
}
637691

helpers/scanner/client.mjs

Lines changed: 0 additions & 104 deletions
This file was deleted.

helpers/scanner/client.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import AppScanWorker from './worker?worker'
2+
3+
import type {
4+
AppScanSnapshot,
5+
ScanFileLike,
6+
ScanMessage
7+
} from './scan'
8+
9+
const noop = () => {}
10+
11+
type ScanMessageReceiver = ( details: ScanMessage ) => void
12+
13+
interface WorkerScanFile extends ScanFileLike {
14+
arrayBuffer: ArrayBuffer
15+
}
16+
17+
interface WorkerFinishedMessage extends ScanMessage {
18+
error?: {
19+
message?: string
20+
}
21+
scan?: AppScanSnapshot
22+
status: 'finished'
23+
}
24+
25+
function toArrayBuffer ( value: ArrayBuffer | ArrayBufferView ) {
26+
if ( value instanceof ArrayBuffer ) {
27+
return value
28+
}
29+
30+
return new Uint8Array(
31+
value.buffer,
32+
value.byteOffset,
33+
value.byteLength
34+
).slice().buffer
35+
}
36+
37+
function isWorkerFinishedMessage ( details: ScanMessage | WorkerFinishedMessage ): details is WorkerFinishedMessage {
38+
return details.status === 'finished'
39+
}
40+
41+
async function getArrayBufferFromFileData ( file: ScanFileLike ) {
42+
if ( typeof file.arrayBuffer === 'function' ) {
43+
return await file.arrayBuffer()
44+
}
45+
46+
if ( file.arrayBuffer instanceof ArrayBuffer ) {
47+
return file.arrayBuffer
48+
}
49+
50+
if ( file.buffer instanceof ArrayBuffer ) {
51+
return file.buffer
52+
}
53+
54+
if ( ArrayBuffer.isView( file.buffer ) ) {
55+
return toArrayBuffer( file.buffer )
56+
}
57+
58+
throw new Error( 'No fileArrayBuffer' )
59+
}
60+
61+
function makeWorkerFile ( file: ScanFileLike, arrayBuffer: ArrayBuffer ): WorkerScanFile {
62+
return {
63+
arrayBuffer,
64+
name: file.name,
65+
size: file.size ?? arrayBuffer.byteLength,
66+
type: file.type ?? file.mimeType ?? ''
67+
}
68+
}
69+
70+
export async function runScanWorker (
71+
file: ScanFileLike,
72+
messageReceiver: ScanMessageReceiver = noop
73+
) {
74+
const AppScanWorkerConstructor = AppScanWorker as unknown as { new (): Worker }
75+
const appScanWorker = new AppScanWorkerConstructor()
76+
const fileArrayBuffer = await getArrayBufferFromFileData( file )
77+
const workerFile = makeWorkerFile( file, fileArrayBuffer )
78+
79+
const scan = await new Promise<AppScanSnapshot>( ( resolve, reject ) => {
80+
const cleanup = () => {
81+
appScanWorker.onmessage = null
82+
appScanWorker.onerror = null
83+
appScanWorker.terminate()
84+
}
85+
86+
appScanWorker.onmessage = ( event: MessageEvent<ScanMessage | WorkerFinishedMessage> ) => {
87+
const details = event.data
88+
89+
messageReceiver( details )
90+
91+
if ( !isWorkerFinishedMessage( details ) ) {
92+
return
93+
}
94+
95+
cleanup()
96+
97+
if ( details.scan ) {
98+
resolve( details.scan )
99+
return
100+
}
101+
102+
reject( new Error( details.error?.message || details.message || 'Worker finished without a scan result.' ) )
103+
}
104+
105+
appScanWorker.onerror = ( errorEvent: ErrorEvent ) => {
106+
cleanup()
107+
reject( new Error( errorEvent.message || 'Error received from App Scan Worker' ) )
108+
}
109+
110+
appScanWorker.postMessage( {
111+
status: 'start',
112+
options: {
113+
file: workerFile
114+
}
115+
}, [
116+
fileArrayBuffer
117+
] )
118+
} )
119+
120+
return {
121+
appScanWorker,
122+
scan
123+
}
124+
}

0 commit comments

Comments
 (0)