Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ DEPLOY_PATH=./server-data/resources/[dev-resources]/fxManager
# Config file inside server-data
FXSERVER_CFG=server.cfg

# Auto-start the fxServer when the panel launches (once setup is complete).
# Default: on. The in-panel toggle (fxserver.autostart) overrides this.
FXSERVER_AUTOSTART=true

NODE_ENV=production
26 changes: 26 additions & 0 deletions apps/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { checkVersion, getCurrentVersion } from './common/version_check';
import apiRoutes from './routes/api';
import internalRoutes from './routes/internal';
import { processManager } from './modules/process/manager';
import { shouldAutostart } from './modules/process/autostart';
import { gameManager } from './modules/game/manager';
import { ConfigManager } from './modules/config/manager';
import { perfManager } from './modules/perf/manager';
Expand Down Expand Up @@ -174,6 +175,30 @@ if (!isProduction) {
});
}

const maybeAutostart = async () => {
const setupComplete = isFxManagerSetup();
const enabled = cm.isAutostartEnabled();
const status = pm.getState().status;

if (!shouldAutostart({ setupComplete, enabled, status })) {
const reason = !setupComplete
? 'first-run setup not complete'
: !enabled
? 'disabled (fxserver.autostart / FXSERVER_AUTOSTART)'
: `server already '${status}'`;
console.log(`[core] Auto-start skipped: ${reason}`);
return;
}

console.log('[core] Auto-starting fxServer');
try {
await pm.start();
} catch (err) {
// A failed auto-start must never take the panel down with it.
console.error('[core] Auto-start failed', err);
}
};

const start = async () => {
try {
await fastify.listen({ port: webServerPort, host: '0.0.0.0' });
Expand All @@ -182,6 +207,7 @@ const start = async () => {
`\thttp://localhost:${webServerPort}\n` +
`\thttp://${ip}:${webServerPort}`,
);
await maybeAutostart();
} catch (err) {
fastify.log.error(err);
process.exit(1);
Expand Down
8 changes: 8 additions & 0 deletions apps/core/src/modules/config/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ServerConfig,
} from '@fxmanager/shared/types';
import crypto from 'node:crypto';
import { resolveAutostartEnabled } from '../process/autostart';

type CoreSettings = CoreConfig &
ServerConfig & {
Expand Down Expand Up @@ -53,6 +54,13 @@ export class ConfigManager {
this.systemValues.resourceApiToken = crypto.randomUUID();
}

isAutostartEnabled(): boolean {
return resolveAutostartEnabled(
repo.settings.get('fxserver.autostart'),
process.env.FXSERVER_AUTOSTART,
);
}

getSystemValues() {
return this.systemValues;
}
Expand Down
91 changes: 91 additions & 0 deletions apps/core/src/modules/process/autostart.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it } from 'bun:test';
import {
parseBoolFlag,
resolveAutostartEnabled,
shouldAutostart,
} from './autostart';

describe('parseBoolFlag', () => {
it('treats true/1/yes/on (any case, trimmed) as true', () => {
for (const value of ['true', 'TRUE', '1', 'yes', 'YES', 'on', ' On ']) {
expect(parseBoolFlag(value)).toBe(true);
}
});

it('treats everything else as false', () => {
for (const value of ['false', '0', 'no', 'off', '', ' ', 'nope']) {
expect(parseBoolFlag(value)).toBe(false);
}
});
});

describe('resolveAutostartEnabled', () => {
it('defaults to enabled when nothing is configured', () => {
expect(resolveAutostartEnabled(undefined, undefined)).toBe(true);
});

it('uses the env value when no persisted setting exists', () => {
expect(resolveAutostartEnabled(undefined, 'false')).toBe(false);
expect(resolveAutostartEnabled(undefined, 'true')).toBe(true);
});

it('lets the persisted panel setting win over the env value', () => {
expect(resolveAutostartEnabled('false', 'true')).toBe(false);
expect(resolveAutostartEnabled('true', 'false')).toBe(true);
});

it('ignores empty strings and falls through to the next source', () => {
expect(resolveAutostartEnabled('', 'false')).toBe(false);
expect(resolveAutostartEnabled('', '')).toBe(true);
});
});

describe('shouldAutostart', () => {
it('starts when setup is complete, enabled, and the server is stopped', () => {
expect(
shouldAutostart({
setupComplete: true,
enabled: true,
status: 'stopped',
}),
).toBe(true);
});

it('also starts from a crashed state', () => {
expect(
shouldAutostart({
setupComplete: true,
enabled: true,
status: 'crashed',
}),
).toBe(true);
});

it('does not start before first-run setup is complete', () => {
expect(
shouldAutostart({
setupComplete: false,
enabled: true,
status: 'stopped',
}),
).toBe(false);
});

it('does not start when disabled', () => {
expect(
shouldAutostart({
setupComplete: true,
enabled: false,
status: 'stopped',
}),
).toBe(false);
});

it('does not start when the server is already running or busy', () => {
for (const status of ['running', 'starting', 'stopping'] as const) {
expect(
shouldAutostart({ setupComplete: true, enabled: true, status }),
).toBe(false);
}
});
});
28 changes: 28 additions & 0 deletions apps/core/src/modules/process/autostart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { ProcessState } from '@fxmanager/shared/types';

export function parseBoolFlag(value: string): boolean {
return /^(1|true|yes|on)$/i.test(value.trim());
}

// Resolves whether auto-start is enabled.
export function resolveAutostartEnabled(
dbValue: string | undefined,
envValue: string | undefined,
): boolean {
if (dbValue !== undefined && dbValue !== '') return parseBoolFlag(dbValue);
if (envValue !== undefined && envValue !== '') return parseBoolFlag(envValue);
return true;
}

// Whether the FXServer should be auto-started at boot.
export function shouldAutostart(params: {
setupComplete: boolean;
enabled: boolean;
status: ProcessState;
}): boolean {
return (
params.setupComplete &&
params.enabled &&
(params.status === 'stopped' || params.status === 'crashed')
);
}
14 changes: 14 additions & 0 deletions apps/webpanel/src/pages/settings/tabs/fxserver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SETTINGS_DEFAULTS } from '@fxmanager/shared/constants';
import type { SettingsTabProps } from '@/types/settings';
import { Separator } from '@fxmanager/ui/components/separator';
import { Input } from '@fxmanager/ui/components/input';
import { Switch } from '@fxmanager/ui/components/switch';
import { Field, FieldDescription } from '@fxmanager/ui/components/field';
import { validateStartupArguments } from '@fxmanager/shared/utils';
import { useState } from 'react';
Expand Down Expand Up @@ -72,6 +73,9 @@ export default function FXServerTab({
const serverConfigPath =
data['fxserver.serverConfigPath'] ??
SETTINGS_DEFAULTS['fxserver.serverConfigPath'];
const autostart =
(data['fxserver.autostart'] ?? SETTINGS_DEFAULTS['fxserver.autostart']) ===
'true';

return (
<div className="space-y-4">
Expand Down Expand Up @@ -104,6 +108,16 @@ export default function FXServerTab({
/>
</SettingRow>

<SettingRow label="Auto-start server on launch">
<Switch
checked={autostart}
disabled={disabled}
onCheckedChange={(checked) =>
onChange('fxserver.autostart', checked ? 'true' : 'false')
}
/>
</SettingRow>

<Separator />

<SettingRow label="Server Executable Path">
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const SETTINGS_SCOPES = {
'executablePath',
'serverDataPath',
'serverConfigPath',
'autostart',
],
whitelist: ['mode', 'discordBotToken', 'discordGuildId', 'discordRoleIds'],
restarts: ['enabled', 'times'],
Expand All @@ -25,6 +26,7 @@ export const SETTINGS_DEFAULTS = {
'fxserver.executablePath': './FXServer',
'fxserver.serverDataPath': './server-data',
'fxserver.serverConfigPath': 'server.cfg',
'fxserver.autostart': 'true',
'whitelist.mode': 'none',
'restarts.enabled': 'false',
'restarts.times': '',
Expand Down
Loading