Skip to content

Commit 77e92f9

Browse files
committed
chore: wip
1 parent 9856e50 commit 77e92f9

File tree

3 files changed

+96
-43
lines changed

3 files changed

+96
-43
lines changed

packages/launchpad/src/services/definitions.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const SERVICE_DEFINITIONS: Record<string, ServiceDefinition> = {
7676
description: 'MySQL database server',
7777
packageDomain: 'mysql.com',
7878
executable: 'mysqld',
79-
args: ['--datadir={dataDir}', '--pid-file={pidFile}', '--port=3306', '--bind-address=127.0.0.1', '--skip-grant-tables', '--default-authentication-plugin=mysql_native_password'],
79+
args: ['--datadir={dataDir}', '--pid-file={pidFile}', '--port=3306', '--bind-address=127.0.0.1'],
8080
env: {},
8181
dataDirectory: path.join(homedir(), '.local', 'share', 'launchpad', 'services', 'mysql', 'data'),
8282
logFile: path.join(homedir(), '.local', 'share', 'launchpad', 'logs', 'mysql.log'),
@@ -93,10 +93,10 @@ export const SERVICE_DEFINITIONS: Record<string, ServiceDefinition> = {
9393
initCommand: ['mysqld', '--initialize-insecure', '--datadir={dataDir}', '--user={currentUser}'],
9494
postStartCommands: [
9595
// Create application database and user for any project type
96-
['mysql', '-u', 'root', '-e', 'CREATE DATABASE IF NOT EXISTS {projectDatabase};'],
97-
['mysql', '-u', 'root', '-e', 'CREATE USER IF NOT EXISTS \'{dbUsername}\'@\'localhost\' IDENTIFIED BY \'{dbPassword}\';'],
98-
['mysql', '-u', 'root', '-e', 'GRANT ALL PRIVILEGES ON {projectDatabase}.* TO \'{dbUsername}\'@\'localhost\';'],
99-
['mysql', '-u', 'root', '-e', 'FLUSH PRIVILEGES;'],
96+
['mysql', '-h', '127.0.0.1', '-P', '3306', '-u', 'root', '-e', 'CREATE DATABASE IF NOT EXISTS {projectDatabase};'],
97+
['mysql', '-h', '127.0.0.1', '-P', '3306', '-u', 'root', '-e', 'CREATE USER IF NOT EXISTS \'{dbUsername}\'@\'localhost\' IDENTIFIED BY \'{dbPassword}\';'],
98+
['mysql', '-h', '127.0.0.1', '-P', '3306', '-u', 'root', '-e', 'GRANT ALL PRIVILEGES ON {projectDatabase}.* TO \'{dbUsername}\'@\'localhost\';'],
99+
['mysql', '-h', '127.0.0.1', '-P', '3306', '-u', 'root', '-e', 'FLUSH PRIVILEGES;'],
100100
],
101101
supportsGracefulShutdown: true,
102102
config: {

packages/launchpad/src/services/manager.ts

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -322,24 +322,23 @@ export async function stopService(serviceName: string): Promise<boolean> {
322322
}
323323

324324
try {
325-
const service = manager.services.get(serviceName)
326-
327-
if (!service) {
328-
console.warn(`⚠️ Service ${serviceName} is not registered`)
329-
operation.result = 'success'
330-
operation.duration = 0
331-
manager.operations.push(operation)
332-
return true
333-
}
325+
// Get or create service instance (ensures service is registered)
326+
const service = await getOrCreateServiceInstance(serviceName)
334327

335-
if (service.status === 'stopped') {
328+
// Check actual service health to determine if it's really running
329+
const isActuallyRunning = await checkServiceHealth(service)
330+
if (!isActuallyRunning) {
331+
service.status = 'stopped'
336332
console.log(`✅ Service ${serviceName} is already stopped`)
337333
operation.result = 'success'
338334
operation.duration = 0
339335
manager.operations.push(operation)
340336
return true
341337
}
342338

339+
// Update status based on health check
340+
service.status = 'running'
341+
343342
console.warn(`🛑 Stopping ${service.definition?.displayName || serviceName}...`)
344343

345344
// Update status to stopping
@@ -575,22 +574,20 @@ export async function disableService(serviceName: string): Promise<boolean> {
575574
* Get the status of a service
576575
*/
577576
export async function getServiceStatus(serviceName: string): Promise<ServiceStatus> {
578-
const manager = await getServiceManager()
579-
const service = manager.services.get(serviceName)
580-
581-
if (!service) {
582-
return 'stopped'
583-
}
577+
try {
578+
// Get or create service instance to ensure it's registered
579+
const service = await getOrCreateServiceInstance(serviceName)
584580

585-
// Check if the service is actually running
586-
if (service.status === 'running') {
581+
// Always check actual health to get real-time status
587582
const isActuallyRunning = await checkServiceHealth(service)
588-
if (!isActuallyRunning) {
589-
service.status = 'stopped'
590-
}
591-
}
583+
service.status = isActuallyRunning ? 'running' : 'stopped'
592584

593-
return service.status
585+
return service.status
586+
}
587+
catch {
588+
// If service definition not found, return stopped
589+
return 'stopped'
590+
}
594591
}
595592

596593
/**
@@ -1150,7 +1147,7 @@ async function autoInitializeDatabase(service: ServiceInstance): Promise<boolean
11501147

11511148
// MySQL auto-initialization
11521149
if (definition.name === 'mysql' || definition.name === 'mariadb') {
1153-
const dataDir = service.dataDir || definition.dataDirectory || path.join(homedir(), '.local', 'share', 'launchpad', 'mysql-data')
1150+
const dataDir = service.dataDir || definition.dataDirectory || path.join(homedir(), '.local', 'share', 'launchpad', 'services', 'mysql', 'data')
11541151
const mysqlDir = path.join(dataDir, 'mysql')
11551152

11561153
// Check if already initialized
@@ -1264,7 +1261,9 @@ async function autoInitializeDatabase(service: ServiceInstance): Promise<boolean
12641261
console.log(`DYLD_LIBRARY_PATH: ${env.DYLD_LIBRARY_PATH}`)
12651262
}
12661263

1267-
execSync(`mysqld --initialize-insecure --datadir="${dataDir}" --basedir="${basedir}" --user=$(whoami)`, {
1264+
// Use the real mysqld binary path instead of shim
1265+
const mysqldBin = path.join(basedir, 'bin', 'mysqld')
1266+
execSync(`"${mysqldBin}" --initialize-insecure --datadir="${dataDir}" --basedir="${basedir}" --user=$(whoami)`, {
12681267
stdio: config.verbose ? 'inherit' : 'pipe',
12691268
timeout: 120000,
12701269
env,

packages/launchpad/src/services/platform.ts

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
4545
})
4646

4747
// Find the executable path
48-
const executablePath = findBinaryInPath(definition.executable) || definition.executable
48+
let executablePath = findBinaryInPath(definition.executable) || definition.executable
4949

5050
// Compute runtime PATH and dynamic library search paths for packages started by launchd
5151
// launchd does not inherit shell env, so we must include env-specific paths here.
@@ -59,6 +59,26 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
5959
? Boolean(config.services.shouldAutoStart)
6060
: Boolean(service.enabled))
6161

62+
// For MySQL/MariaDB: resolve real binary path from shim to get proper library paths
63+
if ((definition.name === 'mysql' || definition.name === 'mariadb') && executablePath && fs.existsSync(executablePath)) {
64+
try {
65+
const content = fs.readFileSync(executablePath, 'utf-8')
66+
if (content.startsWith('#!/bin/sh') || content.startsWith('#!/usr/bin/env bash')) {
67+
// Extract real path from shim's exec line
68+
const execMatch = content.match(/exec\s+"([^"]+)"/)
69+
if (execMatch && execMatch[1]) {
70+
executablePath = execMatch[1]
71+
}
72+
}
73+
}
74+
catch {
75+
// Continue with original path if we can't read it
76+
}
77+
}
78+
79+
// Track library directories for wrapper script generation
80+
let libraryDirs: string[] = []
81+
6282
try {
6383
const binDir = path.dirname(executablePath)
6484
const versionDir = path.dirname(binDir)
@@ -76,7 +96,6 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
7696
envVars.PATH = [envBin, envSbin, basePath].filter(Boolean).join(':')
7797

7898
// Discover all package lib directories under this env root
79-
const libraryDirs: string[] = []
8099

81100
const pushIfExists = (p: string) => {
82101
try {
@@ -91,19 +110,36 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
91110
pushIfExists(path.join(envRoot, 'lib64'))
92111

93112
// Scan domain/version directories for lib folders
113+
// Recursively search up to 2 levels deep for version directories
114+
const scanForLibraries = (dir: string, depth: number = 0) => {
115+
if (depth > 2) return
116+
try {
117+
const entries = fs.readdirSync(dir, { withFileTypes: true })
118+
for (const entry of entries) {
119+
if (!entry.isDirectory()) continue
120+
121+
const entryPath = path.join(dir, entry.name)
122+
123+
// If this is a version directory (starts with 'v'), check for lib
124+
if (entry.name.startsWith('v')) {
125+
pushIfExists(path.join(entryPath, 'lib'))
126+
pushIfExists(path.join(entryPath, 'lib64'))
127+
}
128+
// Otherwise recurse into subdirectories (unless it's a common system dir)
129+
else if (!['bin', 'sbin', 'share', 'include', 'etc', 'pkgs', '.tmp'].includes(entry.name)) {
130+
scanForLibraries(entryPath, depth + 1)
131+
}
132+
}
133+
}
134+
catch {}
135+
}
136+
94137
try {
95138
const entries = fs.readdirSync(envRoot, { withFileTypes: true })
96139
for (const entry of entries) {
97-
if (!entry.isDirectory())
98-
continue
99-
if (['bin', 'sbin', 'share', 'include', 'etc', 'pkgs'].includes(entry.name))
100-
continue
101-
const domainPath = path.join(envRoot, entry.name)
102-
const versions = fs.readdirSync(domainPath, { withFileTypes: true }).filter(v => v.isDirectory() && v.name.startsWith('v'))
103-
for (const ver of versions) {
104-
pushIfExists(path.join(domainPath, ver.name, 'lib'))
105-
pushIfExists(path.join(domainPath, ver.name, 'lib64'))
106-
}
140+
if (!entry.isDirectory()) continue
141+
if (['bin', 'sbin', 'share', 'include', 'etc', 'pkgs'].includes(entry.name)) continue
142+
scanForLibraries(path.join(envRoot, entry.name), 0)
107143
}
108144
}
109145
catch {}
@@ -121,9 +157,27 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
121157
// Best-effort only
122158
}
123159

160+
// For macOS: launchd strips DYLD_* variables, so we need a wrapper script for databases
161+
// that explicitly sets DYLD_LIBRARY_PATH before executing the binary
162+
let finalExecutablePath = executablePath
163+
if ((definition.name === 'mysql' || definition.name === 'mariadb' || definition.name === 'postgres') && platform() === 'darwin') {
164+
if (libraryDirs && libraryDirs.length > 0) {
165+
// Generate wrapper script
166+
const wrapperDir = path.join(homedir(), '.local', 'share', 'launchpad', 'wrappers')
167+
fs.mkdirSync(wrapperDir, { recursive: true })
168+
const wrapperPath = path.join(wrapperDir, `${definition.name}-wrapper.sh`)
169+
170+
const wrapperContent = `#!/bin/bash
171+
exec env DYLD_LIBRARY_PATH="${libraryDirs.join(':')}" "${executablePath}" "$@"
172+
`
173+
fs.writeFileSync(wrapperPath, wrapperContent, { mode: 0o755 })
174+
finalExecutablePath = wrapperPath
175+
}
176+
}
177+
124178
return {
125179
Label: `com.launchpad.${definition.name || service.name}`,
126-
ProgramArguments: [executablePath, ...resolvedArgs],
180+
ProgramArguments: [finalExecutablePath, ...resolvedArgs],
127181
WorkingDirectory: definition.workingDirectory || dataDir || '',
128182
EnvironmentVariables: {
129183
...Object.fromEntries(Object.entries(definition.env || {}).map(([k, v]) => {

0 commit comments

Comments
 (0)