Skip to content

Commit 7303b40

Browse files
committed
chore: wip
1 parent 24fe7e6 commit 7303b40

33 files changed

+2382
-3478
lines changed

packages/launchpad/src/install-helpers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ export async function createShims(packageDir: string, installPath: string, domai
113113
for (const libDir of depLibDirs) {
114114
if (fs.existsSync(libDir) && !libraryPaths.includes(libDir)) {
115115
libraryPaths.push(libDir)
116+
117+
// Special handling for ncurses - check for specific library files
118+
if (entry.name.includes('ncurses') || entry.name.includes('invisible-island.net')) {
119+
const ncursesLibs = ['libncurses.dylib', 'libncursesw.dylib', 'libncurses.6.dylib', 'libncursesw.6.dylib']
120+
for (const lib of ncursesLibs) {
121+
const libPath = path.join(libDir, lib)
122+
if (fs.existsSync(libPath)) {
123+
console.log(`🔗 Found ncurses library: ${libPath}`)
124+
}
125+
}
126+
}
116127
}
117128
}
118129
}

packages/launchpad/src/install-main.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@ export async function install(packages: PackageSpec | PackageSpec[], basePath?:
2121
// Clear message cache at start of installation to avoid stale duplicates
2222
clearMessageCache()
2323

24+
// Add global timeout to prevent infinite hangs
25+
const globalTimeout = setTimeout(() => {
26+
console.error('❌ Installation process timed out after 10 minutes, forcing exit')
27+
process.exit(1)
28+
}, 10 * 60 * 1000) // 10 minute global timeout
29+
30+
try {
31+
const result = await installInternal(packageList, installPath)
32+
clearTimeout(globalTimeout)
33+
return result
34+
} catch (error) {
35+
clearTimeout(globalTimeout)
36+
throw error
37+
}
38+
}
39+
40+
/**
41+
* Internal installation function
42+
*/
43+
async function installInternal(packageList: PackageSpec[], installPath: string): Promise<string[]> {
44+
2445
// Create installation directory even if no packages to install
2546
await fs.promises.mkdir(installPath, { recursive: true })
2647

@@ -141,8 +162,17 @@ export async function install(packages: PackageSpec | PackageSpec[], basePath?:
141162
try {
142163
const parsed = parsePackageSpec(pkg)
143164
packageName = parsed.name
144-
// Direct installation without dependency resolution
145-
const packageFiles = await installPackage(packageName, pkg, installPath)
165+
166+
// Add timeout to individual package installation to prevent hangs
167+
const installPromise = installPackage(packageName, pkg, installPath)
168+
const timeoutPromise = new Promise<string[]>((_, reject) => {
169+
setTimeout(() => {
170+
console.warn(`⚠️ Package installation timeout after 5 minutes: ${pkg}`)
171+
reject(new Error(`Package installation timeout: ${pkg}`))
172+
}, 5 * 60 * 1000) // 5 minute timeout
173+
})
174+
175+
const packageFiles = await Promise.race([installPromise, timeoutPromise])
146176
allInstalledFiles.push(...packageFiles)
147177
}
148178
catch (error) {

packages/launchpad/src/services/manager.ts

Lines changed: 58 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ export async function startService(serviceName: string): Promise<boolean> {
8282
}
8383

8484
// In test mode, still validate service exists but mock the actual operation
85-
if (process.env.NODE_ENV === 'test' || process.env.LAUNCHPAD_TEST_MODE === 'true') {
85+
// Skip test mode for E2E validation tests
86+
if ((process.env.NODE_ENV === 'test' || process.env.LAUNCHPAD_TEST_MODE === 'true') && !process.env.LAUNCHPAD_E2E_TEST) {
8687
try {
8788
const service = await getOrCreateServiceInstance(serviceName)
8889
console.warn(`🧪 Test mode: Mocking start of service ${serviceName}`)
@@ -271,16 +272,18 @@ export async function startService(serviceName: string): Promise<boolean> {
271272
void checkServiceHealth(service)
272273
}, 2000)
273274

274-
// Mark operation success
275+
// Mark operation success and update service status
276+
service.status = 'running'
277+
service.lastCheckedAt = new Date()
275278
operation.result = 'success'
276279
operation.duration = 0
277280
manager.operations.push(operation)
278281
return true
279282
}
280283
catch (error) {
281-
console.error(`❌ Failed to start service ${serviceName}: ${error instanceof Error ? error.message : String(error)}`)
284+
console.error(`❌ Failed to start service ${serviceName}: ${error}`)
282285
operation.result = 'failure'
283-
operation.error = error instanceof Error ? error.message : String(error)
286+
operation.error = error
284287
operation.duration = 0
285288
manager.operations.push(operation)
286289
return false
@@ -304,7 +307,8 @@ export async function stopService(serviceName: string): Promise<boolean> {
304307
}
305308

306309
// In test mode, still validate and track operations
307-
if (process.env.NODE_ENV === 'test' || process.env.LAUNCHPAD_TEST_MODE === 'true') {
310+
// Skip test mode for E2E validation tests
311+
if ((process.env.NODE_ENV === 'test' || process.env.LAUNCHPAD_TEST_MODE === 'true') && !process.env.LAUNCHPAD_E2E_TEST) {
308312
const service = manager.services.get(serviceName)
309313

310314
if (!service) {
@@ -379,19 +383,19 @@ export async function stopService(serviceName: string): Promise<boolean> {
379383
manager.operations.push(operation)
380384

381385
console.error(`❌ Failed to stop ${serviceName}: ${operation.error}`)
382-
return false
386+
return { success: false, error: 'Service stop failed' }
383387
}
384388
}
385389

386390
/**
387391
* Restart a service
388392
*/
389-
export async function restartService(serviceName: string): Promise<boolean> {
393+
export async function restartService(serviceName: string): Promise<{ success: boolean, error?: string }> {
390394
console.warn(`🔄 Restarting ${serviceName}...`)
391395

392396
const stopSuccess = await stopService(serviceName)
393397
if (!stopSuccess) {
394-
return false
398+
return { success: false, error: 'Failed to stop service for restart' }
395399
}
396400

397401
// Wait a moment before starting
@@ -436,7 +440,7 @@ export async function enableService(serviceName: string): Promise<boolean> {
436440
operation.error = error instanceof Error ? error.message : String(error)
437441
operation.duration = 0
438442
manager.operations.push(operation)
439-
return false
443+
return { success: false, error: 'Service stop failed' }
440444
}
441445
}
442446

@@ -480,7 +484,7 @@ export async function enableService(serviceName: string): Promise<boolean> {
480484
manager.operations.push(operation)
481485

482486
console.error(`❌ Failed to enable ${serviceName}: ${operation.error}`)
483-
return false
487+
return { success: false, error: 'Service stop failed' }
484488
}
485489
}
486490

@@ -501,7 +505,8 @@ export async function disableService(serviceName: string): Promise<boolean> {
501505
}
502506

503507
// In test mode, still validate and track operations
504-
if (process.env.NODE_ENV === 'test' || process.env.LAUNCHPAD_TEST_MODE === 'true') {
508+
// Skip test mode for E2E validation tests
509+
if ((process.env.NODE_ENV === 'test' || process.env.LAUNCHPAD_TEST_MODE === 'true') && !process.env.LAUNCHPAD_E2E_TEST) {
505510
const service = manager.services.get(serviceName)
506511

507512
if (!service) {
@@ -569,7 +574,7 @@ export async function disableService(serviceName: string): Promise<boolean> {
569574
manager.operations.push(operation)
570575

571576
console.error(`❌ Failed to disable ${serviceName}: ${operation.error}`)
572-
return false
577+
return { success: false, error: 'Service stop failed' }
573578
}
574579
}
575580

@@ -650,7 +655,7 @@ async function isServiceInitialized(service: ServiceInstance): Promise<boolean>
650655
if (definition?.dataDirectory) {
651656
const dataDir = service.dataDir || definition.dataDirectory
652657
if (!fs.existsSync(dataDir)) {
653-
return false
658+
return { success: false, error: 'Service stop failed' }
654659
}
655660

656661
// For databases, check if data directory has initialization files
@@ -867,55 +872,54 @@ async function ensureServicePackageInstalled(service: ServiceInstance): Promise<
867872
throw new Error(`Invalid package domain for ${definition.displayName}: ${definition.packageDomain}`)
868873
}
869874

870-
// Try multiple import strategies for the install function
875+
// Import the install function with proper error handling
871876
let install: any
872877
try {
873-
// First try the main install module
874-
const installModule = await import('../install-main')
875-
install = installModule.install
878+
const { install: installFn } = await import('../install-main')
879+
install = installFn
876880
if (typeof install !== 'function') {
877-
throw new Error('install function not found in install-main')
881+
throw new Error('install function not found or not a function')
878882
}
879883
} catch (importError) {
880-
try {
881-
// Fallback to the install index
882-
const installModule = await import('../install')
883-
install = installModule.install
884-
if (typeof install !== 'function') {
885-
throw new Error('install function not found in install index')
886-
}
887-
} catch (fallbackError) {
888-
console.error(`❌ Failed to import install function from both modules:`)
889-
console.error(` - install-main: ${importError instanceof Error ? importError.message : String(importError)}`)
890-
console.error(` - install: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`)
891-
return false
892-
}
884+
console.error(`❌ Failed to import install function: ${importError instanceof Error ? importError.message : String(importError)}`)
885+
return { success: false, error: 'Service stop failed' }
893886
}
894887

895888
// Install the main service package - this will automatically install all dependencies
896-
// thanks to our fixed dependency resolution
897889
const installPath = `${process.env.HOME}/.local`
898890

899-
// Call install with proper error handling
891+
// Call install with proper error handling and shorter timeout
900892
try {
901-
await install([definition.packageDomain], installPath)
893+
if (config.verbose) {
894+
console.warn(`📦 Installing ${definition.displayName} package (${definition.packageDomain})...`)
895+
}
896+
897+
// Add shorter timeout to prevent hanging - 5 minutes should be enough
898+
const installPromise = install([definition.packageDomain], installPath)
899+
const timeoutPromise = new Promise<boolean>((_, reject) => {
900+
setTimeout(() => reject(new Error(`Package installation timeout after 5 minutes`)), 5 * 60 * 1000)
901+
})
902+
903+
await Promise.race([installPromise, timeoutPromise])
904+
905+
if (config.verbose) {
906+
console.log(`✅ ${definition.displayName} package installed successfully`)
907+
}
902908
} catch (installError) {
903-
// If the install fails, provide detailed error information
904-
console.error(`❌ Package installation failed for ${definition.displayName}:`)
905-
console.error(` - Package domain: ${definition.packageDomain}`)
906-
console.error(` - Install path: ${installPath}`)
907-
console.error(` - Error: ${installError instanceof Error ? installError.message : String(installError)}`)
908-
909-
if (installError instanceof Error && installError.stack) {
910-
console.error(` - Stack trace: ${installError.stack}`)
909+
// If installation fails or times out, try to continue without the package
910+
console.warn(`⚠️ Package installation failed for ${definition.displayName}, continuing without it`)
911+
console.warn(` - Error: ${installError instanceof Error ? installError.message : String(installError)}`)
912+
913+
// Check if binary is already available in system PATH as fallback
914+
const { findBinaryInPath } = await import('../utils')
915+
if (findBinaryInPath(definition.executable)) {
916+
console.warn(`✅ Found ${definition.executable} in system PATH, using system version`)
917+
return true
911918
}
912-
913-
return false
919+
920+
return { success: false, error: 'Service stop failed' }
914921
}
915922

916-
if (config.verbose)
917-
console.log(`✅ ${definition.displayName} package installed successfully`)
918-
919923
// Verify installation worked by checking in the Launchpad environment
920924
const binaryPath = findBinaryInEnvironment(definition.executable, installPath)
921925
if (!binaryPath) {
@@ -926,7 +930,7 @@ async function ensureServicePackageInstalled(service: ServiceInstance): Promise<
926930
}
927931
catch (error) {
928932
console.error(`❌ Failed to install ${definition.displayName}: ${error instanceof Error ? error.message : String(error)}`)
929-
return false
933+
return { success: false, error: 'Service stop failed' }
930934
}
931935
}
932936

@@ -969,7 +973,7 @@ async function ensurePHPDatabaseExtensions(_service: ServiceInstance): Promise<b
969973
})
970974

971975
if (!checkResult) {
972-
return false
976+
return { success: false, error: 'Service stop failed' }
973977
}
974978

975979
const loadedExtensions = output.toLowerCase().split('\n').map(line => line.trim())
@@ -987,11 +991,11 @@ async function ensurePHPDatabaseExtensions(_service: ServiceInstance): Promise<b
987991
if (config.verbose)
988992
console.warn(`💡 Launchpad ships precompiled PHP binaries with common DB extensions. We'll select the correct binary for your project automatically.`)
989993
// Do not attempt PECL here. Let binary-downloader pick the right PHP and shims load the project php.ini
990-
return false
994+
return { success: false, error: 'Service stop failed' }
991995
}
992996
catch (error) {
993997
console.error(`❌ Failed to check PHP extensions: ${error instanceof Error ? error.message : String(error)}`)
994-
return false
998+
return { success: false, error: 'Service stop failed' }
995999
}
9961000
}
9971001

@@ -1091,7 +1095,7 @@ export async function setupSQLiteForProject(): Promise<boolean> {
10911095
}
10921096
catch (error) {
10931097
console.warn(`⚠️ Could not set up SQLite automatically: ${error instanceof Error ? error.message : String(error)}`)
1094-
return false
1098+
return { success: false, error: 'Service stop failed' }
10951099
}
10961100
}
10971101

@@ -1211,7 +1215,7 @@ async function autoInitializeDatabase(service: ServiceInstance): Promise<boolean
12111215
}
12121216
catch (error) {
12131217
console.error(`❌ Failed to initialize PostgreSQL: ${error instanceof Error ? error.message : String(error)}`)
1214-
return false
1218+
return { success: false, error: 'Service stop failed' }
12151219
}
12161220
}
12171221

@@ -1243,7 +1247,7 @@ async function autoInitializeDatabase(service: ServiceInstance): Promise<boolean
12431247
}
12441248
catch (error) {
12451249
console.error(`❌ Failed to initialize MySQL: ${error instanceof Error ? error.message : String(error)}`)
1246-
return false
1250+
return { success: false, error: 'Service stop failed' }
12471251
}
12481252
}
12491253

0 commit comments

Comments
 (0)