Implemented 'kill by PID' functionality

This commit is contained in:
danielyxie 2019-07-11 19:37:17 -07:00
parent d94516f39b
commit 4f2f75762c
14 changed files with 191 additions and 86 deletions

@ -312,9 +312,12 @@ kill
^^^^
$ kill [script name] [args...]
$ kill [pid]
Kill the script specified by the script name and arguments. Each argument must
be separated by a space. Remember that a running script is uniquely identified
Kill the script specified by the script filename and arguments OR by its PID.
If you are killing the script using its filename and arguments, then each argument
must be separated by a space. Remember that a running script is uniquely identified
by both its name and the arguments that are used to start it. So, if a script
was ran with the following arguments::
@ -324,8 +327,7 @@ Then to kill this script the same arguments would have to be used::
$ kill foo.script 50e3 sigma-cosmetics
Note that after issuing the 'kill' command for a script, it may take a few seconds for
the script to actually stop running.
If you are killing the script using its PID, then the PID argument must be numeric.
killall
^^^^^^^

@ -14,10 +14,15 @@ exec() Netscript Function
Run a script as a separate process on a specified server. This is similar to the *run* function except
that it can be used to run a script on any server, instead of just the current server.
Returns true if the script is successfully started, and false otherwise.
If the script was successfully started, then this functions returns the PID
of that script. Otherwise, it returns 0.
Running this function with a *numThreads* argument of 0 will return false without running the script.
However, running this function with a negative *numThreads* argument will cause a runtime error.
.. note:: PID stands for Process ID. The PID is a unique identifier for each script.
The PID will always be a positive integer.
.. warning:: Running this function with a *numThreads* argument of 0 will return 0 without
running the script. However, running this function with a negative *numThreads*
argument will cause a runtime error.
The simplest way to use the *exec* command is to call it with just the script name and the target server.
The following example will try to run *generic-hack.script* on the *foodnstuff* server::

@ -27,3 +27,22 @@ kill() Netscript Function
The following will try to kill a script named *foo.script* on the current server that was ran with the arguments 1 and "foodnstuff"::
kill("foo.script", getHostname(), 1, "foodnstuff");
.. js:function:: kill(scriptPid)
:param number scriptPid: PID of the script to kill
:RAM cost: 0.5 GB
Kills the script with the specified PID. Killing a script by its PID will typically
have better performance, especially if you have many scripts running.
If this function successfully kills the specified script, then it will return true.
Otherwise, it will return false.
*Examples:*
The following example will try to kill the script with the PID 10::
if (kill(10)) {
print("Killed script with PID 10!");
}

@ -13,10 +13,15 @@ run() Netscript Function
Run a script as a separate process. This function can only be used to run scripts located on the current server (the server
running the script that calls this function).
Returns true if the script is successfully started, and false otherwise.
If the script was successfully started, then this functions returns the PID
of that script. Otherwise, it returns 0.
Running this function with a *numThreads* argument of 0 will return false without running the script.
However, running this function with a negative *numThreads* argument will cause a runtime error.
.. note:: PID stands for Process ID. The PID is a unique identifier for each script.
The PID will always be a positive integer.
.. warning:: Running this function with a *numThreads* argument of 0 will return 0 without
running the script. However, running this function with a negative *numThreads*
argument will cause a runtime error.
The simplest way to use the *run* command is to call it with just the script name. The following example will run
'foo.script' single-threaded with no arguments::

@ -51,8 +51,6 @@ Here is a summary of all rules you need to follow when writing Netscript JS code
* grow
* weaken
* sleep
* run
* exec
* prompt
* wget

@ -230,10 +230,17 @@ export let CONSTANTS: IMap<any> = {
* Bug fix: workForFaction() function now properly accounts for disabled logs
* When writing to a file, the write() function now casts the data being written to a string (using String())
* BitNode-selection page now shows what Source-File level you have for each BitNode
* Overloaded kill() function so that you can kill a script by its PID
* spawn() now only takes 10 seconds to run (decreased from 20 seconds)
* run() and exec() now return the PID of the newly-executed scripts, rather than a boolean
** (A PID is just a positive integer)
* run(), exec(), and spawn() no longer need to be await-ed in NetscriptJS
Misc Changes
* The 'kill' Terminal command can now kill a script by its PID
* Added 'Solarized Dark' theme to CodeMirror editor
* After Infiltration, you will now return to the company page rather than the city page
* Bug fix: Stock Market UI should no longer crash for certain locale settings
* Bug fix: You can now properly remove unfinished programs (the *.exe-N%-INC files)
`
}

@ -293,7 +293,7 @@ function NetscriptFunctions(workerScript) {
if (fn != null && typeof fn === "string") {
// Get Logs of another script
if (ip == null) { ip = workerScript.serverIp; }
const server = getServer(ip, callingFnName);
const server = safeGetServer(ip, callingFnName);
return findRunningScript(fn, scriptArgs, server);
}
@ -973,6 +973,8 @@ function NetscriptFunctions(workerScript) {
if (scriptname == null || threads == null) {
throw makeRuntimeRejectMsg(workerScript, "Invalid scriptname or numThreads argument passed to spawn()");
}
const spawnDelay = 10;
setTimeoutRef(() => {
if (scriptname === undefined) {
throw makeRuntimeRejectMsg(workerScript, "spawn() call has incorrect number of arguments. Usage: spawn(scriptname, numThreads, [arg1], [arg2]...)");
@ -990,40 +992,52 @@ function NetscriptFunctions(workerScript) {
}
return runScriptFromScript(scriptServer, scriptname, argsForNewScript, workerScript, threads);
}, 20e3);
if (workerScript.disableLogs.ALL == null && workerScript.disableLogs.spawn == null) {
workerScript.scriptRef.log("spawn() will execute " + scriptname + " in 20 seconds");
}, spawnDelay * 1e3);
if (workerScript.shouldLog("spawn")) {
workerScript.scriptRef.log(`spawn() will execute ${scriptname} in ${spawnDelay} seconds`);
}
NetscriptFunctions(workerScript).exit();
},
kill: function(filename, ip) {
kill: function(filename, ip, ...scriptArgs) {
updateDynamicRam("kill", getRamCost("kill"));
let res;
const killByPid = (typeof filename === "number");
if (killByPid) {
// Kill by pid
res = killWorkerScript(filename);
} else {
// Kill by filename/ip
if (filename === undefined || ip === undefined) {
throw makeRuntimeRejectMsg(workerScript, "kill() call has incorrect number of arguments. Usage: kill(scriptname, server, [arg1], [arg2]...)");
}
var server = getServer(ip);
if (server == null) {
workerScript.scriptRef.log("kill() failed. Invalid IP or hostname passed in: " + ip);
throw makeRuntimeRejectMsg(workerScript, "kill() failed. Invalid IP or hostname passed in: " + ip);
}
var argsForKillTarget = [];
for (var i = 2; i < arguments.length; ++i) {
argsForKillTarget.push(arguments[i]);
}
var runningScriptObj = findRunningScript(filename, argsForKillTarget, server);
const server = safeGetServer(ip);
const runningScriptObj = getRunningScript(filename, ip, "kill", scriptArgs);
if (runningScriptObj == null) {
workerScript.scriptRef.log("kill() failed. No such script "+ filename + " on " + server.hostname + " with args: " + arrayToString(argsForKillTarget));
workerScript.log(`tail() failed. ${getCannotFindRunningScriptErrorMessage(filename, ip, scriptArgs)}`)
return false;
}
var res = killWorkerScript(runningScriptObj, server.ip);
res = killWorkerScript(runningScriptObj, server.ip);
}
if (res) {
if (workerScript.disableLogs.ALL == null && workerScript.disableLogs.kill == null) {
workerScript.scriptRef.log("Killing " + filename + " on " + server.hostname + " with args: " + arrayToString(argsForKillTarget) + ". May take up to a few minutes for the scripts to die...");
if (workerScript.shouldLog("kill")) {
if (killByPid) {
workerScript.log(`Killing script with PID ${filename}`);
} else {
workerScript.log(`Killing ${filename} on ${ip} with args: ${arrayToString(scriptArgs)}.`);
}
}
return true;
} else {
if (workerScript.disableLogs.ALL == null && workerScript.disableLogs.kill == null) {
workerScript.scriptRef.log("kill() failed. No such script "+ filename + " on " + server.hostname + " with args: " + arrayToString(argsForKillTarget));
if (workerScript.shouldLog("kill")) {
if (killByPid) {
workerScript.log(`kill() failed. No such script with PID ${filename}`);
} else {
workerScript.log(`kill() failed. No such script ${filename} on ${ip} with args: ${arrayToString(scriptArgs)}`);
}
}
return false;
}

@ -605,42 +605,53 @@ export function loadAllRunningScripts() {
* Run a script from inside another script (run(), exec(), spawn(), etc.)
*/
export function runScriptFromScript(server, scriptname, args, workerScript, threads=1) {
//Check if the script is already running
let runningScriptObj = findRunningScript(scriptname, args, server);
if (runningScriptObj != null) {
workerScript.scriptRef.log(scriptname + " is already running on " + server.hostname);
return Promise.resolve(false);
// Sanitize arguments
if (!(workerScript instanceof WorkerScript)) {
return 0;
}
//'null/undefined' arguments are not allowed
if (typeof scriptname !== "string" || !Array.isArray(args)) {
workerScript.log(`ERROR: runScriptFromScript() failed due to invalid arguments`);
console.error(`runScriptFromScript() failed due to invalid arguments`);
return 0;
}
// Check if the script is already running
let runningScriptObj = server.getRunningScript(scriptname, args);
if (runningScriptObj != null) {
workerScript.log(`${scriptname} is already running on ${server.hostname}`);
return 0;
}
// 'null/undefined' arguments are not allowed
for (let i = 0; i < args.length; ++i) {
if (args[i] == null) {
workerScript.scriptRef.log("ERROR: Cannot execute a script with null/undefined as an argument");
return Promise.resolve(false);
workerScript.log("ERROR: Cannot execute a script with null/undefined as an argument");
return 0;
}
}
//Check if the script exists and if it does run it
// Check if the script exists and if it does run it
for (let i = 0; i < server.scripts.length; ++i) {
if (server.scripts[i].filename == scriptname) {
//Check for admin rights and that there is enough RAM availble to run
var script = server.scripts[i];
var ramUsage = script.ramUsage;
threads = Math.round(Number(threads)); //Convert to number and round
if (threads === 0) { return Promise.resolve(false); }
if (server.scripts[i].filename === scriptname) {
// Check for admin rights and that there is enough RAM availble to run
const script = server.scripts[i];
let ramUsage = script.ramUsage;
threads = Math.round(Number(threads));
if (threads === 0) { return 0; }
ramUsage = ramUsage * threads;
var ramAvailable = server.maxRam - server.ramUsed;
const ramAvailable = server.maxRam - server.ramUsed;
if (server.hasAdminRights == false) {
workerScript.scriptRef.log("Cannot run script " + scriptname + " on " + server.hostname + " because you do not have root access!");
return Promise.resolve(false);
workerScript.log(`Cannot run script ${scriptname} on ${server.hostname} because you do not have root access!`);
return 0;
} else if (ramUsage > ramAvailable){
workerScript.scriptRef.log("Cannot run script " + scriptname + "(t=" + threads + ") on " + server.hostname + " because there is not enough available RAM!");
return Promise.resolve(false);
workerScript.log(`Cannot run script ${scriptname} (t=${threads}) on ${server.hostname} because there is not enough available RAM!`);
return 0;
} else {
//Able to run script
// Able to run script
if (workerScript.disableLogs.ALL == null && workerScript.disableLogs.exec == null && workerScript.disableLogs.run == null && workerScript.disableLogs.spawn == null) {
workerScript.scriptRef.log(`Running script: ${scriptname} on ${server.hostname} with ${threads} threads and args: ${arrayToString(args)}. May take a few seconds to start up...`);
workerScript.log(`Running script: ${scriptname} on ${server.hostname} with ${threads} threads and args: ${arrayToString(args)}.`);
}
let runningScriptObj = new RunningScript(script, args);
runningScriptObj.threads = threads;
@ -649,10 +660,14 @@ export function runScriptFromScript(server, scriptname, args, workerScript, thre
// Push onto runningScripts.
// This has to come after addWorkerScript() because that fn updates RAM usage
server.runScript(runningScriptObj, Player.hacknet_node_money_mult);
return Promise.resolve(true);
// Once the WorkerScript is constructed in addWorkerScript(), the RunningScript
// object should have a PID assigned to it, so we return that
return runningScriptObj.pid;
}
}
}
workerScript.scriptRef.log("Could not find script " + scriptname + " on " + server.hostname);
return Promise.resolve(false);
workerScript.log(`Could not find script ${scriptname} on ${server.hostname}`);
return 0;
}

@ -12,6 +12,7 @@ import { IReturnStatus } from "../types";
import { isScriptFilename } from "../Script/ScriptHelpersTS";
import { createRandomIp } from "../../utils/IPAddress";
import { compareArrays } from "../../utils/helpers/compareArrays";
interface IConstructorParams {
adminRights?: boolean;
@ -113,8 +114,27 @@ export class BaseServer {
return null;
}
// Given the name of the script, returns the corresponding
// script object on the server (if it exists)
/**
* Find an actively running script on this server
* @param scriptName - Filename of script to search for
* @param scriptArgs - Arguments that script is being run with
* @returns RunningScript for the specified active script
* Returns null if no such script can be found
*/
getRunningScript(scriptName: string, scriptArgs: any[]): RunningScript | null {
for (let rs of this.runningScripts) {
if (rs.filename === scriptName && compareArrays(rs.args, scriptArgs)) {
return rs;
}
}
return null;
}
/**
* Given the name of the script, returns the corresponding
* Script object on the server (if it exists)
*/
getScript(scriptName: string): Script | null {
for (let i = 0; i < this.scripts.length; i++) {
if (this.scripts[i].filename === scriptName) {
@ -129,7 +149,6 @@ export class BaseServer {
* Returns boolean indicating whether the given script is running on this server
*/
isRunning(fn: string): boolean {
// Check that the script isnt currently running
for (const runningScriptObj of this.runningScripts) {
if (runningScriptObj.filename === fn) {
return true;
@ -157,7 +176,7 @@ export class BaseServer {
* @returns {IReturnStatus} Return status object indicating whether or not file was deleted
*/
removeFile(fn: string): IReturnStatus {
if (fn.endsWith(".exe")) {
if (fn.endsWith(".exe") || fn.match(/^.+\.exe-\d+(?:\.\d*)?%-INC$/) != null) {
for (let i = 0; i < this.programs.length; ++i) {
if (this.programs[i] === fn) {
this.programs.splice(i, 1);

@ -1150,7 +1150,7 @@ let Terminal = {
killWorkerScript(s.runningScripts[i], s.ip, false);
}
WorkerScriptStartStopEventEmitter.emitEvent();
post("Killing all running scripts. May take up to a few minutes for the scripts to die...");
post("Killing all running scripts");
break;
}
case "ls": {
@ -1573,19 +1573,32 @@ let Terminal = {
return;
}
// Kill by PID
if (typeof commandArray[1] === "number") {
const pid = commandArray[1];
const res = killWorkerScript(pid);
if (res) {
post(`Killing script with PID ${pid}`);
} else {
post(`Failed to kill script with PID ${pid}. No such script exists`);
}
return;
}
const s = Player.getCurrentServer();
const scriptName = Terminal.getFilepath(commandArray[1]);
const args = [];
for (let i = 2; i < commandArray.length; ++i) {
args.push(commandArray[i]);
}
const runningScript = findRunningScript(scriptName, args, s);
const runningScript = s.getRunningScript(scriptName, args);
if (runningScript == null) {
postError("No such script is running. Nothing to kill");
return;
}
killWorkerScript(runningScript, s.ip);
post(`Killing ${scriptName}. May take up to a few seconds for the scripts to die...`);
post(`Killing ${scriptName}`);
} catch(e) {
Terminal.postThrownError(e);
}
@ -2291,7 +2304,6 @@ let Terminal = {
} else {
// Able to run script
post("Running script with " + numThreads + " thread(s) and args: " + arrayToString(args) + ".");
post("May take a few seconds to start up the process...");
var runningScriptObj = new RunningScript(script, args);
runningScriptObj.threads = numThreads;

@ -36,9 +36,9 @@ export function removeTrailingSlash(s: string): string {
* not the entire filepath
*/
export function isValidFilename(filename: string): boolean {
// Allows alphanumerics, hyphens, underscores.
// Allows alphanumerics, hyphens, underscores, and percentage signs
// Must have a file extension
const regex = /^[.a-zA-Z0-9_-]+[.][.a-zA-Z0-9]+$/;
const regex = /^[.a-zA-Z0-9_-]+[.][a-zA-Z0-9]+(?:-\d+(?:\.\d*)?%-INC)?$/;
// match() returns null if no match is found
return filename.match(regex) != null;
@ -49,7 +49,7 @@ export function isValidFilename(filename: string): boolean {
* not an entire path
*/
export function isValidDirectoryName(name: string): boolean {
// Allows alphanumerics, hyphens and underscores.
// Allows alphanumerics, hyphens, underscores, and percentage signs.
// Name can begin with a single period, but otherwise cannot have any
const regex = /^.?[a-zA-Z0-9_-]+$/;

@ -20,7 +20,7 @@ export const TerminalHelpText: string =
"home Connect to home computer<br>" +
"hostname Displays the hostname of the machine<br>" +
"ifconfig Displays the IP address of the machine<br>" +
"kill [script] [args...] Stops the specified script on the current server <br>" +
"kill [script/pid] [args...] Stops the specified script on the current server <br>" +
"killall Stops all running scripts on the current machine<br>" +
"ls [dir] [| grep pattern] Displays all files on the machine<br>" +
"lscpu Displays the number of CPU cores on the machine<br>" +
@ -128,15 +128,16 @@ export const HelpTexts: IMap<string> = {
ifconfig: "ipconfig<br>" +
"Prints the IP address of the current server",
kill: "kill [script name] [args...]<br>" +
"Kill the script specified by the script name and arguments. Each argument must be separated by " +
"a space. Remember that a running script is uniquely identified by " +
"both its name and the arguments that are used to start it. So, if a script was ran with the following arguments:<br><br>" +
"kill [pid]<br>" +
"Kill the script specified by the script name and arguments OR by its PID.<br><br>" +
"If you are killing the script using its filename and arguments, then each " +
"argument must be separated by a space. Remember that a running script is " +
"uniquely identified by both its name and the arguments that are used to start " +
"it. So, if a script was ran with the following arguments:<br><br>" +
"run foo.script 1 sigma-cosmetics<br><br>" +
"Then to kill this script the same arguments would have to be used:<br><br>" +
"kill foo.script 1 sigma-cosmetics<br><br>" +
"Note that after issuing the 'kill' command for a script, it may take a while for the script to actually stop running. " +
"This will happen if the script is in the middle of a command such as grow() or weaken() that takes time to execute. " +
"The script will not be stopped/killed until after that time has elapsed.",
"If you are killing the script using its PID, then the PID argument must be numeric",
killall: "killall<br>" +
"Kills all scripts on the current server. " +
"Note that after the 'kill' command is issued for a script, it may take a while for the script to actually stop running. " +

@ -67,6 +67,10 @@ describe("Terminal Directory Tests", function() {
expect(isValidFilename("_foo.lit")).to.equal(true);
expect(isValidFilename("mult.periods.script")).to.equal(true);
expect(isValidFilename("mult.per-iods.again.script")).to.equal(true);
expect(isValidFilename("BruteSSH.exe-50%-INC")).to.equal(true);
expect(isValidFilename("DeepscanV1.exe-1.01%-INC")).to.equal(true);
expect(isValidFilename("DeepscanV2.exe-1.00%-INC")).to.equal(true);
expect(isValidFilename("AutoLink.exe-1.%-INC")).to.equal(true);
});
it("should return false for invalid filenames", function() {
@ -79,6 +83,10 @@ describe("Terminal Directory Tests", function() {
expect(isValidFilename("foo._script")).to.equal(false);
expect(isValidFilename("foo.hyphened-ext")).to.equal(false);
expect(isValidFilename("")).to.equal(false);
expect(isValidFilename("AutoLink-1.%-INC.exe")).to.equal(false);
expect(isValidFilename("AutoLink.exe-1.%-INC.exe")).to.equal(false);
expect(isValidFilename("foo%.exe")).to.equal(false);
expect(isValidFilename("-1.00%-INC")).to.equal(false);
});
});