bitburner/scripts/crawler.js

119 lines
5.0 KiB
JavaScript

import {ArgParser} from '/scripts/lib/arg-parser';
import {availableExploits} from '/scripts/rootkit';
import {copyWithDependencies} from '/scripts/copy';
import {availableThreads} from "/scripts/lib/utils";
/**
* Scan the network for servers.
*
* @param {NS} ns - BitBurner API
* @param {string} server - Entrypoint to network
* @param {number} maxDepth - Depth to scan to
* @returns {[string[], Object]} - A tuple including an array of discovered servers & a tree of the network
*/
export function scanNetwork(ns, server = ns.getHostname(), maxDepth = Infinity) {
let discovered = [server];
function scan (server, depth = 1) {
if(depth > maxDepth) return {};
const localTargets = ns.scan(server).filter(s => !discovered.includes(s));
discovered = [...discovered, ...localTargets];
return localTargets.reduce((acc, s) => ({...acc, [s]: scan(s, depth + 1)}), {});
}
const network = scan(server);
return [discovered.slice(1), network];
}
/**
* Search the network for servers to execute a script against.
*
* @param {NS} ns - BitBurner API
*/
export async function main(ns) {
// Setup
ns.disableLog('ALL');
const argParser = new ArgParser('crawler.js', 'Search the network for servers to execute a script against.', [
{name: 'script', desc: 'Script to copy & execute'},
{name: 'args', desc: 'Arguments for script. Forward the discovered server with: {{SERVER}}', optional: true, extras: true, type: 'string'},
{name: 'cpu', desc: 'Number of CPU threads to start script with, will use maximum if not specified', flags: ['-c', '--cpu']},
{name: 'depth', desc: 'Depth to scan to, defaults to 3', flags: ['-d', '--depth'], default: Infinity},
{name: 'kill', desc: 'Kill all running scripts on the server', flags: ['-k', '--kill'], default: false},
{name: 'level', desc: 'Skip servers with higher hack level, defaults to current hack level', flags: ['--level'], default: ns.getHackingLevel()},
{name: 'remoteExec', desc: 'Execute script on remote server', flags: ['-e', '--remote-exec'], default: false},
{name: 'rooted', desc: 'Only servers that have been rooted', flags: ['-r', '--rooted'], default: false},
{name: 'notRooted', desc: 'Only servers that have not been rooted', flags: ['-n', '--not-rooted'], default: false},
{name: 'ports', desc: 'Skip servers with too many closed ports', flags: ['-p', '--ports'], default: availableExploits(ns).length},
{name: 'quite', desc: 'Suppress program output', flags: ['-q', '--quite'], default: false},
{name: 'verbose', desc: 'Display the server names in the final report', flags: ['-v', '--verbose'], default: false},
]);
const args = argParser.parse(ns.args);
// Help
if(args['help'] || args['_error'].length)
return ns.tprint(argParser.help(args['help'] ? null : args['_error'][0], args['_command']));
// Run
const localhost = ns.getHostname();
const [servers, network] = scanNetwork(ns);
let complete = [], failed = [], skipped = [];
for(let server of servers) {
// Check root status if needed
const rooted = ns.hasRootAccess(server);
if(args['rooted'] && !rooted) continue;
if(args['notRooted'] && rooted) continue;
// Skip invalid servers
if(server == 'home' || args['level'] < ns.getServerRequiredHackingLevel(server) || args['ports'] < ns.getServerNumPortsRequired(server)) {
skipped.push(server);
continue;
}
// Start script
if(args['kill']) ns.killall(server);
const scriptArgs = args['args'].map(arg => arg.toUpperCase() == '{{SERVER}}' ? server : arg);
const threads = args['cpu'] || availableThreads(ns, args['script'], server) || 1;
if(args['remoteExec']) await copyWithDependencies(ns, args['script'], server);
const pid = ns.exec(args['script'], args['remoteExec'] ? server : localhost, threads, ...scriptArgs);
if(pid == 0) {
failed.push(server);
continue;
}
// Wait for script to finish if local
if(!args['remoteExec'])
while(ns.isRunning(args['script'], localhost, ...scriptArgs))
await ns.sleep(1000);
complete.push(server);
}
// Output report
if(!args['quite']) {
ns.tprint('===================================================');
ns.tprint(`Crawler: ${complete.length + failed.length + skipped.length} Servers`);
ns.tprint('===================================================');
if(args['verbose']) {
ns.tprint(`Complete (${complete.length}):`);
if(complete.length) ns.tprint(complete.join(', '));
ns.tprint('');
ns.tprint(`Failed (${failed.length}):`);
if(failed.length) ns.tprint(failed.join(', '));
ns.tprint('');
ns.tprint(`Skipped (${skipped.length}):`);
if(skipped.length) ns.tprint(skipped.join(', '));
ns.tprint('');
} else {
ns.tprint(`Complete: ${complete.length}\tFailed: ${failed.length}\tSkipped: ${skipped.length}`);
ns.tprint('');
}
}
}
/**
* BitBurner autocomplete.
*
* @param {{servers: string[], txts: string[], scripts: string[], flags: string[]}} data - Contextual information
* @returns {string[]} - Pool of autocomplete options
*/
export function autocomplete(data) {
return [...data.scripts, ...data.servers, '{{SERVER}}'];
}