8 files changed, 424 insertions(+), 0 deletions(-)

A => .hgignore
A => .vscodeignore
A => LICENSE
A => README.md
A => images/auto-attach.gif
A => package.json
A => src/extension.ts
A => tsconfig.json
A => .hgignore +5 -0
@@ 0,0 1,5 @@ 
+node_modules/
+out/
+.vscode/
+.*.vsix
+package-lock.json

          
A => .vscodeignore +2 -0
@@ 0,0 1,2 @@ 
+.vscode/**/*
+.hgignore

          
A => LICENSE +21 -0
@@ 0,0 1,21 @@ 
+MIT License
+
+Copyright (c) 2019 Mateusz Krawczuk
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

          
A => README.md +41 -0
@@ 0,0 1,41 @@ 
+# Summary
+
+This extension provides support for debugging Pike code in Visual Studio Code. In order to use this extension, you must have a version of Pike that supports debugging and is compiled with the --with-debug option. For best results while debugging, the pike you use should also be compiled with the --without-machine-code option. 
+
+*Tip:* also install the Pike language extension, available from the Extensions Marketplace. The Pike language extension provides some basic syntax highlighting and code folding support. 
+
+## Notes and Caveats
+
+Most of the following discussion refer to limitations in the Pike debugging subsystem, or in a complication involving the ability of an IDE to express what you really want to do to the debugger. If things don't seem to work the way you should, have a look here to see if it's a known limitation.
+
+### Machine Code and Optimization
+
+A killer feature of Pike is its ability to use native machine code in its compiled programs. Machine code runs faster than interpreted code and sometimes runs many times faster. While this is a boon at runtime, it can wreak havoc when trying to use a debugger. That's because machine code generation coupled with the advanced optimization that Pike performs when compiling code can result in a non-linear execution path compared to the original source. When machine code is enabled, the interpreter often bundles multiple lines of code into one operation, sometimes during program initialization such that not every line will be executed when stepping, and some may never provide an opportunity for the debugger to break execution. It's still possible to debug when machine code is being used, but the flow of execution and granularity may make it more difficult to examine what's happening. Having a copy of pike installed that has machine code generation disabled (possibly in parallel to one that's enabled) allows for a more reasonable debugging session.
+
+### Breakpoints
+
+The debugger is able to create breakpoints on pretty much any line of Pike code. There are, however, some situations that may prove confusing, so we'll discuss them here. Basically, IDEs such as Visual Studio Code like to define breakpoints in terms of a concrete file on disk and a line within that file. The Pike runtime, however, deals strictly interms of the "program". A given file is always a program, but it can contain nested programs and so forth. Pike can also create programs from arbitrary strings unassociated with a file at all. For the bulk of code most people will write, this doesn't cause much confusion and the IDE and Pike debugger get along without too much trouble. However, there are a few cases that may require special handling or cause confusion:
+
+1. Use of the preprocessor
+
+The preprocessor is a double edged sword: It allows a lot of problems to be solved that would otherwise require a lot of repetition or boilerplate, but it can also cause some real headaches. The include directive can be used to insert code one (or more) times into the same file. When compiling a file with this sort of scenario, the ability of Pike to create a breakboint becomes a lot more complicated: if the line you wish to break on is in a file that gets included in another, you have to specify the breakpoint interms of the containing pike file because that's the compilation unit and usually the source of the program.
+
+Internally, Pike provides a provision for this: to create a breakpoint on a particular point in a program corresponding to a location in a file that might not be the file the program is actually defined in (like in the case of a file included in another). However, IDEs tend not to have a way to express this multi-layeredness so it may not be possible for you to do this directly in the IDE (though you could add some temporary code in the code you'll be debugging to create those particular "nested" breakpoints yourself. 
+
+Conversely, putting a breakpoint on a file that gets included into another (or possibly many) file is something that an IDE will commonly permit. Unfortunately, it probably won't cause any breaks because that include file isn't usually the the thing that Pike is compiling into a program. This particular scenario could be solved, but it would likely cause an unacceptable performance hit when debugging. If you really needed to be able to do this, you could probably create a breakpoint for each program that includes the file, in the same manner as the previous case.
+
+Macros may cause confusion as well. Breakpoints on macros included from another file carry all of the caveats as the previous situation. It should be possible to create a breakpoint on a macro defined elsewhere in the same file because cpp() includes the source source location when it derefrences macros. The Pike debugger is also clever enough to scan an entire program for all possible occurances of the line. This means that a breakpoint on a single line might effectively cause halts across a wide portion of a program.
+
+2. Programs compiled outside of the master
+
+Programs that make use of compile_file() and compile_string() may also cause unexpected behavior because these functions do not register the programs they create with the master. When creating a breakpoint based on a file path and line number, the debugger asks the Pike master program for the program corresponding to the file. If the file hasn't been loaded yet, the breakpoint is created in a "pending" state and will be enabled once the master loads it into its program cache. 
+
+A number of Pike programs sidestep the master and perform program compilation directly themselves. Roxen and the Fins web framework both do this for various reasons such as allowing modules to be reloaded from disk without having to restart the whole process. Their "deceit" is even greater because they often broadly refer to a connection between a module and its source file. However, because the programs generated in this way aren't registered with the master so breakpoints made against such files will never transition from "pending". You could programmatically create breakpoints in your code as provision is made for creating breakpoints from the file path, line number /and/ program. Recompiling a program from a file will result in a new program however, and your breakpoints will not have been registered against the new program. Probably a better approach would be for these types of program to integrate with the debugger's breakpoint resolution system. Doing this would permit breakpoints to resolve in the first place, and could also theoretically permit breakpoints to be updated when files and programs associated with them are updated.
+ 
+### Viewing and changing variables
+
+Variables in both the local (function) and global (object) scope can be viewed and changed. Variables containing multiple elements (that is, reference types such as arrays, mappings and objects) are displayed as an expanding list showing keys and values. The debugger is able to "burrow" down multiple levels as they are unfolded. Values that are "unitary" (such as numbers and strings) are displayed in the manner of the "%O" operator in Pike's sprintf() method. So, if you see a value that looks like this: "123", you know that is a string rather than the int 123. Similarly, when modifying values, they should be provided in the same manner. Setting the value of a variable to 123 makes the value an int. If you try to set a value to a string, don't forget to include the quotation marks, otherwise you'll get failures or unexpected results if the value happens to be a number. 
+
+Special care must be used when setting values that are reference types. This is because multiple variables (across multiple objects perhaps) may hold a reference to the value. If you replace a variable that contains a mapping with a mapping, you are effectively creating a new thing that may look like the value that was previously there, but is unique and no longer a shared value. Depending on the situation, this may not be a problem, but it can be the source of much confusion, both for you and your programs.  
+
+

          
A => images/auto-attach.gif +0 -0

        
A => package.json +101 -0
@@ 0,0 1,101 @@ 
+{
+  "name": "pike-debugger",
+  "displayName": "Pike Debugger",
+  "version": "0.5.0",
+  "publisher": "hww3",
+  "description": "Debugging support for the Pike programming language",
+  "author": {
+    "name": "hww3",
+    "email": "william@welliver.org"
+  },
+  "license": "MIT",
+  "keywords": [
+    "pike",
+    "debugger",
+    "debug",
+    "adapter"
+  ],
+  "categories": [
+    "Debuggers"
+  ],
+  "private": true,
+  "repository": {
+    "type": "hg",
+    "url": "https://hg.sr.ht/~hww3/vscode-debugger-pike"
+  },
+  "bugs": {},
+  "scripts": {
+    "vscode:prepublish": "npm run compile",
+    "compile": "tsc -p ./",
+    "watch": "tsc -watch -p ./",
+    "test": "npm run compile && node ./node_modules/vscode/bin/test",
+    "package": "vsce package",
+    "publish": "vsce publish"
+  },
+  "main": "./out/extension",
+  "engines": {
+    "vscode": "^1.50.0"
+  },
+  "devDependencies": {
+    "@types/mocha": "^5.2.5",
+    "@types/node": "^8.9.3",
+    "@types/vscode": "^1.1.37",
+    "typescript": "^3.1.6",
+    "vsce": "1.54.0",
+    "vscode-test": "^1.5.1"
+  },
+  "activationEvents": [
+    "*"
+  ],
+  "dependencies": {
+    "vscode-debugadapter": "1.33.0",
+    "vscode-nls": "4.0.0"
+  },
+  "contributes": {
+    "breakpoints": [
+      {
+        "language": "pike"
+      }
+    ],
+    "debuggers": [
+      {
+        "type": "pike",
+        "runtime": "pike",
+        "languages": [
+          "pike"
+        ],
+        "label": "Pike Debugger",
+        "initialConfigurations": [
+          {
+            "type": "pike",
+            "request": "attach",
+            "name": "attach",
+            "debugServer": 4711
+          }
+        ],
+        "configurationSnippets": [
+          {
+            "label": "Pike: Attach",
+            "description": "Attach to the launched adapter listening on port `debugServer`.",
+            "body": {
+              "type": "pike",
+              "request": "attach",
+              "name": "attach",
+              "debugServer": "4711"
+            }
+          }
+        ]
+      }
+    ],
+    "configuration": {
+      "title": "Pike Debugger",
+      "properties": {
+        "pike.debugger.autoattach": {
+          "type": "boolean",
+          "default": false,
+          "description": "Auto-attach a debugger when starting pike with debugger options through the VS Code terminal."
+        }
+      }
+    }
+  }
+}

          
A => src/extension.ts +238 -0
@@ 0,0 1,238 @@ 
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Copyright (c) William Welliver. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+"use strict";
+
+import * as vscode from "vscode";
+import * as cp from "child_process";
+import * as nls from "vscode-nls";
+
+const localize = nls.loadMessageBundle();
+let poller;
+
+export function activate(context: vscode.ExtensionContext) {
+  context.subscriptions.push(
+    vscode.workspace.onDidChangeConfiguration((e) => {
+      if (e.affectsConfiguration("pike.debugger.autoattach")) {
+        let val: boolean = vscode.workspace
+          .getConfiguration()
+          .get("pike.debugger.autoattach");
+        if (val) startAutoAttach();
+        else stopAutoAttach();
+      }
+    })
+  );
+
+  let val: boolean = vscode.workspace
+    .getConfiguration()
+    .get("pike.debugger.autoattach");
+  if (val) startAutoAttach();
+  else stopAutoAttach();
+}
+
+export function deactivate() {
+  stopAutoAttach();
+}
+
+//---- Pike debugger Auto Attach
+
+const POLL_INTERVAL = 1500;
+
+const DEBUG_PORT_PATTERN = /\s--(debugger)-port=(\d+)/;
+const DEBUG_FLAGS_PATTERN = /\s--(debugger)?/;
+const ochan = vscode.window.createOutputChannel("Pike Auto-attach");
+let isStarted = false;
+
+function stopAutoAttach() {
+  ochan.appendLine("stopping autoattach");
+  if (isStarted) clearInterval(poller);
+  poller = null;
+  isStarted = false;
+}
+
+function startAutoAttach() {
+  ochan.appendLine("starting autoattach");
+  if (isStarted) return;
+
+  isStarted = true;
+  const rootPid = parseInt(process.env["VSCODE_PID"]);
+
+  const defaultLaunchConfig = {
+    type: "pike",
+    name: "Auto-Attach-Default",
+    request: "attach",
+    debugServer: 4711,
+    timeout: 5000,
+  };
+
+  pollChildProcesses(rootPid, (pid, cmd) => {
+    if (cmd.indexOf("pike") >= 0) {
+      attachChildProcess(pid, cmd, defaultLaunchConfig);
+    }
+  });
+}
+
+/**
+ * Poll for all subprocesses of given root process.
+ */
+function pollChildProcesses(
+  rootPid: number,
+  processFoundCallback: (pid: number, cmd: string) => void
+) {
+  poller = setInterval(() => {
+    findChildProcesses(rootPid, processFoundCallback);
+  }, POLL_INTERVAL);
+}
+
+/**
+ * Attach debugger to given process.
+ */
+function attachChildProcess(
+  pid: number,
+  cmd: string,
+  baseConfig: vscode.DebugConfiguration
+): boolean {
+  const config: vscode.DebugConfiguration = {
+    type: "pike",
+    request: "attach",
+    debugServer: 4711,
+    timeout: 5000,
+    name: localize("childProcessWithPid", "Process {0}", pid),
+  };
+
+  // selectively copy attributes
+  if (baseConfig.timeout) {
+    config.timeout = baseConfig.timeout;
+  }
+  if (baseConfig.sourceMaps) {
+    config.sourceMaps = baseConfig.sourceMaps;
+  }
+  if (baseConfig.outFiles) {
+    config.outFiles = baseConfig.outFiles;
+  }
+  if (baseConfig.sourceMapPathOverrides) {
+    config.sourceMapPathOverrides = baseConfig.sourceMapPathOverrides;
+  }
+  if (baseConfig.smartStep) {
+    config.smartStep = baseConfig.smartStep;
+  }
+  if (baseConfig.skipFiles) {
+    config.skipFiles = baseConfig.skipFiles;
+  }
+  if (baseConfig.showAsyncStacks) {
+    config.sourceMaps = baseConfig.showAsyncStacks;
+  }
+  if (baseConfig.trace) {
+    config.trace = baseConfig.trace;
+  }
+
+  // attach via port
+
+  // a debugger-port=1234 overrides the port
+  const matches = DEBUG_PORT_PATTERN.exec(cmd);
+  if (matches && matches.length === 3) {
+    // override port
+    config.debugServer = parseInt(matches[2]);
+  }
+
+  // check to see that the debugger port is open and listening before we attempt to attach.
+  const CMD =
+    "lsof -t -a -n -p " + pid + " -sTCP:LISTEN -iTCP:" + config.debugServer;
+  const CMD_PAT = /^\s*([0-9]+)\s+([0-9]+)\s+(.+)$/;
+
+  cp.exec(CMD, { maxBuffer: 1000 * 1024 }, (err, stdout, stderr) => {
+    if (!err && !stderr) {
+      const lines = stdout.toString().split("\n");
+      for (const line of lines) {
+        if (line.includes("" + pid)) {
+          ochan.appendLine(`attach: ${config.debugServer}`);
+          vscode.debug.startDebugging(undefined, config).then((success) => {
+            if (success) ochan.appendLine("attach succeeded");
+            else ochan.appendLine("attach failed");
+          });
+          return true;
+        }
+      }
+    }
+  });
+  return false;
+}
+
+/**
+ * Find all subprocesses of the given root process
+ */
+function findChildProcesses(
+  rootPid: number,
+  processFoundCallback: (pid: number, cmd: string) => void
+) {
+  const set = new Set<number>();
+  if (!isNaN(rootPid) && rootPid > 0) {
+    set.add(rootPid);
+  }
+
+  function oneProcess(pid: number, ppid: number, cmd: string) {
+    if (set.size === 0) {
+      // try to find the root process
+      const matches = DEBUG_PORT_PATTERN.exec(cmd);
+      if (matches && matches.length >= 3) {
+        // since this is a child we add the parent id as the root id
+        set.add(ppid);
+      }
+    }
+
+    if (set.has(ppid)) {
+      set.add(pid);
+      const matches = DEBUG_PORT_PATTERN.exec(cmd);
+      const matches2 = DEBUG_FLAGS_PATTERN.exec(cmd);
+      if (
+        (matches && matches.length >= 3) ||
+        (matches2 && matches2.length >= 5)
+      ) {
+        processFoundCallback(pid, cmd);
+      }
+    }
+  }
+
+  if (process.platform === "win32") {
+    const CMD = "wmic process get CommandLine,ParentProcessId,ProcessId";
+    const CMD_PAT = /^(.+)\s+([0-9]+)\s+([0-9]+)$/;
+
+    cp.exec(CMD, { maxBuffer: 1000 * 1024 }, (err, stdout, stderr) => {
+      if (!err && !stderr) {
+        const lines = stdout.split("\r\n");
+        for (let line of lines) {
+          let matches = CMD_PAT.exec(line.trim());
+          if (matches && matches.length === 4) {
+            oneProcess(
+              parseInt(matches[3]),
+              parseInt(matches[2]),
+              matches[1].trim()
+            );
+          }
+        }
+      }
+    });
+  } else {
+    // OS X & Linux
+
+    // ideally we would be able to skip the 'a' option to save time, but
+    // then we wouldn't get any processes started under sudo for example. :/
+    const CMD = "ps -ax -o pid=,ppid=,command=";
+    const CMD_PAT = /^\s*([0-9]+)\s+([0-9]+)\s+(.+)$/;
+
+    cp.exec(CMD, { maxBuffer: 1000 * 1024 }, (err, stdout, stderr) => {
+      if (!err && !stderr) {
+        const lines = stdout.toString().split("\n");
+        for (const line of lines) {
+          let matches = CMD_PAT.exec(line.trim());
+          if (matches && matches.length === 4) {
+            oneProcess(parseInt(matches[1]), parseInt(matches[2]), matches[3]);
+          }
+        }
+      }
+    });
+  }
+}

          
A => tsconfig.json +16 -0
@@ 0,0 1,16 @@ 
+{
+    "compilerOptions": {
+        "module": "commonjs",
+        "target": "es6",
+        "outDir": "out",
+        "lib": [
+            "es6"
+        ],
+        "sourceMap": true,
+        "rootDir": "src"
+    },
+    "exclude": [
+        "node_modules",
+        ".vscode-test"
+    ]
+}
  No newline at end of file