BUG: Fix missed cases in offline server handling (#2495)

There were two large holes in the existing offline server handling:

1. It didn't include IPs, so scripts that used IPs instead of hostnames
   would get exceptions thrown for "server not found."
2. Coverage was very low for non-Darknet APIs. Maybe most of them don't
   need to be covered, but many obvious ones like "ps", "killall" and
   "hasRootAccess" were missing. IMO the only reliable answer is one
   that enforces *all* are covered via the type system.

To accomplish the second part, helpers.getServer() was changed to return
null when a server is offline. This intentionally breaks a lot of its
utility, which was to return a server unconditionally. To compensate,
its utility was increased - it now also does unknown argument
processing, allowing it to subsume a common line that all callers were
repeating.

Some callers switched to ctx.workerScript.getServer(), because they
didn't actually need to be using helpers.getServer(). Similarly, a few
callsites switched to GetServerOrThrow(), for the cases where it should
be guaranteed that the server is valid. The rest are returning a
default/failure response when the server is offline. (Except for
contracts, which threw on failure already anyway.)
This commit is contained in:
David Walker
2026-02-15 10:29:47 -08:00
committed by GitHub
parent b5ab495837
commit b51ed8fd59
50 changed files with 478 additions and 471 deletions
+3 -3
View File
@@ -10,7 +10,7 @@ import {
renameCloudServer,
upgradeCloudServer,
} from "../Server/ServerPurchases";
import { DeleteServer, AddToAllServers, createUniqueRandomIp } from "../Server/AllServers";
import { DeleteServer, AddToAllServers, createUniqueRandomIp, GetServerOrThrow } from "../Server/AllServers";
import { safelyCreateUniqueServer } from "../Server/ServerHelpers";
import { formatMoney } from "../ui/formatNumber";
import { isIPAddress } from "../Types/strings";
@@ -195,12 +195,12 @@ export function NetscriptCloud(): InternalAPI<Cloud> {
return false;
},
getServerNames:
(ctx) =>
() =>
(_returnOpts): string[] => {
const returnOpts = helpers.hostReturnOptions(_returnOpts);
const res: string[] = [];
for (const hostname of Player.purchasedServers) {
const server = helpers.getServer(ctx, hostname);
const server = GetServerOrThrow(hostname);
const id = helpers.returnServerID(server, returnOpts);
res.push(id);
}
+20 -22
View File
@@ -10,14 +10,18 @@ import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { getEnumHelper } from "../utils/EnumHelper";
export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
const getCodingContract = function (ctx: NetscriptContext, hostname: string, filename: string): CodingContract {
const server = helpers.getServer(ctx, hostname);
const contract = server.getContract(filename);
if (contract == null) {
throw helpers.errorMessage(ctx, `Cannot find contract '${filename}' on server '${hostname}'`);
const getCodingContract = function (
ctx: NetscriptContext,
_host: unknown,
filename: string,
): [CodingContract, BaseServer] {
const [server, host] = helpers.getServer(ctx, _host);
const contract = server?.getContract(filename);
if (server == null || contract == null) {
throw helpers.errorMessage(ctx, `Cannot find contract '${filename}' on server '${host}'`);
}
return contract;
return [contract, server];
};
function attemptContract(
@@ -81,28 +85,22 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
return {
attempt: (ctx) => (answer, _filename, _host?) => {
const filename = helpers.string(ctx, "filename", _filename);
const host = _host ? helpers.string(ctx, "host", _host) : ctx.workerScript.hostname;
const contract = getCodingContract(ctx, host, filename);
const server = helpers.getServer(ctx, host);
const [contract, server] = getCodingContract(ctx, _host, filename);
return attemptContract(ctx, server, contract, answer);
},
getContractType: (ctx) => (_filename, _host?) => {
const filename = helpers.string(ctx, "filename", _filename);
const host = _host ? helpers.string(ctx, "host", _host) : ctx.workerScript.hostname;
const contract = getCodingContract(ctx, host, filename);
const [contract] = getCodingContract(ctx, _host, filename);
return contract.getType();
},
getData: (ctx) => (_filename, _host?) => {
const filename = helpers.string(ctx, "filename", _filename);
const host = _host ? helpers.string(ctx, "host", _host) : ctx.workerScript.hostname;
const contract = getCodingContract(ctx, host, filename);
const [contract] = getCodingContract(ctx, _host, filename);
return structuredClone(contract.getData());
},
getContract: (ctx) => (_filename, _host?) => {
const filename = helpers.string(ctx, "filename", _filename);
const host = _host ? helpers.string(ctx, "host", _host) : ctx.workerScript.hostname;
const server = helpers.getServer(ctx, host);
const contract = getCodingContract(ctx, host, filename);
const [contract, server] = getCodingContract(ctx, _host, filename);
// asserting type here is required, since it is not feasible to properly type getData
return {
type: contract.type,
@@ -121,20 +119,20 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
},
getDescription: (ctx) => (_filename, _host?) => {
const filename = helpers.string(ctx, "filename", _filename);
const host = _host ? helpers.string(ctx, "host", _host) : ctx.workerScript.hostname;
const contract = getCodingContract(ctx, host, filename);
const [contract] = getCodingContract(ctx, _host, filename);
return contract.getDescription();
},
getNumTriesRemaining: (ctx) => (_filename, _host?) => {
const filename = helpers.string(ctx, "filename", _filename);
const host = _host ? helpers.string(ctx, "host", _host) : ctx.workerScript.hostname;
const contract = getCodingContract(ctx, host, filename);
const [contract] = getCodingContract(ctx, _host, filename);
return contract.getMaxNumTries() - contract.tries;
},
createDummyContract: (ctx) => (_type, _host?) => {
const type = getEnumHelper("CodingContractName").nsGetMember(ctx, _type);
const host = _host ? helpers.string(ctx, "host", _host) : ctx.workerScript.hostname;
const server = helpers.getServer(ctx, host);
const [server] = helpers.getServer(ctx, _host);
if (server == null) {
return null;
}
return generateDummyContract(type, server);
},
getContractTypes: () => () => Object.values(CodingContractName),
+3 -8
View File
@@ -470,16 +470,11 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
const server = Player.getCurrentServer();
cat([filename], server);
},
connect: (ctx) => (_host) => {
connect: (ctx) => (_host?) => {
helpers.checkSingularityAccess(ctx);
const host = helpers.string(ctx, "host", _host);
if (!host) {
throw helpers.errorMessage(ctx, `Invalid server: '${host}'`);
}
const target = GetServer(host);
const [target, host] = helpers.getServer(ctx, _host);
if (target == null) {
throw helpers.errorMessage(ctx, `Invalid server: '${host}'`);
return false;
}
// Adjacent servers
+1 -1
View File
@@ -44,7 +44,7 @@ export function NetscriptStanek(): InternalAPI<IStanek> {
);
}
//Charge the fragment
const cores = helpers.getServer(ctx, ctx.workerScript.hostname).cpuCores;
const cores = ctx.workerScript.getServer().cpuCores;
const coreBonus = getCoreBonus(cores);
const inBonus = staneksGift.inBonus();
const time = inBonus ? 200 : 1000;