fix: bundle and start meilisearch properly on macOS

This commit is contained in:
Christian Benincasa
2025-09-17 13:07:55 -04:00
parent 071be3bd19
commit 32d2e2360d
7 changed files with 81 additions and 8 deletions

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>Tunarr.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View File

@@ -6,31 +6,64 @@
//
import AppKit
import OSLog
import SwiftUI
class AppDelegate: NSObject, NSApplicationDelegate {
let bundle = Bundle.main
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Tunarr Subprocess")
var task = Process()
func applicationDidFinishLaunching(_ notification: Notification) {
guard let executablePath = Bundle.main.url(forAuxiliaryExecutable: "tunarr-macos")
else {
/// TODO: Do a popup here.
NSLog("Error: Bundled executable 'tunarr-macos' not found.")
logger.error("Error: Bundled executable 'tunarr-macos' not found.")
return
}
guard let meilisearchPath = Bundle.main.url(forAuxiliaryExecutable: "meilisearch")
else {
logger.error("Error: Bundled executable 'meilisearch' not found")
return
}
task.executableURL = executablePath
var env = ProcessInfo.processInfo.environment
env["TUNARR_MEILISEARCH_PATH"] = meilisearchPath.absoluteURL.path()
task.currentDirectoryURL = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent(
"Library"
).appendingPathComponent("Preferences")
.appendingPathComponent("tunarr")
task.environment = env
logger.info("\(env, privacy: .public)")
let errorPipe = Pipe()
task.standardError = errorPipe
do {
errorPipe.fileHandleForReading.readabilityHandler = { pipe in
let data = pipe.availableData
if let line = String(data: data, encoding: .utf8) {
if line.count > 0 {
self.logger.info(
"Process stderr: \(line.trimmingCharacters(in: .whitespacesAndNewlines), privacy: .public)"
)
}
}
}
try task.run()
NSLog("Successfully started Tunarr subprocess.")
logger.info("Successfully started Tunarr subprocess.")
} catch {
NSLog("Could not launch Tunarr. Reason: \(error)")
logger.error("Could not launch Tunarr. Reason: \(error, privacy: .public)")
}
}
func applicationWillTerminate(_ aNotification: Notification) {
NSLog("Shutting down Tunarr.")
logger.info("Shutting down Tunarr.")
task.terminate()
task.waitUntilExit()
}

View File

@@ -19,5 +19,5 @@ cp -R "$REPO_ROOT/macos/Tunarr/build/Release/Tunarr.app" "$APP_NAME"
popd || exit
cp -a "$REPO_ROOT/server/bin/$BINARY_NAME" "$APP_NAME/Contents/MacOS/tunarr-macos"
chmod +x "$APP_NAME/Contents/MacOS/tunarr-macos"
cp -a "$REPO_ROOT/server/bin/meilisearch-macos-$2" "$APP_NAME/Contents/MacOS/meilisearch"
chmod +x "$APP_NAME/Contents/MacOS/tunarr-macos" "$APP_NAME/Contents/MacOS/meilisearch"

View File

@@ -12,6 +12,7 @@ import {
getNumericEnvVar,
TUNARR_ENV_VARS,
} from '../util/env.ts';
import { LoggerFactory } from '../util/logging/LoggerFactory.ts';
import type { GlobalArgsType } from './types.ts';
export type ServerArgsType = GlobalArgsType & {
@@ -57,6 +58,16 @@ export const RunServerCommand: CommandModule<GlobalArgsType, ServerArgsType> = {
// port precedence - env var -> argument -> settings
setServerOptions({ ...opts, port: portToUse });
process.on('uncaughtException', (err) => {
LoggerFactory.root.error(err, 'Uncaught exception');
LoggerFactory.root.flush();
});
process.on('unhandledRejection', (err) => {
LoggerFactory.root.error(err, 'Uncaught exception');
LoggerFactory.root.flush();
});
// Hard fail without database connection.
assert(!!container.get<DBAccess>(DBAccess).db);
await container.get(App).start();

View File

@@ -372,6 +372,8 @@ export class MeilisearchService implements ISearchService {
'--db-path',
`${this.dbPath}`,
'--no-analytics',
'--log-level',
'trace',
];
const indexingRamSetting =
@@ -398,7 +400,10 @@ export class MeilisearchService implements ISearchService {
if (
!isWindows() &&
getBooleanEnvVar(TUNARR_ENV_VARS.DEBUG__REDUCE_SEARCH_INDEXING_MEMORY)
getBooleanEnvVar(
TUNARR_ENV_VARS.DEBUG__REDUCE_SEARCH_INDEXING_MEMORY,
os.platform() === 'linux',
)
) {
args.push('--experimental-reduce-indexing-memory-usage');
}
@@ -439,6 +444,9 @@ export class MeilisearchService implements ISearchService {
this.proc = await this.childProcessHelper.spawn(executablePath, args, {
maxAttempts: 3,
additionalOpts: {
cwd: this.serverOptions.databaseDirectory,
},
});
this.logger.info('Meilisearch service started on port %d', this.port);
const outStream = createWriteStream(searchServerLogFile);

View File

@@ -3,7 +3,11 @@ import { isNonEmptyString } from '@/util/index.js';
import { sanitizeForExec } from '@/util/strings.js';
import { inject, injectable } from 'inversify';
import { isEmpty } from 'lodash-es';
import type { ChildProcessByStdio, ExecOptions } from 'node:child_process';
import type {
ChildProcessByStdio,
ExecOptions,
SpawnOptions,
} from 'node:child_process';
import { execFile, spawn } from 'node:child_process';
import events from 'node:events';
import { Readable } from 'node:stream';
@@ -18,6 +22,7 @@ type SpawnOpts = {
name: string;
restartOnFailure: boolean;
maxAttempts: number;
additionalOpts?: SpawnOptions;
};
type ChildProcessEvents = {
@@ -72,6 +77,7 @@ export class ChildProcessWrapper extends ITypedEventEmitter {
this.logger.debug('Starting process: %s %O', this.path, this.args);
const proc = spawn(this.path, this.args, {
...(this.opts.additionalOpts ?? {}),
stdio: ['ignore', 'pipe', 'pipe'],
signal: this.controller.signal,
env: this.env,
@@ -102,6 +108,7 @@ export class ChildProcessWrapper extends ITypedEventEmitter {
if (!this.wasAborted && code !== 0) {
const bufferedBytes = bufferedOut.getLastN().toString('utf-8');
this.logger.error(bufferedBytes);
console.error(bufferedBytes);
}