Add native service supervisor and runtime packaging
This commit is contained in:
+92
-17
@@ -1,5 +1,7 @@
|
||||
import { app } from 'electron'
|
||||
import { execFile } from 'child_process'
|
||||
import { createHash } from 'crypto'
|
||||
import { createReadStream } from 'fs'
|
||||
import { access, readFile } from 'fs/promises'
|
||||
import { isAbsolute, join, resolve, sep } from 'path'
|
||||
import { promisify } from 'util'
|
||||
@@ -8,47 +10,118 @@ import type { AuroraNativeRuntimeManifest, AuroraRuntimeStatus } from '../shared
|
||||
const execFileAsync = promisify(execFile)
|
||||
const supportedPlatforms = new Set(['linux', 'darwin', 'win32'])
|
||||
const supportedArchitectures = new Set(['x64', 'arm64'])
|
||||
const componentIds = new Set(['php', 'nginx', 'apache', 'mariadb', 'mysql', 'postgres', 'node', 'composer', 'wp-cli', 'drush'])
|
||||
const componentIds = new Set([
|
||||
'php',
|
||||
'nginx',
|
||||
'apache',
|
||||
'mariadb',
|
||||
'mysql',
|
||||
'postgres',
|
||||
'node',
|
||||
'composer',
|
||||
'wp-cli',
|
||||
'drush'
|
||||
])
|
||||
|
||||
export function validateNativeRuntimeManifest(value: unknown): AuroraNativeRuntimeManifest {
|
||||
if (!value || typeof value !== 'object') throw new Error('Runtime manifest must be an object.')
|
||||
const manifest = value as Record<string, unknown>
|
||||
if (manifest.schema !== 1) throw new Error('Unsupported native runtime manifest schema.')
|
||||
if (typeof manifest.runtimeVersion !== 'string' || !/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.test(manifest.runtimeVersion)) throw new Error('Invalid native runtime version.')
|
||||
if (typeof manifest.platform !== 'string' || !supportedPlatforms.has(manifest.platform)) throw new Error('Unsupported native runtime platform.')
|
||||
if (typeof manifest.arch !== 'string' || !supportedArchitectures.has(manifest.arch)) throw new Error('Unsupported native runtime architecture.')
|
||||
if (!Array.isArray(manifest.components) || manifest.components.length === 0) throw new Error('Native runtime manifest has no components.')
|
||||
if (
|
||||
typeof manifest.runtimeVersion !== 'string' ||
|
||||
!/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.test(manifest.runtimeVersion)
|
||||
)
|
||||
throw new Error('Invalid native runtime version.')
|
||||
if (typeof manifest.platform !== 'string' || !supportedPlatforms.has(manifest.platform))
|
||||
throw new Error('Unsupported native runtime platform.')
|
||||
if (typeof manifest.arch !== 'string' || !supportedArchitectures.has(manifest.arch))
|
||||
throw new Error('Unsupported native runtime architecture.')
|
||||
if (!Array.isArray(manifest.components) || manifest.components.length === 0)
|
||||
throw new Error('Native runtime manifest has no components.')
|
||||
const components = manifest.components.map((item) => {
|
||||
if (!item || typeof item !== 'object') throw new Error('Invalid native runtime component.')
|
||||
const component = item as Record<string, unknown>
|
||||
if (typeof component.id !== 'string' || !componentIds.has(component.id)) throw new Error('Unknown native runtime component.')
|
||||
if (typeof component.version !== 'string' || !component.version.trim()) throw new Error(`Missing version for ${component.id}.`)
|
||||
if (typeof component.executable !== 'string' || !component.executable || isAbsolute(component.executable) || component.executable.split(/[\\/]/).includes('..')) throw new Error(`Unsafe executable path for ${component.id}.`)
|
||||
if (typeof component.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(component.sha256)) throw new Error(`Invalid checksum for ${component.id}.`)
|
||||
if (typeof component.id !== 'string' || !componentIds.has(component.id))
|
||||
throw new Error('Unknown native runtime component.')
|
||||
if (typeof component.version !== 'string' || !component.version.trim())
|
||||
throw new Error(`Missing version for ${component.id}.`)
|
||||
if (
|
||||
typeof component.executable !== 'string' ||
|
||||
!component.executable ||
|
||||
isAbsolute(component.executable) ||
|
||||
component.executable.split(/[\\/]/).includes('..')
|
||||
)
|
||||
throw new Error(`Unsafe executable path for ${component.id}.`)
|
||||
if (typeof component.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(component.sha256))
|
||||
throw new Error(`Invalid checksum for ${component.id}.`)
|
||||
return component as unknown as AuroraNativeRuntimeManifest['components'][number]
|
||||
})
|
||||
return { schema: 1, runtimeVersion: manifest.runtimeVersion, platform: manifest.platform as AuroraNativeRuntimeManifest['platform'], arch: manifest.arch as AuroraNativeRuntimeManifest['arch'], components }
|
||||
return {
|
||||
schema: 1,
|
||||
runtimeVersion: manifest.runtimeVersion,
|
||||
platform: manifest.platform as AuroraNativeRuntimeManifest['platform'],
|
||||
arch: manifest.arch as AuroraNativeRuntimeManifest['arch'],
|
||||
components
|
||||
}
|
||||
}
|
||||
|
||||
function runtimeRoot(): string {
|
||||
return join(app.getPath('userData'), 'runtimes', `${process.platform}-${process.arch}`)
|
||||
}
|
||||
|
||||
async function sha256(path: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = createHash('sha256')
|
||||
const stream = createReadStream(path)
|
||||
stream.once('error', reject)
|
||||
stream.on('data', (chunk) => hash.update(chunk))
|
||||
stream.once('end', () => resolve(hash.digest('hex')))
|
||||
})
|
||||
}
|
||||
|
||||
async function nativeStatus(): Promise<AuroraRuntimeStatus['native']> {
|
||||
if (!supportedPlatforms.has(process.platform) || !supportedArchitectures.has(process.arch)) return { available: false, platform: process.platform, arch: process.arch, components: [], reason: 'This platform does not have an Aurora Native runtime target yet.' }
|
||||
if (!supportedPlatforms.has(process.platform) || !supportedArchitectures.has(process.arch))
|
||||
return {
|
||||
available: false,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
components: [],
|
||||
reason: 'This platform does not have an Aurora Native runtime target yet.'
|
||||
}
|
||||
const root = runtimeRoot()
|
||||
try {
|
||||
const manifest = validateNativeRuntimeManifest(JSON.parse(await readFile(join(root, 'runtime.json'), 'utf8')))
|
||||
if (manifest.platform !== process.platform || manifest.arch !== process.arch) throw new Error('The installed runtime targets a different platform.')
|
||||
const manifest = validateNativeRuntimeManifest(
|
||||
JSON.parse(await readFile(join(root, 'runtime.json'), 'utf8'))
|
||||
)
|
||||
if (manifest.platform !== process.platform || manifest.arch !== process.arch)
|
||||
throw new Error('The installed runtime targets a different platform.')
|
||||
const canonicalRoot = resolve(root)
|
||||
for (const component of manifest.components) {
|
||||
const executable = resolve(root, component.executable)
|
||||
if (executable !== canonicalRoot && !executable.startsWith(`${canonicalRoot}${sep}`)) throw new Error(`Unsafe executable path for ${component.id}.`)
|
||||
if (executable !== canonicalRoot && !executable.startsWith(`${canonicalRoot}${sep}`))
|
||||
throw new Error(`Unsafe executable path for ${component.id}.`)
|
||||
await access(executable)
|
||||
if ((await sha256(executable)) !== component.sha256.toLowerCase())
|
||||
throw new Error(`Checksum verification failed for ${component.id}.`)
|
||||
}
|
||||
return {
|
||||
available: true,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
runtimeVersion: manifest.runtimeVersion,
|
||||
components: manifest.components
|
||||
}
|
||||
return { available: true, platform: process.platform, arch: process.arch, runtimeVersion: manifest.runtimeVersion, components: manifest.components }
|
||||
} catch (error) {
|
||||
return { available: false, platform: process.platform, arch: process.arch, components: [], reason: error instanceof Error && !error.message.includes('ENOENT') ? error.message : 'Aurora Native runtime is not installed yet.' }
|
||||
return {
|
||||
available: false,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
components: [],
|
||||
reason:
|
||||
error instanceof Error && !error.message.includes('ENOENT')
|
||||
? error.message
|
||||
: 'Aurora Native runtime is not installed yet.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +130,9 @@ async function containerStatus(): Promise<AuroraRuntimeStatus['container']> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(provider, ['--version'], { timeout: 5000 })
|
||||
return { available: true, provider, version: stdout.trim() }
|
||||
} catch { /* try next provider */ }
|
||||
} catch {
|
||||
/* try next provider */
|
||||
}
|
||||
}
|
||||
return { available: false, provider: null, reason: 'Docker or Podman was not detected.' }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user