Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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';

export default function FXServerTab({
data,
Expand All @@ -27,6 +28,9 @@ export default function FXServerTab({
const serverConfigPath =
data['fxserver.serverConfigPath'] ??
SETTINGS_DEFAULTS['fxserver.serverConfigPath'];
const autostart =
(data['fxserver.autostart'] ?? SETTINGS_DEFAULTS['fxserver.autostart']) ===
'true';

function blur(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key !== 'Enter') return;
Expand All @@ -52,6 +56,16 @@ export default function FXServerTab({
</Select>
</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
9 changes: 8 additions & 1 deletion packages/shared/src/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import type { SettingsKey, SettingsKeysByScope } from '../types';

export const SETTINGS_SCOPES = {
general: [],
fxserver: ['onesync', 'executablePath', 'serverDataPath', 'serverConfigPath'],
fxserver: [
'onesync',
'executablePath',
'serverDataPath',
'serverConfigPath',
'autostart',
],
whitelist: ['mode', 'discordBotToken', 'discordGuildId', 'discordRoleIds'],
restarts: ['enabled', 'times'],
} as const;
Expand All @@ -19,6 +25,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