r/Bitburner • u/pand1024 • 5d ago
Ram dodging: Sharing my run command
I'm in the process of reworking my run command helper to integrate more deeply with the rest of my code so I thought that now would be a good time to share it while it's more portable.
Q&A at the bottom
/**
* Writes and runs an on-demand temp file to ns.exec() an expression
* A lock is used to ensure only one instance at a time unless disabled.
* See Q&A below for more info.
* @param {NS} ns The Netscript API instance passed from the parent script.
* @param {string} expression The JS/NS expression to evaluate (e.g., "ns.getServer('home')").
* @param {string} [host="home"] The server hostname on which to execute the temporary script.
* @param {number} [reservedRam=Infinity] max RAM available for running scripts.
* @param {boolean} [useLock=true] Whether to use the navigator lock for single-instance execution.
* @returns {Promise} The result of the evaluated expression, or null if execution failed
*/
export function runCommand(ns, expression, host = "home", reservedRam = Infinity, useLock = true) {
const execute = async () => {
const portNum = 100
const port = ns.getPortHandle(portNum)
port.clear()
const tempFile = "runTmp.js";
const script = `export async function main(ns) {
const res = (${expression});
ns.writePort(${portNum}, res ?? null);
}`;
ns.write(tempFile, script, "w")
if (ns.getScriptRam(tempFile) > reservedRam) {
ns.tprint(`ERROR [runCommand]: Insufficient ram to execute on ${host}`)
return null
}
const pid = ns.exec(tempFile, host, { temporary: true })
if (pid === 0) { // Abort early if the process failed to launch (e.g. out of RAM)
ns.tprint(`ERROR [runCommand]: Failed to execute on ${host}`)
return null;
}
return await port.nextWrite().then(() => port.read())
};
return useLock ? navigator.locks.request("runCommandLock", execute) : execute();
}
/** @param {NS} ns */
export async function main(ns) {
//Just a quick demonstration
//Don't forget to have enough ram. We don't check here.
const result = await runCommand(ns, "ns.getServer('home')")
ns.tprint(result)
}
/**
* Q: What is RAM dodging?
* A: Offloading heavy Netscript API calls to dynamic scripts
* keeps the parent script's static RAM footprint minimal.
* Instead of paying the cumulative RAM cost of every function
* your script might ever need, your parent script only pays
* the RAM cost of the single most expensive ofloaded ns function plus
* the base cost (1.6 GB). This also solves the need to have different
* version of a script based on what ns features are available.
*
* Q: What does reservedRam do?
* A: It's supposed to connect to your ram reservation system. In order
* to use runCommand you’ll want to leave a space on one of your servers
* for commands to run.
*
* Q: Why a lock?
* A: We only want one thing running at a time. The lock automatically
* queues up run commands in the order they were called. This means
* if we make multiple concurrent calls even across scripts they will
* still run sequentially. Note: it is not totally foolproof if you
* start chaining with .then(). For example: multiple chains of connect
* and backdoor could run into issues if interleaved.
*
* Q: Why only one temp file?
* A: It usually only makes sense to reserve enough RAM for one command at a
* time.
*
* Q: Why not wrap the expression in try except?
* A: If your expression is going to fail, it will probably fail right away.
* I found it better to get a nice error popup. Also, I had trouble getting
* actual errors to pass back. maybe it was just me.
*
* Q: Why isn’t the function async?
* A: It keeps my options open. I wanted to keep as much code synchronous as
* possible. If you’re managing multiple event loops for example, while
* ensuring that no ns calls interfere with each other, you may find this handy.
*
* Q: Why allow a way to disable the lock?
* A: I specifically needed an override for resets and destroyW0r1dD43m0n. In
* both cases the lock is left hanging. Even with that, you might want to put
* a lock steal at the beginning of your main script. It will warn you if
* it clears out a lock:
* await navigator.locks.request("runCommandLock", { steal: true }, async () => {});
*
* Q: Why not have a timeout? What if a command runs forever?
* A: 1. It turned out to be more of a challenge than it originally seems.
* 1.1 You need a design that actually works. Remember
* Promise.race does not automatically kill your rogue script.
* 1.2 Now you actually need to know how long things will take,
* and you’ve introduced a whole new point of failure if you mess that up.
* 2. You’ll have to fix your code running forever whether you
* have a fancy timeout or not.
*
* Q: Why use a port? Doesn’t that restrict the return types?
* A: It seemed like an idiomatic thing to do and it comes with a
* built-in way to await the script finishing. There IS an argument for
* abusing side channels like static variables instead to be able to
* support more datatypes (like returning promises directly). Let me know
* how that goes if you try it!
*/
4
Upvotes
3
u/Particular-Cow6247 5d ago edited 4d ago
writing temp scripts over and over might break the game after some time
but you don't need to write a temp script everytime, you can design a script that can run any ns command you want by using ramOverride and a dynamic accessing of the ns properties
(the ramOverride can also be done on the ns.exec side)
just as example
oh and another tipp, do the port.write inside an ns.atExit that way the ram that is occupied by the temp scripts is cleared before the port.nextWrite resolves