Skip to content
Open
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
16 changes: 16 additions & 0 deletions core/modules/ConfigStore/schema/restarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,25 @@ const resourceStartingTolerance = typeDefinedConfig({
fixer: SYM_FIXER_DEFAULT,
});

const onlyWhenEmpty = typeDefinedConfig({
name: 'Delay Restart Until Empty',
default: false,
validator: z.boolean(),
fixer: SYM_FIXER_DEFAULT,
});

const maxRestartDelayMinutes = typeDefinedConfig({
name: 'Max Restart Delay',
default: 30,
validator: z.number().int().min(1).max(120),
fixer: SYM_FIXER_DEFAULT,
});


export default {
schedule,
bootGracePeriod,
resourceStartingTolerance,
onlyWhenEmpty,
maxRestartDelayMinutes,
} as const;
56 changes: 54 additions & 2 deletions core/modules/FxScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ const getNextScheduled = (parsedSchedule: ParsedTime[]): RestartInfo => {
* or a temporary schedule set by the user at runtime.
*/
export default class FxScheduler {
static readonly configKeysWatched = ['restarter.schedule'];
static readonly configKeysWatched = ['restarter.schedule', 'restarter.onlyWhenEmpty', 'restarter.maxRestartDelayMinutes'];
private nextTempSchedule: RestartInfo | false = false;
private calculatedNextRestartMinuteFloorTs: number | false = false;
private nextSkip: number | false = false;
private pendingDelayedRestart: RestartInfo | false = false;
private pendingDelayedRestartSince: number | false = false;

constructor() {
//Initial check to update status
Expand All @@ -74,6 +76,8 @@ export default class FxScheduler {
handleConfigUpdate(updatedConfigs: UpdateConfigKeySet) {
this.nextSkip = false;
this.nextTempSchedule = false;
this.pendingDelayedRestart = false;
this.pendingDelayedRestartSince = false;
this.checkSchedule();
txCore.webServer.webSocket.pushRefresh('status');
}
Expand All @@ -86,6 +90,8 @@ export default class FxScheduler {
handleServerClose() {
//Clear temp schedule, recalculates next restart
if (this.nextTempSchedule) this.nextTempSchedule = false;
this.pendingDelayedRestart = false;
this.pendingDelayedRestartSince = false;
this.checkSchedule(true);

//Check if next scheduled restart is in less than 2 hours
Expand Down Expand Up @@ -116,12 +122,14 @@ export default class FxScheduler {
nextRelativeMs: this.calculatedNextRestartMinuteFloorTs - thisMinuteTs,
nextSkip: this.nextSkip === this.calculatedNextRestartMinuteFloorTs,
nextIsTemp: !!this.nextTempSchedule,
nextIsDelayed: !!this.pendingDelayedRestart,
};
} else {
return {
nextRelativeMs: false,
nextSkip: false,
nextIsTemp: false,
nextIsDelayed: false,
} as const;
}
}
Expand All @@ -135,7 +143,13 @@ export default class FxScheduler {
setNextSkip(enabled: boolean, author?: string) {
if (enabled) {
let prevMinuteFloorTs, temporary;
if (this.nextTempSchedule) {
if (this.pendingDelayedRestart) {
prevMinuteFloorTs = this.pendingDelayedRestart.minuteFloorTs;
temporary = !!this.nextTempSchedule;
this.pendingDelayedRestart = false;
this.pendingDelayedRestartSince = false;
this.nextTempSchedule = false;
} else if (this.nextTempSchedule) {
prevMinuteFloorTs = this.nextTempSchedule.minuteFloorTs;
temporary = true;
this.nextTempSchedule = false;
Expand Down Expand Up @@ -232,6 +246,12 @@ export default class FxScheduler {
* Checks the schedule to see if it's time to announce or restart the server
*/
async checkSchedule(calculateOnly = false) {
//If a restart is being delayed due to players online, keep checking that instead of recalculating the schedule
if (this.pendingDelayedRestart) {
if (calculateOnly) return;
return this.processPendingDelayedRestart();
}

//Check settings and temp scheduled restart
let nextRestart: RestartInfo;
if (this.nextTempSchedule) {
Expand Down Expand Up @@ -260,6 +280,14 @@ export default class FxScheduler {

//Checking if server restart or warning time
if (nextDistMins === 0) {
//Delay the restart if the server is not empty
if (txConfig.restarter.onlyWhenEmpty && txCore.fxPlayerlist.onlineCount > 0) {
console.warn(`Delaying scheduled restart at ${nextRestart.string} because there are players online.`);
this.pendingDelayedRestart = nextRestart;
this.pendingDelayedRestartSince = Date.now();
return;
}

//restart server
this.triggerServerRestart(
`scheduled restart at ${nextRestart.string}`,
Expand Down Expand Up @@ -300,6 +328,30 @@ export default class FxScheduler {
}


/**
* Re-checks a restart that got delayed due to players being online.
* Restarts once the server is empty, or once the configured max delay is reached.
*/
async processPendingDelayedRestart() {
if (!this.pendingDelayedRestart || !this.pendingDelayedRestartSince) return;
const onlineCount = txCore.fxPlayerlist.onlineCount;
const delayedMinutes = Math.floor((Date.now() - this.pendingDelayedRestartSince) / 60_000);
if (onlineCount > 0 && delayedMinutes < txConfig.restarter.maxRestartDelayMinutes) {
console.verbose.log(`Restart still delayed, ${onlineCount} players online.`);
return;
}

const restartInfo = this.pendingDelayedRestart;
this.pendingDelayedRestart = false;
this.pendingDelayedRestartSince = false;
this.nextTempSchedule = false;
this.triggerServerRestart(
`scheduled restart at ${restartInfo.string} (delayed)`,
txCore.translator.t('restarter.schedule_reason', { time: restartInfo.string }),
);
}


/**
* Triggers FXServer restart and logs the reason.
*/
Expand Down
7 changes: 5 additions & 2 deletions panel/src/layout/ServerSidebar/ServerSchedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ export default function ServerSchedule() {
const tempFlag = (scheduler.nextIsTemp) ? '(temp)' : '';
const relativeTime = msToDuration(scheduler.nextRelativeMs, { units: ['h', 'm'] });
const isLessThanMinute = scheduler.nextRelativeMs < 60_000;
if (isLessThanMinute) {
if (scheduler.nextIsDelayed) {
disableAddEditBtn = true;
nextScheduledText = `delayed, waiting for players to leave ${tempFlag}`;
} else if (isLessThanMinute) {
disableAddEditBtn = true;
nextScheduledText = `right now ${tempFlag}`;
} else {
Expand All @@ -108,7 +111,7 @@ export default function ServerSchedule() {
}
} else {
nextScheduledClasses = 'text-warning-inline';
if (!isLessThanMinute) {
if (!isLessThanMinute || scheduler.nextIsDelayed) {
showCancelBtn = true;
}
}
Expand Down
36 changes: 36 additions & 0 deletions panel/src/pages/Settings/tabCards/fxserver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ export const pageConfigs = {
onesync: getPageConfig('server', 'onesync', true),
autoStart: getPageConfig('server', 'autoStart', true),
resourceTolerance: getPageConfig('restarter', 'resourceStartingTolerance', true),
restarterOnlyWhenEmpty: getPageConfig('restarter', 'onlyWhenEmpty'),
restarterMaxDelay: getPageConfig('restarter', 'maxRestartDelayMinutes', true),
} as const;

export default function ConfigCardFxserver({ cardCtx, pageCtx }: SettingsCardProps) {
Expand Down Expand Up @@ -389,6 +391,20 @@ export default function ConfigCardFxserver({ cardCtx, pageCtx }: SettingsCardPro
<strong>Note:</strong> Make sure your schedule matches your server time and not your local time.
</SettingItemDesc>
</SettingItem>
<SettingItem label="Delay Restart Until Empty">
<SwitchText
id={cfg.restarterOnlyWhenEmpty.eid}
checkedLabel="Enabled"
uncheckedLabel="Disabled"
checked={states.restarterOnlyWhenEmpty}
onCheckedChange={cfg.restarterOnlyWhenEmpty.state.set}
disabled={pageCtx.isReadOnly}
/>
<SettingItemDesc>
Postpone a scheduled restart while there are players online, instead of restarting at the exact scheduled time. <br />
The restart is forced once the max delay below is reached.
</SettingItemDesc>
</SettingItem>
<SettingItem label="Quiet Mode">
<SwitchText
id={cfg.quietMode.eid}
Expand Down Expand Up @@ -494,6 +510,26 @@ export default function ConfigCardFxserver({ cardCtx, pageCtx }: SettingsCardPro
<strong>Note:</strong> If you are getting <InlineCode>failed to start in time</InlineCode> errors, increase this value.
</SettingItemDesc>
</SettingItem>
<SettingItem label="Max Restart Delay" htmlFor={cfg.restarterMaxDelay.eid} showIf={showAdvanced && states.restarterOnlyWhenEmpty}>
<Select
value={selectNumberUtil.toUi(states.restarterMaxDelay)}
onValueChange={(val) => cfg.restarterMaxDelay.state.set(selectNumberUtil.toCfg(val))}
disabled={pageCtx.isReadOnly}
>
<SelectTrigger id={cfg.restarterMaxDelay.eid}>
<SelectValue placeholder="Select..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="15">15 minutes</SelectItem>
<SelectItem value="30">30 minutes (default)</SelectItem>
<SelectItem value="60">1 hour</SelectItem>
<SelectItem value="120">2 hours</SelectItem>
</SelectContent>
</Select>
<SettingItemDesc>
How long to postpone a scheduled restart for before forcing it, even if players are still online.
</SettingItemDesc>
</SettingItem>
</SettingsCardShell>
)
}
2 changes: 2 additions & 0 deletions shared/socketioTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ export type GlobalStatusType = {
nextRelativeMs: number;
nextSkip: boolean;
nextIsTemp: boolean;
nextIsDelayed: boolean;
} | {
nextRelativeMs: false;
nextSkip: false;
nextIsTemp: false;
nextIsDelayed: false;
};
}

Expand Down