├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── License.txt ├── assets ├── dumpvar-context-menu.png ├── watch.png └── whatisit.png ├── buildPlugins ├── buildPluginX │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── xbuild │ │ └── XBuild.java └── settings.gradle.kts ├── github-release ├── .gitignore ├── ReleaseManager.ts ├── package-lock.json ├── package.json ├── publish.bat └── tsconfig.json ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── luceedebug ├── build.gradle.kts ├── extern │ ├── 5.3.9.158-SNAPSHOT.jar │ └── lucee-5.3.9.158-SNAPSHOT.jar └── src │ ├── main │ └── java │ │ └── luceedebug │ │ ├── Agent.java │ │ ├── Config.java │ │ ├── DapServer.java │ │ ├── Either.java │ │ ├── GlobalIDebugManagerHolder.java │ │ ├── IBreakpoint.java │ │ ├── ICfValueDebuggerBridge.java │ │ ├── IDebugEntity.java │ │ ├── IDebugFrame.java │ │ ├── IDebugManager.java │ │ ├── ILuceeVm.java │ │ ├── IPathTransform.java │ │ ├── IdentityPathTransform.java │ │ ├── LuceeTransformer.java │ │ ├── PrefixPathTransform.java │ │ ├── coreinject │ │ ├── Breakpoint.java │ │ ├── CfValueDebuggerBridge.java │ │ ├── ClosureScopeLocalScopeAccessorShim.java │ │ ├── ComponentScopeMarkerTraitShim.java │ │ ├── DebugEntity.java │ │ ├── DebugManager.dot │ │ ├── DebugManager.java │ │ ├── ExprEvaluator.java │ │ ├── Iife.java │ │ ├── KlassMap.java │ │ ├── LuceeVm.java │ │ ├── UnsafeUtils.java │ │ ├── Utils.java │ │ ├── ValTracker.java │ │ └── frame │ │ │ ├── DebugFrame.java │ │ │ ├── DummyFrame.java │ │ │ └── Frame.java │ │ ├── generated │ │ └── .gitkeep │ │ ├── instrumenter │ │ ├── CfmOrCfc.java │ │ ├── ClosureScope.java │ │ ├── ComponentImpl.java │ │ └── PageContextImpl.java │ │ └── strong │ │ ├── CanonicalServerAbsPath.java │ │ ├── DapBreakpointID.java │ │ ├── JdwpThreadID.java │ │ ├── RawIdePath.java │ │ └── StrongT.java │ └── test │ └── java │ └── luceedebug │ ├── EvaluatesAnExpression.java │ ├── HitsABreakpointAndRetrievesVariableInfo.java │ ├── SteppingThroughDefaultArgs.java │ ├── SteppingWorksAsExpectedOnSinglelineStatementWithManySubexpressions.java │ ├── StepsToFinallyAndThenCatchSkippingPastUnwoundLines.java │ └── testutils │ ├── DapUtils.java │ ├── DockerUtils.java │ ├── LuceeUtils.java │ ├── TestParams.java │ └── Utils.java ├── readme.md ├── settings.gradle.kts ├── test ├── .vscode │ └── launch.json ├── docker │ ├── 5.3.10.120 │ │ ├── Dockerfile │ │ └── luceedebug.yml │ ├── 6.1.0.243 │ │ └── Dockerfile │ ├── app1 │ │ ├── a.cfm │ │ └── heartbeat.cfm │ ├── step_to_catch_block │ │ ├── a.cfm │ │ └── heartbeat.cfm │ ├── stepping_through_default_args │ │ ├── a.cfm │ │ └── heartbeat.cfm │ └── stepping_works_as_expected_on_singleline_statement_with_many_subexpressions │ │ ├── a.cfm │ │ └── heartbeat.cfm └── scratch │ └── .gitkeep └── vscode-client ├── .gitignore ├── .vscode └── launch.json ├── README.md ├── package-lock.json ├── package.json ├── sampleWorkspace ├── .vscode │ └── launch.json └── index.cfm ├── src └── extension.ts └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build LuceeDebug Agent 2 | 3 | on: [ push, pull_request, workflow_dispatch ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | cache: 'npm' 13 | cache-dependency-path: vscode-client/package-lock.json 14 | - name: Install vsce 15 | run: npm i -g @vscode/vsce 16 | - name: Set up JDK 11 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '11' 20 | distribution: 'adopt' 21 | - name: Set up JDK 21 22 | uses: actions/setup-java@v4 23 | with: 24 | java-version: '21' 25 | distribution: 'adopt' 26 | - name: Setup Gradle and Build LuceeDebug Java Agent 27 | uses: gradle/gradle-build-action@v2 28 | with: 29 | arguments: shadowjar 30 | - name: Build LuceeDebug VS Code Extension 31 | run: | 32 | cd vscode-client 33 | npm install 34 | npm run build-dev-linux 35 | vsce package 36 | - name: Upload Artifact 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: luceedebug 40 | path: | 41 | luceedebug/vscode-client/*.vsix 42 | luceedebug/build/libs/*.jar 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | bin 4 | 5 | # Ignore Gradle build output directory 6 | build 7 | 8 | .idea 9 | .metals 10 | .bloop 11 | generated/ 12 | test/scratch -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "java", 9 | "name": "attach", 10 | "request": "attach", 11 | "hostName": "localhost", 12 | "port": 9999 13 | }, 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic", 3 | "java.compile.nullAnalysis.mode": "automatic" 4 | } -------------------------------------------------------------------------------- /assets/dumpvar-context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwareCobbler/luceedebug/2de98fdaad6f3e2b2a2c5a56a2e169841a470548/assets/dumpvar-context-menu.png -------------------------------------------------------------------------------- /assets/watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwareCobbler/luceedebug/2de98fdaad6f3e2b2a2c5a56a2e169841a470548/assets/watch.png -------------------------------------------------------------------------------- /assets/whatisit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwareCobbler/luceedebug/2de98fdaad6f3e2b2a2c5a56a2e169841a470548/assets/whatisit.png -------------------------------------------------------------------------------- /buildPlugins/buildPluginX/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | id("java-gradle-plugin") 4 | } 5 | 6 | dependencies { 7 | compileOnly(gradleApi()) 8 | } 9 | 10 | gradlePlugin { 11 | plugins.create("buildPluginX") { 12 | id = name 13 | implementationClass = "xbuild.XBuild" 14 | } 15 | } 16 | 17 | java { 18 | sourceCompatibility = JavaVersion.VERSION_17 19 | } 20 | -------------------------------------------------------------------------------- /buildPlugins/buildPluginX/src/main/java/xbuild/XBuild.java: -------------------------------------------------------------------------------- 1 | package xbuild; 2 | 3 | import org.gradle.api.Project; 4 | import org.gradle.api.Plugin; 5 | import org.gradle.api.provider.Property; 6 | import org.gradle.api.tasks.Input; 7 | import org.gradle.api.tasks.TaskAction; 8 | import java.io.File; 9 | import java.io.FileOutputStream; 10 | import java.nio.file.Files; 11 | import java.util.Arrays; 12 | 13 | import org.gradle.api.DefaultTask; 14 | 15 | public class XBuild implements Plugin { 16 | @Override 17 | public void apply(Project project) {} 18 | 19 | /** 20 | * Usage is like: 21 | * 22 | * tasks.register("generateJavaConstantsFile") { 23 | * version = "1.2.3" 24 | * className = "the.fully.qualified.class.to.write.the.file.To" 25 | * } 26 | */ 27 | public static abstract class GenerateConstants extends DefaultTask { 28 | @Input 29 | public abstract Property getVersion(); 30 | 31 | @Input 32 | public abstract Property getClassName(); 33 | 34 | @TaskAction 35 | public void go() { 36 | String[] classNameComponents = getClassName().get().split("\\."); 37 | if (classNameComponents.length == 0) { 38 | throw new RuntimeException("Invalid value for 'className' -- '" + getClassName().get() + "'"); 39 | } 40 | 41 | String packageName = Arrays.stream(classNameComponents) 42 | .limit(classNameComponents.length - 1) 43 | .reduce("", (in, accum) -> in.equals("") ? accum : (in + "." + accum)); 44 | String className = classNameComponents[classNameComponents.length - 1]; 45 | 46 | File path = getProject() 47 | .getLayout() 48 | .getProjectDirectory() 49 | .file("src/main/java/" + packageName.replaceAll("\\.", "/") + "/" + className + ".java") 50 | .getAsFile(); 51 | 52 | try { 53 | Files.createDirectories(path.toPath().getParent()); 54 | try (FileOutputStream out = new FileOutputStream(path)) { 55 | out.write(GenerateConstants.generateConstantsFile( 56 | packageName, 57 | className, 58 | getVersion().get() 59 | ).getBytes("utf-8")); 60 | } 61 | } 62 | catch (Throwable e) { 63 | throw new RuntimeException(e); 64 | } 65 | } 66 | 67 | private static String generateConstantsFile( 68 | String packageName, 69 | String className, 70 | String version 71 | ) { 72 | return "" 73 | + "// THIS FILE WAS AUTOGENERATED BY A GRADLE BUILD STEP. DO NOT EDIT.\n" 74 | + "package " + packageName + ";\n" 75 | + "public final class " + className + " {\n" 76 | + " public static final String version = \"" + version + "\";\n" 77 | + "}\n"; 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /buildPlugins/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "luceedebug-buildstuff" 2 | 3 | include("buildPluginX") 4 | -------------------------------------------------------------------------------- /github-release/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /github-release/ReleaseManager.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs" 2 | import * as path from "node:path" 3 | import * as process from "node:process" 4 | import * as child_process from "node:child_process" 5 | import { Octokit } from "@octokit/core" 6 | 7 | async function makeRelease(authToken: string, tagName: string) { 8 | const octokit = new Octokit({ 9 | auth: authToken 10 | }) 11 | 12 | return await octokit.request('POST /repos/{owner}/{repo}/releases', { 13 | owner: 'softwareCobbler', 14 | repo: 'luceedebug', 15 | tag_name: tagName, 16 | name: tagName, 17 | body: `build for ${tagName}`, 18 | draft: false, 19 | prerelease: false, 20 | generate_release_notes: false, 21 | headers: { 22 | 'X-GitHub-Api-Version': '2022-11-28' 23 | } 24 | }) 25 | } 26 | 27 | async function addFileToRelease(authToken: string, releaseID: number, name: string, bytes: ArrayBuffer) { 28 | const result = await fetch(`https://uploads.github.com/repos/softwareCobbler/luceedebug/releases/${releaseID}/assets?name=${name}`, { 29 | method: "POST", 30 | headers: { 31 | Accept: `application/vnd.github+json`, 32 | Authorization: `Bearer ${authToken}`, 33 | "Content-Type": `application/octet-stream` 34 | }, 35 | body: bytes 36 | }) 37 | 38 | console.log(result.status) 39 | console.log(result.statusText) 40 | console.log(await result.json()) 41 | } 42 | 43 | async function inspectReleases() { 44 | const octokit = new Octokit({ 45 | auth: process.env[EnvVars.authToken] 46 | }) 47 | 48 | // const releases = await octokit.request('GET /repos/{owner}/{repo}/releases', { 49 | // owner: 'softwareCobbler', 50 | // repo: 'luceedebug', 51 | // headers: { 52 | // 'X-GitHub-Api-Version': '2022-11-28' 53 | // } 54 | // }); 55 | 56 | const releases = await octokit.request('GET /repos/{owner}/{repo}/releases/latest', { 57 | owner: 'softwareCobbler', 58 | repo: 'luceedebug', 59 | headers: { 60 | 'X-GitHub-Api-Version': '2022-11-28' 61 | } 62 | }) 63 | 64 | console.log(releases) 65 | } 66 | 67 | enum EnvVars { 68 | authToken = `LUCEEDEBUG_RELEASE_GITHUB_AUTH_TOKEN`, 69 | tag = `LUCEEDEBUG_RELEASE_TAG` 70 | } 71 | 72 | async function doit() { 73 | const authToken = process.env[EnvVars.authToken]; 74 | const tag = process.env[EnvVars.tag] 75 | 76 | if (!authToken || !tag) { 77 | const required = [] 78 | !authToken ? required.push(EnvVars.authToken) : 0; 79 | !tag ? required.push(EnvVars.tag) : 0; 80 | throw Error(`missing required env vars ${required.join(",")}`); 81 | } 82 | 83 | // sanity check that the target tag from env is the currently checked out version 84 | const tagCommit = child_process.execSync(`cd .. && git rev-list -n 1 ${tag}`).toString().trim() 85 | const currentCommit = child_process.execSync(`cd .. && git rev-list -n 1 HEAD`).toString().trim() 86 | 87 | if (tagCommit !== currentCommit) { 88 | console.error("tagCommit=" + tagCommit) 89 | console.error("currentCommit=" + currentCommit) 90 | console.error("target tag (from env)=" + tag) 91 | throw Error("target commit based on supplied tag is not current HEAD?") 92 | } 93 | 94 | const libname = child_process.execSync("cd .. && gradlew printCurrentLibName --quiet").toString().replaceAll(/[^-a-z0-9-.]/ig, "") 95 | 96 | // run build 97 | child_process.execSync("cd .. && gradlew clean shadowJar", {stdio: "inherit"}) 98 | 99 | const bytes = fs.readFileSync(path.resolve(`../luceedebug/build/libs/${libname}`)) 100 | 101 | // make release and get associated ID, then add file 102 | const {data: {id}} = await makeRelease(authToken, tag) 103 | // const id = <<>> 104 | 105 | await addFileToRelease(authToken, id, libname, bytes); 106 | } 107 | 108 | // await doit(); 109 | await inspectReleases(); 110 | -------------------------------------------------------------------------------- /github-release/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-release", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@octokit/core": "^5.0.1" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^20.9.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /github-release/publish.bat: -------------------------------------------------------------------------------- 1 | deno run --allow-net --allow-env --allow-run --allow-read ReleaseManager.ts 2 | -------------------------------------------------------------------------------- /github-release/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ESNext", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwareCobbler/luceedebug/2de98fdaad6f3e2b2a2c5a56a2e169841a470548/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /luceedebug/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import xbuild.XBuild.GenerateConstants 2 | 3 | plugins { 4 | java 5 | id("com.gradleup.shadow") version "8.3.0" 6 | id("org.owasp.dependencycheck") version "8.4.0" apply false 7 | id("buildPluginX") 8 | } 9 | 10 | allprojects { 11 | apply(plugin = "org.owasp.dependencycheck") 12 | } 13 | 14 | configure { 15 | format = org.owasp.dependencycheck.reporting.ReportGenerator.Format.ALL.toString() 16 | } 17 | 18 | repositories { 19 | // Use Maven Central for resolving dependencies. 20 | mavenCentral() 21 | } 22 | 23 | dependencies { 24 | // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api 25 | testImplementation("org.junit.jupiter:junit-jupiter:5.11.0") 26 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 27 | // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params 28 | testImplementation("org.junit.jupiter:junit-jupiter-params:5.11.0") 29 | // https://mvnrepository.com/artifact/com.github.docker-java/docker-java-core 30 | testImplementation("com.github.docker-java:docker-java-core:3.3.0") 31 | // https://mvnrepository.com/artifact/com.github.docker-java/docker-java-transport-httpclient5 32 | testImplementation("com.github.docker-java:docker-java-transport-httpclient5:3.3.0") 33 | // https://mvnrepository.com/artifact/com.google.http-client/google-http-client 34 | testImplementation("com.google.http-client:google-http-client:1.43.1") 35 | 36 | // https://mvnrepository.com/artifact/com.google.guava/guava 37 | implementation("com.google.guava:guava:32.1.2-jre") 38 | 39 | implementation("org.ow2.asm:asm:9.7.1") 40 | implementation("org.ow2.asm:asm-util:9.7.1") 41 | implementation("org.ow2.asm:asm-commons:9.7.1") 42 | 43 | // https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api 44 | compileOnly("javax.servlet.jsp:javax.servlet.jsp-api:2.3.3") 45 | // https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api 46 | compileOnly("javax.servlet:javax.servlet-api:3.1.0") // same as lucee deps 47 | 48 | 49 | compileOnly(files("extern/lucee-5.3.9.158-SNAPSHOT.jar")) 50 | compileOnly(files("extern/5.3.9.158-SNAPSHOT.jar")) 51 | 52 | // https://mvnrepository.com/artifact/org.eclipse.lsp4j/org.eclipse.lsp4j.debug 53 | implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.debug:0.23.1") 54 | 55 | } 56 | 57 | java { 58 | sourceCompatibility = JavaVersion.VERSION_11 59 | } 60 | 61 | tasks.compileJava { 62 | dependsOn("generateJavaConstantsFile") 63 | options.compilerArgs.add("-Xlint:unchecked") 64 | options.compilerArgs.add("-Xlint:deprecation") 65 | } 66 | 67 | tasks.test { 68 | dependsOn("shadowJar") 69 | 70 | useJUnitPlatform() 71 | 72 | // maxHeapSize = "1G" // infinite, don't care 73 | 74 | maxParallelForks = (Runtime.getRuntime().availableProcessors()).coerceAtLeast(1).also { 75 | println("Setting maxParallelForks to $it") 76 | } 77 | 78 | testLogging { 79 | events("passed") 80 | events("failed") 81 | showStandardStreams = true 82 | } 83 | } 84 | 85 | tasks.jar { 86 | manifest { 87 | attributes( 88 | mapOf( 89 | "Premain-Class" to "luceedebug.Agent", 90 | "Can-Redefine-Classes" to "true", 91 | "Bundle-SymbolicName" to "luceedebug-osgi", 92 | "Bundle-Version" to "2.0.1.1", 93 | "Export-Package" to "luceedebug.*" 94 | ) 95 | ) 96 | } 97 | } 98 | 99 | val luceedebugVersion = "2.0.15" 100 | val libfile = "luceedebug-" + luceedebugVersion + ".jar" 101 | 102 | // TODO: this should, but does not currently, participate in the `clean` task, so the generated file sticks around after invoking `clean`. 103 | tasks.register("generateJavaConstantsFile") { 104 | version = luceedebugVersion 105 | // n.b. this ends up in src/luceedebug/generated, rather than build/... 106 | // for the sake of ide autocomplete 107 | className = "luceedebug.generated.Constants" 108 | } 109 | 110 | tasks.register("printCurrentLibName") { 111 | println(libfile) 112 | } 113 | 114 | tasks.shadowJar { 115 | configurations = listOf(project.configurations.runtimeClasspath.get()) 116 | setEnableRelocation(true) 117 | relocationPrefix = "luceedebug_shadow" 118 | archiveFileName.set(libfile) 119 | } 120 | -------------------------------------------------------------------------------- /luceedebug/extern/5.3.9.158-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwareCobbler/luceedebug/2de98fdaad6f3e2b2a2c5a56a2e169841a470548/luceedebug/extern/5.3.9.158-SNAPSHOT.jar -------------------------------------------------------------------------------- /luceedebug/extern/lucee-5.3.9.158-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwareCobbler/luceedebug/2de98fdaad6f3e2b2a2c5a56a2e169841a470548/luceedebug/extern/lucee-5.3.9.158-SNAPSHOT.jar -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/Config.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import java.io.File; 4 | 5 | public class Config { 6 | private final boolean fsIsCaseSensitive_; 7 | // we probably never want to step into this (the a=b in `function foo(a=b) { ... }` ) 8 | // but for now it's configurable 9 | private boolean stepIntoUdfDefaultValueInitFrames_ = false; 10 | 11 | Config(boolean fsIsCaseSensitive) { 12 | this.fsIsCaseSensitive_ = fsIsCaseSensitive; 13 | } 14 | 15 | public boolean getStepIntoUdfDefaultValueInitFrames() { 16 | return this.stepIntoUdfDefaultValueInitFrames_; 17 | } 18 | public void setStepIntoUdfDefaultValueInitFrames(boolean v) { 19 | this.stepIntoUdfDefaultValueInitFrames_ = v; 20 | } 21 | 22 | private static String invertCase(String path) { 23 | int offset = 0; 24 | int strLen = path.length(); 25 | final var builder = new StringBuilder(); 26 | while (offset < strLen) { 27 | int c = path.codePointAt(offset); 28 | if (Character.isUpperCase(c)) { 29 | builder.append(Character.toString(Character.toLowerCase(c))); 30 | } 31 | else if (Character.isLowerCase(c)) { 32 | builder.append(Character.toString(Character.toUpperCase(c))); 33 | } 34 | else { 35 | builder.append(Character.toString(c)); 36 | } 37 | offset += Character.charCount(c); 38 | } 39 | return builder.toString(); 40 | } 41 | 42 | public static boolean checkIfFileSystemIsCaseSensitive(String absPath) { 43 | if (!(new File(absPath)).exists()) { 44 | throw new IllegalArgumentException("File '" + absPath + "' doesn't exist, so it cannot be used to check for file system case sensitivity."); 45 | } 46 | return !(new File(invertCase(absPath))).exists(); 47 | } 48 | 49 | public boolean getFsIsCaseSensitive() { 50 | return fsIsCaseSensitive_; 51 | } 52 | 53 | public static String canonicalizeFileName(String s) { 54 | return s.replaceAll("[\\\\/]+", "/").toLowerCase(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/Either.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import java.util.Optional; 4 | import java.util.function.Function; 5 | 6 | public class Either { 7 | private enum Which { left, right }; 8 | 9 | private final Which which; 10 | public final L left; 11 | public final R right; 12 | 13 | private Either(Which which, L L, R R) { 14 | if (which == Which.left) { 15 | this.which = which; 16 | this.left = L; 17 | this.right = null; 18 | } 19 | else if (which == Which.right) { 20 | this.which = which; 21 | this.left = null; 22 | this.right = R; 23 | } 24 | else { 25 | assert false : "unreachable"; 26 | throw new RuntimeException("unreachable"); 27 | } 28 | } 29 | 30 | public static Either Left(L v) { 31 | return new Either<>(Which.left, v, null); 32 | } 33 | 34 | public static Either Right(R v) { 35 | return new Either<>(Which.right, null, v); 36 | } 37 | 38 | /** 39 | * Left biased (that is, if optional is engaged, returns a Left; otherwise, a Right). 40 | */ 41 | public static Either fromOpt(Optional opt) { 42 | return opt.isPresent() ? Either.Left(opt.get()) : Either.Right(null); 43 | } 44 | 45 | public boolean isLeft() { 46 | return which == Which.left; 47 | } 48 | 49 | public L getLeft() { 50 | return left; 51 | } 52 | 53 | public boolean isRight() { 54 | return which == Which.right; 55 | } 56 | 57 | public R getRight() { 58 | return right; 59 | } 60 | 61 | // Either a b -> (a -> x) -> (b -> y) -> Either x y 62 | public Either bimap(Function l, Function r) { 63 | return isLeft() 64 | ? Left(l.apply(getLeft())) 65 | : Right(r.apply(getRight())); 66 | } 67 | 68 | // Either a b -> (a -> c) -> (b -> c) -> c 69 | public Result collapse(Function l, Function r) { 70 | return isLeft() 71 | ? l.apply(getLeft()) 72 | : r.apply(getRight()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/GlobalIDebugManagerHolder.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | /** 4 | * This smooths out classloading issues with respect isolated OSGi classloaders, 5 | * similar in spirit to the difference between lucee's "loader" and "core" modules 6 | * (this would be a "loader" thing, externally visible, providing interface defs 7 | * to core things). 8 | */ 9 | public class GlobalIDebugManagerHolder { 10 | // hm ... can there be 2 different engines on the same vm, with different loaders? 11 | // would that happen alot in a dev environment where you want to hook up a debugger? 12 | public static ClassLoader luceeCoreLoader; 13 | public static IDebugManager debugManager; 14 | } -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/IBreakpoint.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | public interface IBreakpoint { 4 | public int getLine(); 5 | 6 | public int getID(); 7 | 8 | public boolean getIsBound(); 9 | } 10 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/ICfValueDebuggerBridge.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | public interface ICfValueDebuggerBridge { 4 | public long getID(); 5 | public int getNamedVariablesCount(); 6 | public int getIndexedVariablesCount(); 7 | 8 | // see main impl for details about nullity 9 | public IDebugEntity maybeNull_asValue(String name); 10 | } 11 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/IDebugEntity.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | public interface IDebugEntity { 4 | public static enum DebugEntityType { 5 | NAMED, INDEXED 6 | } 7 | 8 | public String getName(); 9 | public String getValue(); 10 | public int getNamedVariables(); 11 | public int getIndexedVariables(); 12 | public boolean getExpensive(); 13 | public long getVariablesReference(); 14 | } 15 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/IDebugFrame.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | public interface IDebugFrame { 4 | public String getSourceFilePath(); 5 | public long getId(); 6 | public String getName(); 7 | public int getDepth(); 8 | public int getLine(); 9 | public void setLine(int line); 10 | public IDebugEntity[] getScopes(); 11 | } 12 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/IDebugManager.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * We might be able to whittle this down to just {push,pop,step}, 7 | * which is what instrumented pages need. The other methods are defined in package coreinject, 8 | * and used only from package coreinject, so the definitely-inside-coreinject use site could 9 | * probably cast this to the (single!) concrete implementation. 10 | */ 11 | public interface IDebugManager { 12 | public interface CfStepCallback { 13 | void call(Thread thread, int minDistanceToLuceedebugBaseFrame); 14 | } 15 | 16 | void spawnWorker(Config config, String jdwpHost, int jdwpPort, String debugHost, int debugPort); 17 | /** 18 | * most common frame type 19 | */ 20 | public void pushCfFrame(lucee.runtime.PageContext pc, String sourceFilePath); 21 | /** 22 | * a "default value initialization frame" is the frame that does default function value init, 23 | * like setting a,b,c in the following: 24 | * `function foo(a=1,b=2,c=3) {}; foo(42);` <-- init frame will be stepped into twice, once for `b`, once for `c`; `a` is not default init'd 25 | */ 26 | public void pushCfFunctionDefaultValueInitializationFrame(lucee.runtime.PageContext pageContext, String sourceFilePath); 27 | public void popCfFrame(); 28 | 29 | // these method names are "magic" in that they serve as tags 30 | // when scanning the stack for "where did we transition from lucee to luceedebug code". 31 | // These must be the only "entry points" from lucee compiled CF files into luceedebug. 32 | public void luceedebug_stepNotificationEntry_step(int currentLine); 33 | public void luceedebug_stepNotificationEntry_stepAfterCompletedUdfCall(); 34 | static public boolean isStepNotificationEntryFunc(String methodName) { 35 | return methodName.startsWith("luceedebug_stepNotificationEntry_"); 36 | } 37 | 38 | public void registerStepRequest(Thread thread, int stepType); 39 | public void clearStepRequest(Thread thread); 40 | public IDebugFrame[] getCfStack(Thread thread); 41 | public IDebugEntity[] getScopesForFrame(long frameID); 42 | public IDebugEntity[] getVariables(long id, IDebugEntity.DebugEntityType maybeNull_whichType); 43 | public void registerCfStepHandler(CfStepCallback cb); 44 | 45 | public String doDump(ArrayList suspendedThreads, int variableID); 46 | public String doDumpAsJSON(ArrayList suspendedThreads, int variableID); 47 | 48 | /** 49 | * @return String, or null if there is no path for the target ref 50 | */ 51 | public String getSourcePathForVariablesRef(int variablesRef); 52 | 53 | public Either> evaluate(Long frameID, String expr); 54 | public boolean evaluateAsBooleanForConditionalBreakpoint(Thread thread, String expr); 55 | } 56 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/ILuceeVm.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import java.util.function.BiConsumer; 4 | import java.util.function.Consumer; 5 | 6 | import com.sun.jdi.*; 7 | 8 | import luceedebug.strong.DapBreakpointID; 9 | import luceedebug.strong.JdwpThreadID; 10 | import luceedebug.strong.CanonicalServerAbsPath; 11 | import luceedebug.strong.RawIdePath; 12 | 13 | public interface ILuceeVm { 14 | public void registerStepEventCallback(Consumer cb); 15 | public void registerBreakpointEventCallback(BiConsumer cb); 16 | 17 | public static class BreakpointsChangedEvent { 18 | IBreakpoint[] newBreakpoints = new IBreakpoint[0]; 19 | IBreakpoint[] changedBreakpoints = new IBreakpoint[0]; 20 | int[] deletedBreakpointIDs = new int[0]; 21 | 22 | public static BreakpointsChangedEvent justChanges(IBreakpoint[] changes) { 23 | var result = new BreakpointsChangedEvent(); 24 | result.changedBreakpoints = changes; 25 | return result; 26 | } 27 | } 28 | public void registerBreakpointsChangedCallback(Consumer cb); 29 | 30 | public ThreadReference[] getThreadListing(); 31 | public IDebugFrame[] getStackTrace(long jdwpThreadID); 32 | public IDebugEntity[] getScopes(long frameID); 33 | 34 | /** 35 | * note we return an array, for a single ID 36 | * The ID might be itself for an array or object, with many nested variables 37 | * 38 | * named and indexed are pretty much the same thing for CF purposes ... though we do report that something "has" indexed variables if it is an Array, 39 | * so we'll need to respect frontend requests for those indexed variables. 40 | */ 41 | public IDebugEntity[] getVariables(long ID); // both named and indexed 42 | public IDebugEntity[] getNamedVariables(long ID); 43 | public IDebugEntity[] getIndexedVariables(long ID); 44 | 45 | public IBreakpoint[] bindBreakpoints(RawIdePath idePath, CanonicalServerAbsPath serverAbsPath, int[] lines, String[] exprs); 46 | 47 | public void continue_(long jdwpThreadID); 48 | 49 | public void continueAll(); 50 | 51 | public void stepIn(long jdwpThreadID); 52 | public void stepOver(long jdwpThreadID); 53 | public void stepOut(long jdwpThreadID); 54 | 55 | 56 | public void clearAllBreakpoints(); 57 | 58 | public String dump(int dapVariablesReference); 59 | public String dumpAsJSON(int dapVariablesReference); 60 | 61 | public String[] getTrackedCanonicalFileNames(); 62 | /** 63 | * array of tuples 64 | * [original path, transformed path][] 65 | **/ 66 | public String[][] getBreakpointDetail(); 67 | 68 | /** 69 | * @return String | null 70 | */ 71 | public String getSourcePathForVariablesRef(int variablesRef); 72 | 73 | public Either> evaluate(int frameID, String expr); 74 | } 75 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/IPathTransform.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import java.util.Optional; 4 | 5 | interface IPathTransform { 6 | 7 | /** empty if no match, never null */ 8 | public Optional serverToIde(String s); 9 | /** empty if no match, never null */ 10 | public Optional ideToServer(String s); 11 | 12 | /** 13 | * like toString, but contractually for trace purposes 14 | */ 15 | public String asTraceString(); 16 | } 17 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/IdentityPathTransform.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import java.util.Optional; 4 | 5 | class IdentityPathTransform implements IPathTransform { 6 | public Optional serverToIde(String s) { return Optional.of(s); } 7 | public Optional ideToServer(String s) { return Optional.of(s); } 8 | 9 | public String asTraceString() { 10 | return "IdentityPathTransform"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/LuceeTransformer.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import java.lang.instrument.*; 4 | import java.lang.reflect.Method; 5 | 6 | import org.objectweb.asm.*; 7 | 8 | import java.security.ProtectionDomain; 9 | import java.util.ArrayList; 10 | 11 | public class LuceeTransformer implements ClassFileTransformer { 12 | private final String jdwpHost; 13 | private final int jdwpPort; 14 | private final String debugHost; 15 | private final int debugPort; 16 | private final Config config; 17 | 18 | static public class ClassInjection { 19 | final String name; 20 | final byte[] bytes; 21 | ClassInjection(String name, byte[] bytes) { 22 | this.name = name; 23 | this.bytes = bytes; 24 | } 25 | } 26 | 27 | /** 28 | * if non-null, we are awaiting the initial class load of PageContextImpl 29 | * When that happens, these classes will be injected into that class loader. 30 | * Then, this should be set to null, since we don't need to hold onto them locally. 31 | */ 32 | private ClassInjection[] pendingCoreLoaderClassInjections; 33 | 34 | public LuceeTransformer( 35 | ClassInjection[] injections, 36 | String jdwpHost, 37 | int jdwpPort, 38 | String debugHost, 39 | int debugPort, 40 | Config config 41 | ) { 42 | this.pendingCoreLoaderClassInjections = injections; 43 | 44 | this.jdwpHost = jdwpHost; 45 | this.jdwpPort = jdwpPort; 46 | this.debugHost = debugHost; 47 | this.debugPort = debugPort; 48 | this.config = config; 49 | } 50 | 51 | public byte[] transform(ClassLoader loader, 52 | String className, 53 | Class classBeingRedefined, 54 | ProtectionDomain protectionDomain, 55 | byte[] classfileBuffer 56 | ) throws IllegalClassFormatException { 57 | try { 58 | var classReader = new ClassReader(classfileBuffer); 59 | String superClass = classReader.getSuperName(); 60 | 61 | if (className.equals("lucee/runtime/type/scope/ClosureScope")) { 62 | return instrumentClosureScope(classfileBuffer); 63 | } 64 | else if (className.equals("lucee/runtime/ComponentImpl")) { 65 | if (loader == null) { 66 | throw new RuntimeException("instrumention ComponentImpl but core loader not seen yet"); 67 | } 68 | return instrumentComponentImpl(classfileBuffer, loader); 69 | } 70 | else if (className.equals("lucee/runtime/PageContextImpl")) { 71 | GlobalIDebugManagerHolder.luceeCoreLoader = loader; 72 | 73 | try { 74 | Method m = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); 75 | m.setAccessible(true); 76 | 77 | for (var injection : pendingCoreLoaderClassInjections) { 78 | // warn: reflection ... when does that become unsupported? 79 | m.invoke(GlobalIDebugManagerHolder.luceeCoreLoader, injection.name, injection.bytes, 0, injection.bytes.length); 80 | } 81 | 82 | pendingCoreLoaderClassInjections = null; 83 | 84 | try { 85 | final var klass = GlobalIDebugManagerHolder.luceeCoreLoader.loadClass("luceedebug.coreinject.DebugManager"); 86 | GlobalIDebugManagerHolder.debugManager = (IDebugManager)klass.getConstructor().newInstance(); 87 | 88 | System.out.println("[luceedebug] Loaded " + GlobalIDebugManagerHolder.debugManager + " with ClassLoader '" + GlobalIDebugManagerHolder.debugManager.getClass().getClassLoader() + "'"); 89 | GlobalIDebugManagerHolder.debugManager.spawnWorker(config, jdwpHost, jdwpPort, debugHost, debugPort); 90 | } 91 | catch (Throwable e) { 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | 96 | return classfileBuffer; 97 | } 98 | catch (Throwable e) { 99 | e.printStackTrace(); 100 | System.exit(1); 101 | return null; 102 | } 103 | } 104 | else if (superClass.equals("lucee/runtime/ComponentPageImpl") || superClass.equals("lucee/runtime/PageImpl")) { 105 | // System.out.println("[luceedebug] Instrumenting " + className); 106 | if (GlobalIDebugManagerHolder.luceeCoreLoader == null) { 107 | System.out.println("Got class " + className + " before receiving PageContextImpl, debugging will fail."); 108 | System.exit(1); 109 | } 110 | 111 | return instrumentCfmOrCfc(classfileBuffer, classReader, className); 112 | } 113 | else { 114 | return classfileBuffer; 115 | } 116 | } 117 | catch (Throwable e) { 118 | e.printStackTrace(); 119 | System.exit(1); 120 | return null; 121 | } 122 | } 123 | 124 | private byte[] instrumentPageContextImpl(final byte[] classfileBuffer) { 125 | // Weird problems if we try to compute frames ... tries to lookup PageContextImpl but then it's not yet available in the classloader? 126 | // Mostly meaning, don't do things in PageContextImpl that change frame sizes 127 | var classWriter = new ClassWriter(/*ClassWriter.COMPUTE_FRAMES |*/ ClassWriter.COMPUTE_MAXS); 128 | 129 | try { 130 | var instrumenter = new luceedebug.instrumenter.PageContextImpl(Opcodes.ASM9, classWriter, jdwpHost, jdwpPort, debugHost, debugPort); 131 | var classReader = new ClassReader(classfileBuffer); 132 | 133 | classReader.accept(instrumenter, ClassReader.EXPAND_FRAMES); 134 | 135 | return classWriter.toByteArray(); 136 | } 137 | catch (Throwable e) { 138 | System.err.println("[luceedebug] exception during attempted classfile rewrite"); 139 | System.err.println(e.getMessage()); 140 | e.printStackTrace(); 141 | System.exit(1); 142 | return null; 143 | } 144 | } 145 | 146 | private byte[] instrumentClosureScope(final byte[] classfileBuffer) { 147 | var classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); 148 | 149 | try { 150 | var instrumenter = new luceedebug.instrumenter.ClosureScope(Opcodes.ASM9, classWriter); 151 | var classReader = new ClassReader(classfileBuffer); 152 | 153 | classReader.accept(instrumenter, ClassReader.EXPAND_FRAMES); 154 | 155 | return classWriter.toByteArray(); 156 | } 157 | catch (Throwable e) { 158 | System.err.println("[luceedebug] exception during attempted classfile rewrite"); 159 | System.err.println(e.getMessage()); 160 | e.printStackTrace(); 161 | System.exit(1); 162 | return null; 163 | } 164 | } 165 | 166 | private byte[] instrumentComponentImpl(final byte[] classfileBuffer, ClassLoader loader) { 167 | var classWriter = new ClassWriter(/*ClassWriter.COMPUTE_FRAMES |*/ ClassWriter.COMPUTE_MAXS) { 168 | @Override 169 | protected ClassLoader getClassLoader() { 170 | return loader; 171 | } 172 | }; 173 | 174 | try { 175 | var instrumenter = new luceedebug.instrumenter.ComponentImpl(Opcodes.ASM9, classWriter); 176 | var classReader = new ClassReader(classfileBuffer); 177 | 178 | classReader.accept(instrumenter, ClassReader.EXPAND_FRAMES); 179 | 180 | return classWriter.toByteArray(); 181 | } 182 | catch (Throwable e) { 183 | System.err.println("[luceedebug] exception during attempted classfile rewrite"); 184 | System.err.println(e.getMessage()); 185 | e.printStackTrace(); 186 | System.exit(1); 187 | return null; 188 | } 189 | } 190 | 191 | private byte[] instrumentCfmOrCfc(final byte[] classfileBuffer, ClassReader reader, String className) { 192 | byte[] stepInstrumentedBuffer = classfileBuffer; 193 | var classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) { 194 | @Override 195 | protected ClassLoader getClassLoader() { 196 | return GlobalIDebugManagerHolder.luceeCoreLoader; 197 | } 198 | }; 199 | 200 | try { 201 | var instrumenter = new luceedebug.instrumenter.CfmOrCfc(Opcodes.ASM9, classWriter, className); 202 | var classReader = new ClassReader(stepInstrumentedBuffer); 203 | 204 | classReader.accept(instrumenter, ClassReader.EXPAND_FRAMES); 205 | 206 | return classWriter.toByteArray(); 207 | } 208 | catch (MethodTooLargeException e) { 209 | String baseName = e.getMethodName(); 210 | boolean targetMethodWasBeingInstrumented = false; 211 | 212 | if (baseName.startsWith("__luceedebug__")) { 213 | baseName = baseName.replaceFirst("__luceedebug__", ""); 214 | targetMethodWasBeingInstrumented = true; 215 | } 216 | 217 | if (targetMethodWasBeingInstrumented) { 218 | System.err.println("[luceedebug] Method '" + baseName + "' in class '" + className + "' became too large after instrumentation (size=" + e.getCodeSize() + "). luceedebug won't be able to hit breakpoints in, or expose frame information for, this file."); 219 | } 220 | else { 221 | // this shouldn't happen, we really should only get MethodTooLargeExceptions for code we were instrumenting 222 | System.err.println("[luceedebug] Method " + baseName + " in class " + className + " was too large to for org.objectweb.asm to reemit."); 223 | } 224 | 225 | return classfileBuffer; 226 | } 227 | catch (Throwable e) { 228 | System.err.println("[luceedebug] exception during attempted classfile rewrite"); 229 | System.err.println(e.getMessage()); 230 | e.printStackTrace(); 231 | System.exit(1); 232 | return null; 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/PrefixPathTransform.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import java.util.Arrays; 4 | import java.util.Optional; 5 | import java.util.regex.Pattern; 6 | import java.util.stream.Collectors; 7 | 8 | class PrefixPathTransform implements IPathTransform { 9 | // 10 | // where "unadjusted" means "literally whatever the IDE sent over, no case or path separator adjustments" 11 | // 12 | private final String unadjusted_idePrefix; 13 | private final String unadjusted_serverPrefix; 14 | private final Pattern caseAndPathSepLenient_idePrefixPattern; 15 | private final Pattern caseAndPathSepLenient_serverPrefixPattern; 16 | 17 | public PrefixPathTransform(String idePrefix, String serverPrefix) { 18 | this.unadjusted_idePrefix = idePrefix; 19 | this.caseAndPathSepLenient_idePrefixPattern = asCaseAndPathSepLenientPrefixPattern(idePrefix); 20 | this.unadjusted_serverPrefix = serverPrefix; 21 | this.caseAndPathSepLenient_serverPrefixPattern = asCaseAndPathSepLenientPrefixPattern(serverPrefix); 22 | } 23 | 24 | public Optional serverToIde(String s) { 25 | return replacePrefix(s, caseAndPathSepLenient_serverPrefixPattern, unadjusted_idePrefix); 26 | } 27 | public Optional ideToServer(String s) { 28 | return replacePrefix(s, caseAndPathSepLenient_idePrefixPattern, unadjusted_serverPrefix); 29 | } 30 | 31 | public String asTraceString() { 32 | return "PrefixPathTransform{idePrefix='" + unadjusted_idePrefix + "', serverPrefix='" + unadjusted_serverPrefix + "'}"; 33 | } 34 | 35 | /** 36 | * A prefix like "foo\bar" will match strings like 37 | * "foo\bar 38 | * "foo/bar 39 | * "fOO/baR" 40 | * "foO\\bAr" 41 | * "foO/\bAr" 42 | * etc. 43 | */ 44 | private static Pattern asCaseAndPathSepLenientPrefixPattern(String prefix) { 45 | var prefixPattern = Arrays 46 | .stream(prefix.split("[\\\\/]+")) 47 | .map(v -> Pattern.quote(v)) 48 | .collect(Collectors.joining("[\\\\/]+")); 49 | return Pattern.compile("(?i)^" + prefixPattern + "(.*)$"); 50 | } 51 | 52 | private static Optional replacePrefix(String unadjustedSource, Pattern lenientPattern, String unadjustedPrefix) { 53 | var m = lenientPattern.matcher(unadjustedSource); 54 | if (m.find()) { 55 | return Optional.of(unadjustedPrefix + m.group(1)); 56 | } 57 | return Optional.empty(); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/Breakpoint.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | import luceedebug.*; 4 | import luceedebug.strong.DapBreakpointID; 5 | 6 | class Breakpoint implements IBreakpoint { 7 | final int line; 8 | final DapBreakpointID ID; 9 | final boolean isBound; 10 | 11 | private Breakpoint(int line, DapBreakpointID ID, boolean isBound) { 12 | this.line = line; 13 | this.ID = ID; 14 | this.isBound = isBound; 15 | } 16 | 17 | public static Breakpoint Bound(int line, DapBreakpointID ID) { 18 | return new Breakpoint(line, ID, true); 19 | } 20 | 21 | public static Breakpoint Unbound(int line, DapBreakpointID ID) { 22 | return new Breakpoint(line, ID, false); 23 | } 24 | 25 | public int getLine() { return line; } 26 | public int getID() { return ID.get(); } 27 | public boolean getIsBound() { return isBound; } 28 | } 29 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/ClosureScopeLocalScopeAccessorShim.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | /** 4 | * Intended to be an extension on lucee.runtime.type.scope.ClosureScope, applied during classfile rewrites during agent startup. 5 | */ 6 | public interface ClosureScopeLocalScopeAccessorShim { 7 | lucee.runtime.type.scope.Scope getLocalScope(); 8 | } 9 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/ComponentScopeMarkerTraitShim.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | /** 4 | * Intended to be an extension on lucee.runtime.ComponentImpl 5 | * We need to disambiguate between a component meaning "container of a this/variables/static scope" and "literally a this scope" 6 | * Because we want to show something like the following to the IDE: 7 | * 8 | * someObj : cfc 9 | * - variables -> {...} 10 | * - this -> {...} 11 | * - static -> {...} 12 | * 13 | * But, `someObj` literally is `this` in the above; so we need to know when to show the nested scope listing, and when to show 14 | * the contents of the `this` scope. 15 | * 16 | * There is only a setter because we just use the setter to pin the disambiguating-wrapper object onto something with a 17 | * reasonable lifetime to prevent it from being GC'd. 18 | */ 19 | public interface ComponentScopeMarkerTraitShim { 20 | void __luceedebug__pinComponentScopeMarkerTrait(Object obj); 21 | } 22 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/DebugEntity.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | import luceedebug.*; 4 | 5 | public class DebugEntity implements IDebugEntity { 6 | public String name; 7 | public String value; 8 | public int namedVariables; 9 | public int indexedVariables; 10 | public boolean expensive; 11 | public long variablesReference; 12 | 13 | public String getName() { return name; } 14 | public String getValue() { return value; } 15 | public int getNamedVariables() { return namedVariables; } 16 | public int getIndexedVariables() { return indexedVariables; } 17 | public boolean getExpensive() { return expensive; } 18 | public long getVariablesReference() { return variablesReference; } 19 | } 20 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/DebugManager.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | rankdir=BT 3 | node[shape=rect] 4 | compound=true 5 | ranksep=2 6 | 7 | subgraph cluster_outside_core { 8 | label="outside 'core'\l(effectively 'loader' in Lucee terms)" 9 | IDebugManager 10 | GlobalIDebugManagerHolder 11 | } 12 | 13 | subgraph cluster_inside_core { 14 | label="inside 'core'" 15 | PageContextImpl 16 | DebugManager 17 | luceeCoreLoader [label="Lucee core loader"] 18 | some_page [label="some page"] 19 | some_page -> luceeCoreLoader [arrowhead=curve] 20 | subgraph cluster_leel { 21 | label="isolated by osgi (lucee plugin bundles, esp. admin)" 22 | someOtherOsgiLoader_A [label="BundleWiringImpl$BundleClassLoader"] 23 | some_page_A [label="some page"] 24 | some_page_A -> someOtherOsgiLoader_A [arrowhead=curve] 25 | someOtherOsgiLoader_B [label="BundleWiringImpl$BundleClassLoader"] 26 | some_page_B [label="some page"] 27 | some_page_B -> someOtherOsgiLoader_B [arrowhead=curve] 28 | } 29 | } 30 | 31 | GlobalIDebugManagerHolder -> IDebugManager [arrowhead=dot label="owns 1"] 32 | 33 | DebugManager -> IDebugManager [arrowhead=diamond] 34 | DebugManager -> luceeCoreLoader [arrowhead=curve] 35 | PageContextImpl -> luceeCoreLoader [arrowhead=curve] 36 | someOtherOsgiLoader_A -> luceeCoreLoader [arrowhead=curve] 37 | someOtherOsgiLoader_B -> luceeCoreLoader [arrowhead=curve] 38 | 39 | luceeCoreLoader -> GlobalIDebugManagerHolder [lhead=cluster_outside_core label="OSGi boot delgated\lget opaque ref to IDebugManager\lactual impl has access to core loader"] 40 | } -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/ExprEvaluator.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | import java.lang.invoke.MethodType; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import java.lang.invoke.MethodHandle; 7 | import java.lang.invoke.MethodHandles; 8 | 9 | import lucee.runtime.PageContext; 10 | import luceedebug.Either; 11 | import luceedebug.coreinject.frame.Frame; 12 | 13 | import static luceedebug.coreinject.Utils.terminate; 14 | 15 | import static lucee.loader.engine.CFMLEngine.DIALECT_CFML; 16 | 17 | class ExprEvaluator { 18 | public static final Optional lucee5 = Lucee5Evaluator.maybeGet(); 19 | public static final Optional lucee6 = Lucee6Evaluator.maybeGet(); 20 | 21 | static { 22 | if (lucee5.isEmpty() && lucee6.isEmpty()) { 23 | System.out.println("[luceedebug] No expression evaluator method found."); 24 | System.out.println("[luceedebug] Tried:"); 25 | System.out.println("[luceedebug] lucee.runtime.compiler.Renderer.tag(PageContext,String,int,boolean,boolean) (lucee5 signature)"); 26 | System.out.println("[luceedebug] lucee.runtime.compiler.Renderer.tag(PageContext,String,boolean,boolean) (lucee6 signature)"); 27 | } 28 | } 29 | 30 | public static Either eval(Frame frame, String expr) { 31 | return lucee5 32 | .map(v -> v.eval(frame, expr)) 33 | .or(() -> lucee6.map(v -> v.eval(frame, expr))) 34 | .or(() -> Optional.of(Either.Left("Couldn't find a Lucee engine method to perform evaluation."))) 35 | .get(); 36 | } 37 | 38 | static abstract class Evaluator { 39 | // assignment to result var of a name of our choosing is expected safe because: 40 | // - prefix shouldn't clash with user variables 41 | // - we are synchronized on PageContext by virtue of `doWorkInThisFrame` 42 | // - we delete it after grabbing the result 43 | // At this time, `lucee.runtime.compiler.Renderer.loadPage` will 44 | // cache compilations based on the hash of the source text; so, using the same result name 45 | // every time ensures we don't need to recompile a particular expression every time. 46 | static protected final String errName = "__luceedebug__error"; 47 | static protected final String resultName = "__luceedebug__evalResult"; 48 | static protected String getEvaluatableSourceText(String expr) { 49 | return "" 50 | + "" 51 | + "try { variables['" + resultName + "'] = {'ok': true, 'result': " + expr + " } }" 52 | + "catch (any " + errName + ") { variables['" + resultName + "'] = {'ok': false, 'result': " + errName + ".message } }" 53 | + ""; 54 | } 55 | 56 | protected abstract void evalIntoVariablesScope(Frame frame, String expr) throws Throwable; 57 | 58 | public Either eval(Frame frame, String expr) { 59 | try { 60 | evalIntoVariablesScope(frame, expr); 61 | var obj = consumeResult(frame); 62 | return mungeResult(obj); 63 | } 64 | catch (Throwable e) { 65 | return Either.Left(e.getMessage()); 66 | } 67 | } 68 | 69 | /** 70 | * get the eval'd result out of the frame's variables scope and then delete it from the variables scope. 71 | */ 72 | private Object consumeResult(Frame frame) throws Throwable { 73 | Object evalResult = UnsafeUtils.deprecatedScopeGet(frame.getFrameContext().variables, resultName); 74 | frame.getFrameContext().variables.remove(resultName); 75 | return evalResult; 76 | } 77 | 78 | private Either mungeResult(Object evalResult) { 79 | if (evalResult instanceof Map) { 80 | Map struct = UnsafeUtils.uncheckedCast(evalResult); 81 | var isOk = struct.get("ok"); 82 | var result = struct.get("result"); 83 | if (isOk instanceof Boolean) { 84 | if ((Boolean)isOk) { 85 | return Either.Right(result); 86 | } 87 | else { 88 | var msg = result instanceof String ? (String)result : "Couldn't evaluate expression - expression threw an exception, but resulting message was non-string"; 89 | return Either.Left(msg); 90 | } 91 | } 92 | else { 93 | // shouldn't happen 94 | var isOkClassName = isOk == null ? "null" : isOk.getClass().getName(); 95 | return Either.Left("Couldn't evaluate expression - result `ok` property was non-boolean (got " + isOkClassName + ")"); 96 | } 97 | } 98 | else { 99 | // shouldn't happen 100 | var evalResultClassName = evalResult == null ? "null" : evalResult.getClass().getName(); 101 | return Either.Left("Evaluated expression returned non-Map result of type '" + evalResultClassName + "'"); 102 | } 103 | } 104 | } 105 | 106 | private static class Lucee5Evaluator extends Evaluator { 107 | private final MethodHandle methodHandle; 108 | Lucee5Evaluator(MethodHandle methodHandle) { 109 | this.methodHandle = methodHandle; 110 | } 111 | 112 | protected void evalIntoVariablesScope(Frame frame, String expr) throws Throwable { 113 | methodHandle.invoke( 114 | /*PageContext pc*/ frame.getFrameContext().pageContext, 115 | /*String cfml*/ Evaluator.getEvaluatableSourceText(expr), 116 | /*int dialect*/ DIALECT_CFML, 117 | /*boolean catchOutput*/ false, 118 | /*boolean ignoreScopes*/ false 119 | ); 120 | } 121 | 122 | static Optional maybeGet() { 123 | try { 124 | final MethodType lucee5_evaluateExpr = MethodType.methodType( 125 | /*returntype*/lucee.runtime.compiler.Renderer.Result.class, 126 | /*PageContext pc*/ PageContext.class, 127 | /*String cfml*/ String.class, 128 | /*int dialect*/ int.class, 129 | /*boolean catchOutput*/ boolean.class, 130 | /*boolean ignoreScopes*/ boolean.class 131 | ); 132 | var methodHandle = MethodHandles 133 | .lookup() 134 | .findStatic(lucee.runtime.compiler.Renderer.class, "tag", lucee5_evaluateExpr); 135 | return Optional.of(new Lucee5Evaluator(methodHandle)); 136 | } 137 | catch (NoSuchMethodException e) { 138 | return Optional.empty(); 139 | } 140 | catch (Throwable e) { 141 | return terminate(e); 142 | } 143 | } 144 | } 145 | 146 | private static class Lucee6Evaluator extends Evaluator { 147 | private final MethodHandle methodHandle; 148 | Lucee6Evaluator(MethodHandle methodHandle) { 149 | this.methodHandle = methodHandle; 150 | } 151 | 152 | protected void evalIntoVariablesScope(Frame frame, String expr) throws Throwable { 153 | methodHandle.invoke( 154 | /*PageContext pc*/ frame.getFrameContext().pageContext, 155 | /*String cfml*/ Evaluator.getEvaluatableSourceText(expr), 156 | /*boolean catchOutput*/ false, 157 | /*boolean ignoreScopes*/ false 158 | ); 159 | } 160 | 161 | static Optional maybeGet() { 162 | try { 163 | final MethodType lucee6_evaluateExpr = MethodType.methodType( 164 | /*returntype*/lucee.runtime.compiler.Renderer.Result.class, 165 | /*PageContext pc*/ PageContext.class, 166 | /*String cfml*/ String.class, 167 | /*boolean catchOutput*/ boolean.class, 168 | /*boolean ignoreScopes*/ boolean.class 169 | ); 170 | var methodHandle = MethodHandles 171 | .lookup() 172 | .findStatic(lucee.runtime.compiler.Renderer.class, "tag", lucee6_evaluateExpr); 173 | return Optional.of(new Lucee6Evaluator(methodHandle)); 174 | } 175 | catch (NoSuchMethodException e) { 176 | return Optional.empty(); 177 | } 178 | catch (Throwable e) { 179 | return terminate(e); 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/Iife.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | class Iife { 4 | @FunctionalInterface 5 | public interface Supplier2 { 6 | T get() throws Throwable; 7 | } 8 | 9 | @SuppressWarnings("unchecked") 10 | static public R rethrowUnchecked(Object e) throws T { 11 | throw (T) e; 12 | } 13 | 14 | static public T iife(Supplier2 f) { 15 | try { 16 | return f.get(); 17 | } 18 | catch (Throwable e) { 19 | return rethrowUnchecked(e); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/KlassMap.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | import java.util.HashMap; 4 | 5 | import luceedebug.Config; 6 | import luceedebug.strong.CanonicalServerAbsPath; 7 | 8 | import com.sun.jdi.*; 9 | 10 | class KlassMap { 11 | 12 | final public CanonicalServerAbsPath sourceName; 13 | final public HashMap lineMap; 14 | private final ClassObjectReference objRef; 15 | 16 | final public ReferenceType refType; 17 | 18 | private KlassMap(Config config, ReferenceType refType) throws AbsentInformationException { 19 | objRef = refType.classObject(); 20 | 21 | String sourceName = refType.sourceName(); 22 | var lineMap = new HashMap(); 23 | 24 | for (var loc : refType.allLineLocations()) { 25 | lineMap.put(loc.lineNumber(), loc); 26 | } 27 | 28 | this.sourceName = new CanonicalServerAbsPath(Config.canonicalizeFileName(sourceName)); 29 | 30 | this.lineMap = lineMap; 31 | this.refType = refType; 32 | } 33 | 34 | boolean isCollected() { 35 | return objRef.isCollected(); 36 | } 37 | 38 | /** 39 | * May return null if ReferenceType throws an AbsentInformationException, which the caller 40 | * should interpret as "we can't do anything meaningful with this file" 41 | */ 42 | static KlassMap maybeNull_tryBuildKlassMap(Config config, ReferenceType refType) { 43 | try { 44 | return new KlassMap(config, refType); 45 | } 46 | catch (AbsentInformationException e) { 47 | return null; 48 | } 49 | catch (Throwable e) { 50 | e.printStackTrace(); 51 | System.exit(1); 52 | } 53 | 54 | // unreachable 55 | return null; 56 | } 57 | 58 | @Override 59 | public boolean equals(Object e) { 60 | if (e instanceof KlassMap) { 61 | return ((KlassMap)e).sourceName.equals(this.sourceName); 62 | } 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/UnsafeUtils.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | import lucee.runtime.exp.PageException; 4 | 5 | public class UnsafeUtils { 6 | @SuppressWarnings("unchecked") 7 | static T uncheckedCast(Object e) { 8 | return (T)e; 9 | } 10 | 11 | @SuppressWarnings("deprecation") 12 | public static Object deprecatedScopeGet(lucee.runtime.type.Collection scope, String key) throws PageException { 13 | // lucee wants us to use Collection's 14 | // public Object get(Collection.Key key) throws PageException; 15 | return scope.get(key); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/Utils.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | class Utils { 4 | public static T terminate(Throwable e) { 5 | e.printStackTrace(); 6 | System.exit(1); 7 | return null; 8 | } 9 | 10 | public static T unreachable() { 11 | return unreachable("unreachable"); 12 | } 13 | 14 | public static T unreachable(String s) { 15 | terminate(new RuntimeException(s)); 16 | return null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/ValTracker.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject; 2 | 3 | import java.lang.ref.Cleaner; 4 | import java.lang.ref.WeakReference; 5 | import java.util.Collections; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | import java.util.Optional; 9 | import java.util.WeakHashMap; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | import java.util.concurrent.atomic.AtomicLong; 12 | 13 | public class ValTracker { 14 | private final Cleaner cleaner; 15 | 16 | /** 17 | * Really we want a ConcurrentWeakHashMap - we could use Guava mapMaker with weakKeys. 18 | * Instead we opt to use a sync'd map, because we expect that the number of threads 19 | * touching the map can be more than 1, but will typically be exactly 1 (the DAP session issuing 'show variables' requests) 20 | */ 21 | private final Map wrapperByObj = Collections.synchronizedMap(new WeakHashMap<>()); 22 | private final Map wrapperByID = new ConcurrentHashMap<>(); 23 | 24 | private static class WeakTaggedObject { 25 | private static final AtomicLong nextId = new AtomicLong(); 26 | public final long id; 27 | public final WeakReference wrapped; 28 | public WeakTaggedObject(Object obj) { 29 | this.id = nextId.getAndIncrement(); 30 | this.wrapped = new WeakReference<>(Objects.requireNonNull(obj)); 31 | } 32 | 33 | public Optional maybeToStrong() { 34 | var obj = wrapped.get(); 35 | if (obj == null) { 36 | return Optional.empty(); 37 | } 38 | return Optional.of(new TaggedObject(this.id, obj)); 39 | } 40 | } 41 | 42 | public static class TaggedObject { 43 | public final long id; 44 | 45 | /** 46 | * nonNull 47 | */ 48 | public final Object obj; 49 | 50 | private TaggedObject(long id, Object obj) { 51 | this.id = id; 52 | this.obj = Objects.requireNonNull(obj); 53 | } 54 | } 55 | 56 | private class CleanerRunner implements Runnable { 57 | private final long id; 58 | 59 | CleanerRunner(long id) { 60 | this.id = id; 61 | } 62 | 63 | @Override 64 | public void run() { 65 | // Remove the mapping from (id -> Object) 66 | // The other mapping, Map should have been cleared as per the behavior of the weak-key'd map 67 | // It would be nice to assert that wrapperByObj().size() == wrapperByID.size() after we're done here, but the entries for wrapperByObj 68 | // are cleaned non-deterministically (in the google guava case, the java sync'd WeakHashMap seems much more deterministic but maybe 69 | // not guaranteed to be so), so there's no guarantee that the sizes sync up. 70 | 71 | wrapperByID.remove(id); 72 | 73 | // __debug_updatedTracker("remove", id); 74 | } 75 | } 76 | 77 | public ValTracker(Cleaner cleaner) { 78 | this.cleaner = cleaner; 79 | } 80 | 81 | /** 82 | * This should always succeed, and return an existing or freshly generated TaggedObject. 83 | * @return TaggedObject 84 | */ 85 | public TaggedObject idempotentRegisterObject(Object obj) { 86 | Objects.requireNonNull(obj); 87 | 88 | { 89 | final WeakTaggedObject weakTaggedObj = wrapperByObj.get(obj); 90 | if (weakTaggedObj != null) { 91 | Optional maybeStrong = weakTaggedObj.maybeToStrong(); 92 | if (maybeStrong.isPresent()) { 93 | return maybeStrong.get(); 94 | } 95 | } 96 | } 97 | 98 | final WeakTaggedObject fresh = new WeakTaggedObject(obj); 99 | 100 | registerCleaner(obj, fresh.id); 101 | 102 | wrapperByObj.put(obj, fresh); 103 | wrapperByID.put(fresh.id, fresh); 104 | 105 | // __debug_updatedTracker("add", fresh.id); 106 | 107 | // expected to always succeed here 108 | return fresh.maybeToStrong().get(); 109 | } 110 | 111 | private void registerCleaner(Object obj, long id) { 112 | cleaner.register(obj, new CleanerRunner(id)); 113 | } 114 | 115 | public Optional maybeGetFromId(long id) { 116 | final WeakTaggedObject weakTaggedObj = wrapperByID.get(id); 117 | if (weakTaggedObj == null) { 118 | return Optional.empty(); 119 | } 120 | 121 | return weakTaggedObj.maybeToStrong(); 122 | } 123 | 124 | /** 125 | * debug/sanity check that tracked values are being cleaned up in both maps in response to gc events 126 | */ 127 | @SuppressWarnings("unused") 128 | private void __debug_updatedTracker(String what, long id) { 129 | synchronized (wrapperByObj) { 130 | System.out.println(what + " id=" + id + " wrapperByObjSize=" + wrapperByObj.entrySet().size() + ", wrapperByIDSize=" + wrapperByID.entrySet().size()); 131 | for (var e : wrapperByObj.entrySet()) { 132 | // size might be reported as N but if all keys have been GC'd then we won't iterate at all 133 | System.out.println(" entry (K null)=" + (e.getKey() == null ? "y" : "n") + " (V.id)=" + e.getValue().id + " (v.obj null)=" + (e.getValue().wrapped.get() == null ? "y" : "n")); 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/frame/DebugFrame.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject.frame; 2 | 3 | import lucee.runtime.PageContext; 4 | import luceedebug.IDebugFrame; 5 | import luceedebug.coreinject.ValTracker; 6 | import luceedebug.coreinject.frame.Frame.FrameContext; 7 | 8 | /** 9 | * Should be a sealed class, subtypes are: 10 | * - Frame 11 | * - DummyFrame 12 | */ 13 | public abstract class DebugFrame implements IDebugFrame { 14 | // https://github.com/softwareCobbler/luceedebug/issues/68 15 | // generating a debug frame involves calling into lucee engine code to grab scopes. 16 | // Lucee can do anything it wants there, and at least when reading from the session scope 17 | // to deserialize cfcs that had been serialized, can end up invoking more coldfusion code (i.e. calling their 18 | // pseudoconstructors), which in turn can push more frames, and we want to track those frames, which can push 19 | // more frames ... and so on. So when we push a frame, we need to know if we are "already pushing a frame", 20 | // and if so, we just return some dummy frame which is guranteed to NOT schedule more work. 21 | static private ThreadLocal isPushingFrame = ThreadLocal.withInitial(() -> false); 22 | 23 | static public DebugFrame makeFrame(String sourceFilePath, int depth, ValTracker valTracker, PageContext pageContext, FrameContext root) { 24 | if (isPushingFrame.get()) { 25 | return DummyFrame.get(); 26 | } 27 | else { 28 | try { 29 | isPushingFrame.set(true); 30 | return new Frame(sourceFilePath, depth, valTracker, pageContext, root); 31 | } 32 | finally { 33 | isPushingFrame.set(false); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/coreinject/frame/DummyFrame.java: -------------------------------------------------------------------------------- 1 | package luceedebug.coreinject.frame; 2 | 3 | import luceedebug.IDebugEntity; 4 | 5 | /** 6 | * DummyFrame is just a placeholder for when we wanted to push a frame 7 | * but were unable to generate one. See notes on `DebugFrame`. 8 | */ 9 | public class DummyFrame extends DebugFrame { 10 | private static DummyFrame instance = new DummyFrame(); 11 | private DummyFrame() {} 12 | 13 | public static DummyFrame get() { 14 | return instance; 15 | } 16 | 17 | private static T fail() { 18 | throw new RuntimeException("Methods on 'DummyFrame' should never be called."); 19 | } 20 | 21 | public String getSourceFilePath() { return fail(); }; 22 | public long getId() { return fail(); }; 23 | public String getName() { return fail(); }; 24 | public int getDepth() { return fail(); }; 25 | public int getLine() { return fail(); }; 26 | public void setLine(int line) { fail(); }; 27 | public IDebugEntity[] getScopes() { return fail(); } 28 | } 29 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/generated/.gitkeep: -------------------------------------------------------------------------------- 1 | # we write generated files into generated/ though it seems like we'd like 2 | # to write them into build/; but in build/, the vscode java plugin does 3 | # not find classes. So then we get errors in the IDE even though the gradle 4 | # build is fine. 5 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/instrumenter/CfmOrCfc.java: -------------------------------------------------------------------------------- 1 | package luceedebug.instrumenter; 2 | 3 | import org.objectweb.asm.*; 4 | import org.objectweb.asm.commons.AdviceAdapter; 5 | import org.objectweb.asm.commons.GeneratorAdapter; 6 | import org.objectweb.asm.commons.Method; 7 | 8 | public class CfmOrCfc extends ClassVisitor { 9 | private Type thisType = null; // is not initialized until `visit` 10 | private String sourceName = "??????"; // is not initialized until `visitSource` 11 | 12 | public CfmOrCfc(int api, ClassWriter cw, String className) { 13 | super(api, cw); 14 | } 15 | 16 | @Override 17 | public void visit( 18 | int version, 19 | int access, 20 | String name, 21 | String signature, 22 | String superName, 23 | String[] interfaces 24 | ) { 25 | this.thisType = Type.getType("L" + name + ";"); 26 | super.visit(version, access, name, signature, superName, interfaces); 27 | } 28 | 29 | static class IDebugManager_t { 30 | static final Type type = Type.getType("Lluceedebug/IDebugManager;"); 31 | // pushCfFrame : (_ : PageContext, filenameAbsPath : string, distanceToFrame : int) => void 32 | static final Method m_pushCfFrame = Method.getMethod("void pushCfFrame(lucee.runtime.PageContext, String)"); 33 | // pushCfFunctionDefaultValueInitializationFrame : (_ : PageContext, filenameAbsPath : string, distanceToFrame : int) => void 34 | static final Method m_pushCfFunctionDefaultValueInitializationFrame = Method.getMethod("void pushCfFunctionDefaultValueInitializationFrame(lucee.runtime.PageContext, String)"); 35 | // popCfFrame : () => void 36 | static final Method m_popCfFrame = Method.getMethod("void popCfFrame()"); 37 | // step : (currentLine : int) => void 38 | static final Method m_step = Method.getMethod("void luceedebug_stepNotificationEntry_step(int)"); 39 | // stepAfterCompletedUdfCall : () => void 40 | static final Method m_stepAfterCompletedUdfCall = Method.getMethod("void luceedebug_stepNotificationEntry_stepAfterCompletedUdfCall()"); 41 | } 42 | 43 | static class GlobalIDebugManagerHolder_t { 44 | static final Type type = Type.getType("Lluceedebug/GlobalIDebugManagerHolder;"); 45 | } 46 | 47 | @Override 48 | public void visitSource(String source, String debug) { 49 | this.sourceName = source; 50 | super.visitSource(source, debug); 51 | } 52 | 53 | /** 54 | * prevents recursively trying to wrap wrapper methods 55 | */ 56 | private boolean isWrappingMethod = false; 57 | 58 | /** 59 | * wrap some cf call like 60 | * 61 | * Object udfCallX(...) throws ... { 62 | * try { 63 | * DebugManager.pushCfFrame(); 64 | * return __luceedebug__udfCallX(...args); // "real" method is renamed 65 | * } 66 | * finally { 67 | * DebugManager.popCfFrame(); 68 | * } 69 | * } 70 | */ 71 | private void createWrapperMethod( 72 | int access, 73 | String name, 74 | String descriptor, 75 | String signature, 76 | String[] exceptions, 77 | String delegateToName 78 | ) { 79 | try { 80 | isWrappingMethod = true; 81 | 82 | final var argCount = Type.getArgumentTypes(descriptor).length; 83 | final var mv = visitMethod(access, name, descriptor, signature, exceptions); 84 | final var ga = new GeneratorAdapter(mv, access, name, descriptor); 85 | 86 | final var tryStart = ga.mark(); 87 | 88 | // 89 | // try 90 | // 91 | { 92 | // [] 93 | 94 | // pushCfFrame 95 | { 96 | ga.getStatic(GlobalIDebugManagerHolder_t.type, "debugManager", IDebugManager_t.type); 97 | // [IDebugManager_t] 98 | 99 | ga.loadArg(0); // should be PageContextImpl as PageContext 100 | // [IDebugManager_t, PageContext] 101 | 102 | ga.push(sourceName); 103 | // [IDebugManager_t, PageContext, String] 104 | 105 | if (name.startsWith("udfDefaultValue")) { 106 | ga.invokeInterface(IDebugManager_t.type, IDebugManager_t.m_pushCfFunctionDefaultValueInitializationFrame); 107 | } 108 | else { 109 | ga.invokeInterface(IDebugManager_t.type, IDebugManager_t.m_pushCfFrame); 110 | } 111 | // [] 112 | } 113 | 114 | ga.loadThis(); 115 | // [] 116 | 117 | for (int i = 0; i < argCount; ++i) { 118 | ga.loadArg(i); 119 | } 120 | // [, ...args] 121 | 122 | ga.invokeVirtual(thisType, new Method(delegateToName, descriptor)); 123 | // [] 124 | 125 | // popCfFrame 126 | { 127 | ga.getStatic(GlobalIDebugManagerHolder_t.type, "debugManager", IDebugManager_t.type); 128 | ga.invokeInterface(IDebugManager_t.type, IDebugManager_t.m_popCfFrame); 129 | 130 | // non-exceptional function return gets a step notification, 131 | // with the exception of udfDefaultValue frames (serves to set function default args), which behave sort of weirdly 132 | // (as if they're merged with their associated UDF? not clear at the moment) 133 | if (!name.equals("udfDefaultValue")) { 134 | ga.getStatic(GlobalIDebugManagerHolder_t.type, "debugManager", IDebugManager_t.type); 135 | ga.invokeInterface(IDebugManager_t.type, IDebugManager_t.m_stepAfterCompletedUdfCall); 136 | } 137 | } 138 | 139 | // [] 140 | 141 | ga.returnValue(); 142 | } 143 | 144 | final var tryEnd = ga.mark(); 145 | 146 | // 147 | // catch 148 | // 149 | { 150 | // [] 151 | 152 | // popCfFrame 153 | { 154 | ga.getStatic(GlobalIDebugManagerHolder_t.type, "debugManager", IDebugManager_t.type); 155 | ga.invokeInterface(IDebugManager_t.type, IDebugManager_t.m_popCfFrame); 156 | 157 | // 158 | // n.b exceptional function return DOES NOT get a step notification 159 | // 160 | } 161 | // [] 162 | 163 | ga.throwException(); 164 | } 165 | 166 | ga.visitTryCatchBlock(tryStart, tryEnd, tryEnd, null); 167 | 168 | ga.endMethod(); 169 | } 170 | finally { 171 | isWrappingMethod = false; 172 | } 173 | } 174 | 175 | @Override 176 | public MethodVisitor visitMethod( 177 | final int access, 178 | final String name, 179 | final String descriptor, 180 | final String signature, 181 | final String[] exceptions 182 | ) { 183 | // `call` is the main entry point to a cfm page (how about cfc's, is that the same?) 184 | // `udfCall()?` are generated -- per function statement? how about per function expression? 185 | // sometimes they can have empty bodies (at least exactly "udfCall"), even if subsequently indexed methods don't 186 | // e.g. `udfCall` is an empty method with zero bytecodes, but `udfCall1` is non-empty, etc. 187 | 188 | 189 | if (!isWrappingMethod && ( 190 | name.equals("call") 191 | || name.startsWith("call_") // call_001, call_002, etc. 192 | || name.startsWith("udfCall") 193 | || name.equals("initComponent") 194 | || name.equals("newInstance") 195 | || name.equals("threadCall") 196 | || name.startsWith("udfDefaultValue") 197 | || name.equals("staticConstructor") 198 | )) { 199 | // We'd like to retain the ability for `callStackGet` to return function names. 200 | // Lucee scans stack traces for frames starting with "udfCall" in order to reflect back the function names. 201 | // It will ignore wrappers (that start with "udfCall") because we don't visit line number info 202 | // in those functions (that is, our wrapper method that changes the body of the lucee function to call our 203 | // "delegated-to" function has no line info, but the delegated-to method does get line number info visited). 204 | // Because the wrapper methods have no line info, stack trace elems in those methods have line numbers of -1, 205 | // which luckily for us, means "ignore this frame", for Lucee's `callStackGet` method. 206 | // see `lucee.runtime.functions.system.CallStackGet` 207 | final String delegateToName = name.startsWith("udfCall") 208 | ? "udfCall__luceedebug__" + name 209 | : "__luceedebug__" + name; 210 | 211 | createWrapperMethod(access, name, descriptor, signature, exceptions, delegateToName); 212 | 213 | final var mv = super.visitMethod(access, delegateToName, descriptor, signature, exceptions); 214 | 215 | return new AdviceAdapter(this.api, mv, access, delegateToName, descriptor) { 216 | @Override 217 | public void visitLineNumber(int line, Label start) { 218 | // step 219 | { 220 | this.getStatic(GlobalIDebugManagerHolder_t.type, "debugManager", IDebugManager_t.type); 221 | this.push(line); 222 | this.invokeInterface(IDebugManager_t.type, IDebugManager_t.m_step); 223 | } 224 | 225 | super.visitLineNumber(line, this.mark()); 226 | } 227 | }; 228 | } 229 | else { 230 | return super.visitMethod(access, name, descriptor, signature, exceptions); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/instrumenter/ClosureScope.java: -------------------------------------------------------------------------------- 1 | package luceedebug.instrumenter; 2 | 3 | import org.objectweb.asm.*; 4 | import org.objectweb.asm.commons.GeneratorAdapter; 5 | 6 | /** 7 | * extend lucee.runtime.type.scope.ClosureScope to implement ClosureScopeLocalScopeAccessorShim 8 | */ 9 | 10 | public class ClosureScope extends ClassVisitor { 11 | public ClosureScope(int api, ClassWriter cw) { 12 | super(api, cw); 13 | } 14 | 15 | @Override 16 | public void visit( 17 | int version, 18 | int access, 19 | String name, 20 | String signature, 21 | String superName, 22 | String[] interfaces 23 | ) { 24 | final var augmentedInterfaces = new String[interfaces.length + 1]; 25 | for (int i = 0; i < interfaces.length; i++) { 26 | augmentedInterfaces[i] = interfaces[i]; 27 | } 28 | augmentedInterfaces[interfaces.length] = "luceedebug/coreinject/ClosureScopeLocalScopeAccessorShim"; 29 | 30 | super.visit(version, access, name, signature, superName, augmentedInterfaces); 31 | } 32 | 33 | @Override 34 | public void visitEnd() { 35 | final var name = "getLocalScope"; 36 | final var descriptor = "()Llucee/runtime/type/scope/Scope;"; 37 | final var mv = visitMethod(org.objectweb.asm.Opcodes.ACC_PUBLIC, name, descriptor, null, null); 38 | final var ga = new GeneratorAdapter(mv, org.objectweb.asm.Opcodes.ACC_PUBLIC, name, descriptor); 39 | ga.loadThis(); 40 | ga.getField(org.objectweb.asm.Type.getType("Llucee/runtime/type/scope/ClosureScope;"), "local", org.objectweb.asm.Type.getType("Llucee/runtime/type/scope/Local;")); 41 | ga.returnValue(); 42 | ga.endMethod(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/instrumenter/ComponentImpl.java: -------------------------------------------------------------------------------- 1 | package luceedebug.instrumenter; 2 | 3 | import org.objectweb.asm.*; 4 | import org.objectweb.asm.commons.GeneratorAdapter; 5 | 6 | public class ComponentImpl extends ClassVisitor { 7 | public ComponentImpl(int api, ClassWriter cw) { 8 | super(api, cw); 9 | } 10 | 11 | @Override 12 | public void visit( 13 | int version, 14 | int access, 15 | String name, 16 | String signature, 17 | String superName, 18 | String[] interfaces 19 | ) { 20 | final var augmentedInterfaces = new String[interfaces.length + 1]; 21 | for (int i = 0; i < interfaces.length; i++) { 22 | augmentedInterfaces[i] = interfaces[i]; 23 | } 24 | augmentedInterfaces[interfaces.length] = "luceedebug/coreinject/ComponentScopeMarkerTraitShim"; 25 | 26 | super.visit(version, access, name, signature, superName, augmentedInterfaces); 27 | } 28 | 29 | @Override 30 | public void visitEnd() { 31 | final var fieldName = "__luceedebug__pinned_componentScopeMarkerTrait"; 32 | visitField(org.objectweb.asm.Opcodes.ACC_PUBLIC | org.objectweb.asm.Opcodes.ACC_TRANSIENT, fieldName, "Ljava/lang/Object;", null, null); 33 | 34 | final var name = "__luceedebug__pinComponentScopeMarkerTrait"; 35 | final var descriptor = "(Ljava/lang/Object;)V"; 36 | final var mv = visitMethod(org.objectweb.asm.Opcodes.ACC_PUBLIC, name, descriptor, null, null); 37 | final var ga = new GeneratorAdapter(mv, org.objectweb.asm.Opcodes.ACC_PUBLIC, name, descriptor); 38 | 39 | ga.loadThis(); 40 | ga.loadArg(0); 41 | ga.putField(org.objectweb.asm.Type.getType("Llucee/runtime/ComponentImpl;"), fieldName, org.objectweb.asm.Type.getType("Ljava/lang/Object;")); 42 | ga.visitInsn(org.objectweb.asm.Opcodes.RETURN); 43 | ga.endMethod(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/instrumenter/PageContextImpl.java: -------------------------------------------------------------------------------- 1 | package luceedebug.instrumenter; 2 | 3 | import org.objectweb.asm.*; 4 | import org.objectweb.asm.commons.AdviceAdapter; 5 | import org.objectweb.asm.commons.Method; 6 | 7 | public class PageContextImpl extends ClassVisitor { 8 | 9 | final String jdwpHost; 10 | final int jdwpPort; 11 | final String debugHost; 12 | final int debugPort; 13 | 14 | public PageContextImpl( 15 | int api, 16 | ClassWriter cw, 17 | String jdwpHost, 18 | int jdwpPort, 19 | String debugHost, 20 | int debugPort 21 | ) { 22 | super(api, cw); 23 | this.jdwpHost = jdwpHost; 24 | this.jdwpPort = jdwpPort; 25 | this.debugHost = debugHost; 26 | this.debugPort = debugPort; 27 | } 28 | 29 | @Override 30 | public MethodVisitor visitMethod( 31 | final int access, 32 | final String name, 33 | final String descriptor, 34 | final String signature, 35 | final String[] exceptions 36 | ) { 37 | MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); 38 | 39 | if (name.equals("")) { 40 | return new AdviceAdapter(this.api, mv, access, name, descriptor) { 41 | @Override 42 | protected void onMethodEnter() { 43 | this.push(jdwpHost); 44 | this.push(jdwpPort); 45 | this.push(debugHost); 46 | this.push(debugPort); 47 | this.invokeStatic(Type.getType("Lluceedebug/coreinject/DebugManager;"), Method.getMethod("void spawnWorker(java.lang.String, int, java.lang.String, int)")); 48 | } 49 | }; 50 | } 51 | else { 52 | return mv; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/strong/CanonicalServerAbsPath.java: -------------------------------------------------------------------------------- 1 | package luceedebug.strong; 2 | 3 | public final class CanonicalServerAbsPath extends StrongT { 4 | public CanonicalServerAbsPath(String v) { 5 | super(v); 6 | } 7 | 8 | @Override 9 | public String toString() { 10 | return this.get(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/strong/DapBreakpointID.java: -------------------------------------------------------------------------------- 1 | package luceedebug.strong; 2 | 3 | public final class DapBreakpointID extends StrongT { 4 | public DapBreakpointID(Integer v) { 5 | super(v); 6 | } 7 | } -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/strong/JdwpThreadID.java: -------------------------------------------------------------------------------- 1 | package luceedebug.strong; 2 | 3 | public final class JdwpThreadID extends StrongT { 4 | public JdwpThreadID(Long v) { 5 | super(v); 6 | } 7 | 8 | public static JdwpThreadID of(com.sun.jdi.ThreadReference v) { 9 | return new JdwpThreadID(v.uniqueID()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/strong/RawIdePath.java: -------------------------------------------------------------------------------- 1 | package luceedebug.strong; 2 | 3 | public final class RawIdePath extends StrongT { 4 | public RawIdePath(String v) { 5 | super(v); 6 | } 7 | 8 | @Override 9 | public String toString() { 10 | return this.get(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /luceedebug/src/main/java/luceedebug/strong/StrongT.java: -------------------------------------------------------------------------------- 1 | package luceedebug.strong; 2 | 3 | /** 4 | * Typical use case here is derived classes are final and 5 | * are simple "strong" wrappers around the underlying type `T`. 6 | */ 7 | public abstract class StrongT { 8 | private final T v; 9 | 10 | StrongT(T v) { 11 | this.v = v; 12 | } 13 | 14 | public T get() { 15 | return v; 16 | } 17 | 18 | @Override 19 | public int hashCode() { 20 | return v.hashCode(); 21 | } 22 | 23 | @Override 24 | public boolean equals(Object other) { 25 | return (other instanceof StrongT) && v.equals(((StrongT)other).v); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/EvaluatesAnExpression.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import org.junit.jupiter.params.ParameterizedTest; 4 | import org.junit.jupiter.params.provider.MethodSource; 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | import java.io.IOException; 8 | import java.net.InetSocketAddress; 9 | import java.net.Socket; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import com.github.dockerjava.api.DockerClient; 13 | import com.google.api.client.http.GenericUrl; 14 | import com.google.api.client.http.HttpRequest; 15 | import com.google.api.client.http.javanet.NetHttpTransport; 16 | 17 | import luceedebug.testutils.DapUtils; 18 | import luceedebug.testutils.DockerUtils; 19 | import luceedebug.testutils.LuceeUtils; 20 | import luceedebug.testutils.TestParams.LuceeAndDockerInfo; 21 | import luceedebug.testutils.DockerUtils.HostPortBindings; 22 | 23 | import org.eclipse.lsp4j.debug.launch.DSPLauncher; 24 | 25 | class EvaluatesAnExpression { 26 | @ParameterizedTest 27 | @MethodSource("luceedebug.testutils.TestParams#getLuceeAndDockerInfo") 28 | void a(LuceeAndDockerInfo dockerInfo) throws Throwable { 29 | final DockerClient dockerClient = DockerUtils.getDefaultDockerClient(); 30 | 31 | final String imageID = DockerUtils 32 | .buildOrGetImage(dockerClient, dockerInfo.dockerFile) 33 | .getImageID(); 34 | 35 | final String containerID = DockerUtils 36 | .getFreshDefaultContainer( 37 | dockerClient, 38 | imageID, 39 | dockerInfo.luceedebugProjectRoot.toFile(), 40 | dockerInfo.getTestWebRoot("app1"), 41 | new int[][]{ 42 | new int[]{8888,8888}, 43 | new int[]{10000,10000} 44 | } 45 | ) 46 | .getContainerID(); 47 | 48 | dockerClient 49 | .startContainerCmd(containerID) 50 | .exec(); 51 | 52 | HostPortBindings portBindings = DockerUtils.getPublishedHostPortBindings(dockerClient, containerID); 53 | 54 | try { 55 | LuceeUtils.pollForServerIsActive("http://localhost:" + portBindings.http + "/heartbeat.cfm"); 56 | 57 | final var dapClient = new DapUtils.MockClient(); 58 | 59 | final var socket = new Socket(); 60 | socket.connect(new InetSocketAddress("localhost", portBindings.dap)); 61 | final var launcher = DSPLauncher.createClientLauncher(dapClient, socket.getInputStream(), socket.getOutputStream()); 62 | launcher.startListening(); 63 | final var dapServer = launcher.getRemoteProxy(); 64 | 65 | DapUtils.init(dapServer).join(); 66 | DapUtils.attach(dapServer).join(); 67 | 68 | DapUtils 69 | .setBreakpoints(dapServer, "/var/www/a.cfm", 4) 70 | .join(); 71 | 72 | final var requestThreadToBeBlockedByBreakpoint = new java.lang.Thread(() -> { 73 | final var requestFactory = new NetHttpTransport().createRequestFactory(); 74 | HttpRequest request; 75 | try { 76 | request = requestFactory.buildGetRequest(new GenericUrl("http://localhost:" + portBindings.http + "/a.cfm")); 77 | request.execute().disconnect(); 78 | } 79 | catch (IOException e) { 80 | throw new RuntimeException(e); 81 | } 82 | }); 83 | 84 | final var threadID = DapUtils.doWithStoppedEventFuture( 85 | dapClient, 86 | () -> requestThreadToBeBlockedByBreakpoint.start() 87 | ).get(1000, TimeUnit.MILLISECONDS).getThreadId(); 88 | 89 | final var frameID = DapUtils 90 | .getStackTrace(dapServer, threadID) 91 | .join() 92 | .getStackFrames()[0] 93 | .getId(); 94 | 95 | assertEquals( 96 | "false", 97 | DapUtils.evaluate(dapServer, frameID, "isNull(arguments.n)").join().getResult(), 98 | "evaluation result as expected" 99 | ); 100 | 101 | assertEquals( 102 | "\"1,2,3\"", 103 | DapUtils.evaluate(dapServer, frameID, "arrayToList([1,2,3], \",\")").join().getResult(), 104 | "evaluation result as expected" 105 | ); 106 | 107 | assertEquals( 108 | "\"bar\"", 109 | DapUtils.evaluate(dapServer, frameID, "e").join().getResult(), 110 | "e is initialized to \"bar\"" 111 | ); 112 | 113 | // An expression that will throw an exception, should not clobber "e" in the local scope 114 | DapUtils.evaluate(dapServer, frameID, "zzz"); 115 | 116 | assertEquals( 117 | "\"bar\"", 118 | DapUtils.evaluate(dapServer, frameID, "e").join().getResult(), 119 | "e is still \"bar\"" 120 | ); 121 | 122 | DapUtils.continue_(dapServer, threadID); 123 | 124 | requestThreadToBeBlockedByBreakpoint.join(); 125 | 126 | DapUtils.disconnect(dapServer); 127 | 128 | //socket.close(); // how to let launcher know we want to do this? 129 | } 130 | finally { 131 | dockerClient.stopContainerCmd(containerID).exec(); 132 | dockerClient.removeContainerCmd(containerID).exec(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/HitsABreakpointAndRetrievesVariableInfo.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | import java.net.Socket; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.function.Supplier; 8 | 9 | import org.eclipse.lsp4j.debug.Scope; 10 | import org.eclipse.lsp4j.debug.Variable; 11 | import org.eclipse.lsp4j.debug.launch.DSPLauncher; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertNotNull; 14 | import org.junit.jupiter.params.ParameterizedTest; 15 | import org.junit.jupiter.params.provider.MethodSource; 16 | 17 | import com.github.dockerjava.api.DockerClient; 18 | import com.google.api.client.http.GenericUrl; 19 | import com.google.api.client.http.HttpRequest; 20 | import com.google.api.client.http.javanet.NetHttpTransport; 21 | 22 | import luceedebug.testutils.DapUtils; 23 | import luceedebug.testutils.DockerUtils; 24 | import luceedebug.testutils.DockerUtils.HostPortBindings; 25 | import luceedebug.testutils.LuceeUtils; 26 | import luceedebug.testutils.TestParams.LuceeAndDockerInfo; 27 | 28 | import static luceedebug.testutils.Utils.unreachable; 29 | 30 | class HitsABreakpointAndRetrievesVariableInfo { 31 | @ParameterizedTest 32 | @MethodSource("luceedebug.testutils.TestParams#getLuceeAndDockerInfo") 33 | void a(LuceeAndDockerInfo dockerInfo) throws Throwable { 34 | final DockerClient dockerClient = DockerUtils.getDefaultDockerClient(); 35 | 36 | final String imageID = DockerUtils 37 | .buildOrGetImage(dockerClient, dockerInfo.dockerFile) 38 | .getImageID(); 39 | 40 | final String containerID = DockerUtils 41 | .getFreshDefaultContainer( 42 | dockerClient, 43 | imageID, 44 | dockerInfo.luceedebugProjectRoot.toFile(), 45 | dockerInfo.getTestWebRoot("app1"), 46 | new int[][]{ 47 | new int[]{8888,8888}, 48 | new int[]{10000,10000} 49 | } 50 | ) 51 | .getContainerID(); 52 | 53 | dockerClient 54 | .startContainerCmd(containerID) 55 | .exec(); 56 | 57 | HostPortBindings portBindings = DockerUtils.getPublishedHostPortBindings(dockerClient, containerID); 58 | 59 | try { 60 | LuceeUtils.pollForServerIsActive("http://localhost:" + portBindings.http + "/heartbeat.cfm"); 61 | 62 | final var dapClient = new DapUtils.MockClient(); 63 | 64 | final var socket = new Socket(); 65 | socket.connect(new InetSocketAddress("localhost", portBindings.dap)); 66 | final var launcher = DSPLauncher.createClientLauncher(dapClient, socket.getInputStream(), socket.getOutputStream()); 67 | launcher.startListening(); 68 | final var dapServer = launcher.getRemoteProxy(); 69 | 70 | DapUtils.init(dapServer).join(); 71 | DapUtils.attach(dapServer).join(); 72 | 73 | DapUtils 74 | .setBreakpoints(dapServer, "/var/www/a.cfm", 3) 75 | .join(); 76 | 77 | final var requestThreadToBeBlockedByBreakpoint = new java.lang.Thread(() -> { 78 | final var requestFactory = new NetHttpTransport().createRequestFactory(); 79 | HttpRequest request; 80 | try { 81 | request = requestFactory.buildGetRequest(new GenericUrl("http://localhost:" + portBindings.http + "/a.cfm")); 82 | request.execute().disconnect(); 83 | } 84 | catch (IOException e) { 85 | throw new RuntimeException(e); 86 | } 87 | }); 88 | 89 | final var threadID = DapUtils.doWithStoppedEventFuture( 90 | dapClient, 91 | () -> { 92 | requestThreadToBeBlockedByBreakpoint.start(); 93 | } 94 | ) 95 | .get(1000, TimeUnit.MILLISECONDS) 96 | .getThreadId(); 97 | 98 | final var stackTrace = DapUtils 99 | .getStackTrace(dapServer, threadID) 100 | .join(); 101 | 102 | assertEquals(stackTrace.getTotalFrames(), 2); 103 | 104 | final var scopes = DapUtils 105 | .getScopes( 106 | dapServer, 107 | stackTrace.getStackFrames()[0].getId() 108 | ) 109 | .join() 110 | .getScopes(); 111 | 112 | final var argScope = ((Supplier)() -> { 113 | for (var scope : scopes) { 114 | if (scope.getName().equals("arguments")) { 115 | return scope; 116 | } 117 | } 118 | return null; 119 | }).get(); 120 | 121 | assertNotNull(argScope, "got arg scope"); 122 | 123 | final var variables = DapUtils 124 | .getVariables(dapServer, argScope) 125 | .join() 126 | .getVariables(); 127 | 128 | final var target = ((Supplier)() -> { 129 | for (var variable : variables) { 130 | if (variable.getName().equals("n")) { 131 | return variable; 132 | } 133 | } 134 | return null; 135 | }).get(); 136 | 137 | assertNotNull(target, "got expected variable"); 138 | 139 | if (dockerInfo.engineVersion == 5) { 140 | assertEquals("42.0", target.getValue()); 141 | } 142 | else if (dockerInfo.engineVersion == 6) { 143 | assertEquals("42", target.getValue()); 144 | } 145 | else { 146 | unreachable(); 147 | } 148 | 149 | DapUtils.continue_(dapServer, threadID); 150 | 151 | requestThreadToBeBlockedByBreakpoint.join(); 152 | 153 | DapUtils.disconnect(dapServer); 154 | 155 | //socket.close(); // how to let launcher know we want to do this? 156 | } 157 | finally { 158 | dockerClient.stopContainerCmd(containerID).exec(); 159 | dockerClient.removeContainerCmd(containerID).exec(); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/SteppingThroughDefaultArgs.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import org.junit.jupiter.params.ParameterizedTest; 4 | import org.junit.jupiter.params.provider.MethodSource; 5 | 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | import java.io.IOException; 9 | import java.net.InetSocketAddress; 10 | import java.net.Socket; 11 | import java.util.Map; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import com.github.dockerjava.api.DockerClient; 15 | import com.google.api.client.http.GenericUrl; 16 | import com.google.api.client.http.HttpRequest; 17 | import com.google.api.client.http.javanet.NetHttpTransport; 18 | 19 | import luceedebug.testutils.DapUtils; 20 | import luceedebug.testutils.DockerUtils; 21 | import luceedebug.testutils.LuceeUtils; 22 | import luceedebug.testutils.TestParams.LuceeAndDockerInfo; 23 | import luceedebug.testutils.DockerUtils.HostPortBindings; 24 | 25 | import org.eclipse.lsp4j.debug.launch.DSPLauncher; 26 | 27 | class SteppingThroughDefaultArgs { 28 | @ParameterizedTest 29 | @MethodSource("luceedebug.testutils.TestParams#getLuceeAndDockerInfo") 30 | void a(LuceeAndDockerInfo dockerInfo) throws Throwable { 31 | final DockerClient dockerClient = DockerUtils.getDefaultDockerClient(); 32 | 33 | final String imageID = DockerUtils 34 | .buildOrGetImage(dockerClient, dockerInfo.dockerFile) 35 | .getImageID(); 36 | 37 | final String containerID = DockerUtils 38 | .getFreshDefaultContainer( 39 | dockerClient, 40 | imageID, 41 | dockerInfo.luceedebugProjectRoot.toFile(), 42 | dockerInfo.getTestWebRoot("stepping_through_default_args"), 43 | new int[][]{ 44 | new int[]{8888,8888}, 45 | new int[]{10000,10000} 46 | } 47 | ) 48 | .getContainerID(); 49 | 50 | dockerClient 51 | .startContainerCmd(containerID) 52 | .exec(); 53 | 54 | HostPortBindings portBindings = DockerUtils.getPublishedHostPortBindings(dockerClient, containerID); 55 | 56 | try { 57 | LuceeUtils.pollForServerIsActive("http://localhost:" + portBindings.http + "/heartbeat.cfm"); 58 | 59 | final var dapClient = new DapUtils.MockClient(); 60 | 61 | final var FIXME_socket_needs_close = new Socket(); 62 | FIXME_socket_needs_close.connect(new InetSocketAddress("localhost", portBindings.dap)); 63 | final var launcher = DSPLauncher.createClientLauncher(dapClient, FIXME_socket_needs_close.getInputStream(), FIXME_socket_needs_close.getOutputStream()); 64 | launcher.startListening(); 65 | final var dapServer = launcher.getRemoteProxy(); 66 | 67 | DapUtils.init(dapServer).join(); 68 | DapUtils.attach(dapServer, Map.of("stepIntoUdfDefaultValueInitFrames", (Object)Boolean.TRUE)).join(); 69 | 70 | DapUtils 71 | .setBreakpoints(dapServer, "/var/www/a.cfm", 10) 72 | .join(); 73 | 74 | final var requestThreadToBeBlockedByBreakpoint = new java.lang.Thread(() -> { 75 | final var requestFactory = new NetHttpTransport().createRequestFactory(); 76 | HttpRequest request; 77 | try { 78 | request = requestFactory.buildGetRequest(new GenericUrl("http://localhost:" + portBindings.http + "/a.cfm")); 79 | request.execute().disconnect(); 80 | } 81 | catch (IOException e) { 82 | throw new RuntimeException(e); 83 | } 84 | }); 85 | 86 | // 87 | // we use stepIn/stepOver arbitrarly, for our purposes in this 88 | // 89 | 90 | final var threadID = DapUtils.doWithStoppedEventFuture( 91 | dapClient, 92 | () -> requestThreadToBeBlockedByBreakpoint.start() 93 | ).get(1000, TimeUnit.MILLISECONDS).getThreadId(); 94 | 95 | { 96 | DapUtils.doWithStoppedEventFuture( 97 | dapClient, 98 | () -> DapUtils.stepIn(dapServer, threadID) 99 | ).get(1000, TimeUnit.MILLISECONDS); 100 | 101 | // a 102 | assertEquals( 103 | 3, 104 | DapUtils 105 | .getStackTrace(dapServer, threadID) 106 | .get(1, TimeUnit.SECONDS) 107 | .getStackFrames()[0] 108 | .getLine() 109 | ); 110 | } 111 | 112 | { 113 | DapUtils.doWithStoppedEventFuture( 114 | dapClient, 115 | () -> DapUtils.stepIn(dapServer, threadID) 116 | ).get(1000, TimeUnit.MILLISECONDS); 117 | 118 | // c (b runs, but we can't seem to stop on it) 119 | assertEquals( 120 | 4, 121 | DapUtils 122 | .getStackTrace(dapServer, threadID) 123 | .get(1, TimeUnit.SECONDS) 124 | .getStackFrames()[0] 125 | .getLine() 126 | ); 127 | } 128 | 129 | { 130 | DapUtils.doWithStoppedEventFuture( 131 | dapClient, 132 | () -> DapUtils.stepIn(dapServer, threadID) 133 | ).get(1000, TimeUnit.MILLISECONDS); 134 | 135 | // e 136 | assertEquals( 137 | 5, 138 | DapUtils 139 | .getStackTrace(dapServer, threadID) 140 | .get(1, TimeUnit.SECONDS) 141 | .getStackFrames()[0] 142 | .getLine() 143 | ); 144 | } 145 | 146 | { 147 | DapUtils.doWithStoppedEventFuture( 148 | dapClient, 149 | () -> DapUtils.stepOver(dapServer, threadID) 150 | ).get(1000, TimeUnit.MILLISECONDS); 151 | 152 | // empty line (would like to not hit this if possible) 153 | assertEquals( 154 | 6, 155 | DapUtils 156 | .getStackTrace(dapServer, threadID) 157 | .get(1, TimeUnit.SECONDS) 158 | .getStackFrames()[0] 159 | .getLine() 160 | ); 161 | } 162 | 163 | { 164 | DapUtils.doWithStoppedEventFuture( 165 | dapClient, 166 | () -> DapUtils.stepOver(dapServer, threadID) 167 | ).get(1000, TimeUnit.MILLISECONDS); 168 | 169 | // function name declaration 170 | assertEquals( 171 | 2, 172 | DapUtils 173 | .getStackTrace(dapServer, threadID) 174 | .get(1, TimeUnit.SECONDS) 175 | .getStackFrames()[0] 176 | .getLine() 177 | ); 178 | 179 | DapUtils.doWithStoppedEventFuture( 180 | dapClient, 181 | () -> DapUtils.stepOver(dapServer, threadID) 182 | ).get(1000, TimeUnit.MILLISECONDS); 183 | 184 | // return statement 185 | assertEquals( 186 | 7, 187 | DapUtils 188 | .getStackTrace(dapServer, threadID) 189 | .get(1, TimeUnit.SECONDS) 190 | .getStackFrames()[0] 191 | .getLine() 192 | ); 193 | } 194 | 195 | { 196 | DapUtils.doWithStoppedEventFuture( 197 | dapClient, 198 | () -> DapUtils.stepOver(dapServer, threadID) 199 | ).get(1000, TimeUnit.MILLISECONDS); 200 | 201 | // back to callsite 202 | assertEquals( 203 | 10, 204 | DapUtils 205 | .getStackTrace(dapServer, threadID) 206 | .get(1, TimeUnit.SECONDS) 207 | .getStackFrames()[0] 208 | .getLine() 209 | ); 210 | } 211 | 212 | DapUtils.disconnect(dapServer).join(); 213 | } 214 | finally { 215 | dockerClient.stopContainerCmd(containerID).exec(); 216 | dockerClient.removeContainerCmd(containerID).exec(); 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/SteppingWorksAsExpectedOnSinglelineStatementWithManySubexpressions.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import org.junit.jupiter.params.ParameterizedTest; 4 | import org.junit.jupiter.params.provider.MethodSource; 5 | 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | import java.io.IOException; 9 | import java.net.InetSocketAddress; 10 | import java.net.Socket; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import com.github.dockerjava.api.DockerClient; 14 | import com.google.api.client.http.GenericUrl; 15 | import com.google.api.client.http.HttpRequest; 16 | import com.google.api.client.http.javanet.NetHttpTransport; 17 | 18 | import luceedebug.testutils.DapUtils; 19 | import luceedebug.testutils.DockerUtils; 20 | import luceedebug.testutils.LuceeUtils; 21 | import luceedebug.testutils.TestParams.LuceeAndDockerInfo; 22 | import luceedebug.testutils.DockerUtils.HostPortBindings; 23 | 24 | import org.eclipse.lsp4j.debug.launch.DSPLauncher; 25 | 26 | class SteppingWorksAsExpectedOnSinglelineStatementWithManySubexpressions { 27 | @ParameterizedTest 28 | @MethodSource("luceedebug.testutils.TestParams#getLuceeAndDockerInfo") 29 | void a(LuceeAndDockerInfo dockerInfo) throws Throwable { 30 | final DockerClient dockerClient = DockerUtils.getDefaultDockerClient(); 31 | 32 | final String imageID = DockerUtils 33 | .buildOrGetImage(dockerClient, dockerInfo.dockerFile) 34 | .getImageID(); 35 | 36 | final String containerID = DockerUtils 37 | .getFreshDefaultContainer( 38 | dockerClient, 39 | imageID, 40 | dockerInfo.luceedebugProjectRoot.toFile(), 41 | dockerInfo.getTestWebRoot("stepping_works_as_expected_on_singleline_statement_with_many_subexpressions"), 42 | new int[][]{ 43 | new int[]{8888,8888}, 44 | new int[]{10000,10000} 45 | } 46 | ) 47 | .getContainerID(); 48 | 49 | dockerClient 50 | .startContainerCmd(containerID) 51 | .exec(); 52 | 53 | HostPortBindings portBindings = DockerUtils.getPublishedHostPortBindings(dockerClient, containerID); 54 | 55 | try { 56 | LuceeUtils.pollForServerIsActive("http://localhost:" + portBindings.http + "/heartbeat.cfm"); 57 | 58 | final var dapClient = new DapUtils.MockClient(); 59 | 60 | final var FIXME_socket_needs_close = new Socket(); 61 | FIXME_socket_needs_close.connect(new InetSocketAddress("localhost", portBindings.dap)); 62 | final var launcher = DSPLauncher.createClientLauncher(dapClient, FIXME_socket_needs_close.getInputStream(), FIXME_socket_needs_close.getOutputStream()); 63 | launcher.startListening(); 64 | final var dapServer = launcher.getRemoteProxy(); 65 | 66 | DapUtils.init(dapServer).join(); 67 | DapUtils.attach(dapServer).join(); 68 | 69 | DapUtils 70 | .setBreakpoints(dapServer, "/var/www/a.cfm", 6) 71 | .join(); 72 | 73 | final var requestThreadToBeBlockedByBreakpoint = new java.lang.Thread(() -> { 74 | final var requestFactory = new NetHttpTransport().createRequestFactory(); 75 | HttpRequest request; 76 | try { 77 | request = requestFactory.buildGetRequest(new GenericUrl("http://localhost:" + portBindings.http + "/a.cfm")); 78 | request.execute().disconnect(); 79 | } 80 | catch (IOException e) { 81 | throw new RuntimeException(e); 82 | } 83 | }); 84 | 85 | final var threadID = DapUtils.doWithStoppedEventFuture(dapClient, () -> { 86 | requestThreadToBeBlockedByBreakpoint.start(); 87 | }) 88 | .get(1000, TimeUnit.MILLISECONDS) 89 | .getThreadId(); 90 | 91 | // 92 | // ^N is "^" points at expected column (though we don't get column info) and "N" is frame number 93 | // ^1 is at top of stack, ^2 is the frame below it, ... 94 | // 95 | 96 | { 97 | // foo(n) { ... } 98 | // 99 | // foo(1).foo(2).foo(3).foo(4); 100 | // ^1 101 | final var frames = DapUtils 102 | .getStackTrace(dapServer, threadID) 103 | .join() 104 | .getStackFrames(); 105 | assertEquals(1, frames.length); 106 | assertEquals("??", frames[0].getName()); 107 | assertEquals(6, frames[0].getLine()); 108 | } 109 | 110 | DapUtils.doWithStoppedEventFuture( 111 | dapClient, 112 | () -> DapUtils.stepIn(dapServer, threadID).join() 113 | ).get(1000, TimeUnit.MILLISECONDS); 114 | 115 | { 116 | // foo(n) { ... } 117 | // ^1 118 | // foo(1).foo(2).foo(3).foo(4); 119 | // ^2 120 | final var frames = DapUtils 121 | .getStackTrace(dapServer, threadID) 122 | .join() 123 | .getStackFrames(); 124 | assertEquals(2, frames.length); 125 | assertEquals("FOO", frames[0].getName()); 126 | assertEquals(2, frames[0].getLine()); 127 | } 128 | 129 | DapUtils.doWithStoppedEventFuture( 130 | dapClient, 131 | () -> DapUtils.stepOut(dapServer, threadID).join() 132 | ).get(1000, TimeUnit.MILLISECONDS); 133 | 134 | { 135 | // foo(n) { ... } 136 | // 137 | // foo(1).foo(2).foo(3).foo(4); 138 | // ^1 (out-but-not-yet-stepped) 139 | final var frames = DapUtils 140 | .getStackTrace(dapServer, threadID) 141 | .join() 142 | .getStackFrames(); 143 | assertEquals(1, frames.length); 144 | assertEquals("??", frames[0].getName()); 145 | assertEquals(6, frames[0].getLine()); 146 | } 147 | 148 | DapUtils.doWithStoppedEventFuture( 149 | dapClient, 150 | () -> DapUtils.stepIn(dapServer, threadID).join() 151 | ).get(1000, TimeUnit.MILLISECONDS); 152 | 153 | { 154 | // foo(n) { ... } 155 | // ^1 156 | // foo(1).foo(2).foo(3).foo(4); 157 | // ^2 158 | final var frames = DapUtils 159 | .getStackTrace(dapServer, threadID) 160 | .join() 161 | .getStackFrames(); 162 | assertEquals(2, frames.length); 163 | assertEquals("foo", frames[0].getName(), "'foo' instead of 'FOO', different case the second time around"); 164 | assertEquals(2, frames[0].getLine()); 165 | } 166 | 167 | DapUtils.doWithStoppedEventFuture( 168 | dapClient, 169 | () -> DapUtils.stepOut(dapServer, threadID).join() 170 | ).get(1000, TimeUnit.MILLISECONDS); 171 | 172 | { 173 | // foo(n) { ... } 174 | // 175 | // foo(1).foo(2).foo(3).foo(4); 176 | // ^ (out-but-not-yet-stepped) 177 | final var frames = DapUtils 178 | .getStackTrace(dapServer, threadID) 179 | .join() 180 | .getStackFrames(); 181 | assertEquals(1, frames.length); 182 | assertEquals("??", frames[0].getName()); 183 | assertEquals(6, frames[0].getLine()); 184 | } 185 | 186 | DapUtils.doWithStoppedEventFuture( 187 | dapClient, 188 | () -> DapUtils.stepOver(dapServer, threadID).join() 189 | ).get(1000, TimeUnit.MILLISECONDS); 190 | 191 | { 192 | // foo(n) { ... } 193 | // 194 | // foo(1).foo(2).foo(3).foo(4); 195 | // ^1 196 | final var frames = DapUtils 197 | .getStackTrace(dapServer, threadID) 198 | .join() 199 | .getStackFrames(); 200 | assertEquals(1, frames.length); 201 | assertEquals("??", frames[0].getName()); 202 | assertEquals(6, frames[0].getLine()); 203 | } 204 | 205 | DapUtils.doWithStoppedEventFuture( 206 | dapClient, 207 | () -> DapUtils.stepOver(dapServer, threadID).join() 208 | ).get(1000, TimeUnit.MILLISECONDS); 209 | 210 | { 211 | // foo(n) { ... } 212 | // 213 | // foo(1).foo(2).foo(3).foo(4); 214 | // ^1 215 | final var frames = DapUtils 216 | .getStackTrace(dapServer, threadID) 217 | .join() 218 | .getStackFrames(); 219 | assertEquals(1, frames.length); 220 | assertEquals("??", frames[0].getName()); 221 | assertEquals(6, frames[0].getLine()); 222 | } 223 | 224 | DapUtils.doWithStoppedEventFuture( 225 | dapClient, 226 | () -> DapUtils.stepOver(dapServer, threadID).join() 227 | ).get(1000, TimeUnit.MILLISECONDS); 228 | 229 | { 230 | // foo(n) { ... } 231 | // 232 | // foo(1).foo(2).foo(3).foo(4); 233 | // 234 | // ^1 235 | final var frames = DapUtils 236 | .getStackTrace(dapServer, threadID) 237 | .join() 238 | .getStackFrames(); 239 | assertEquals(1, frames.length); 240 | assertEquals("??", frames[0].getName()); 241 | assertEquals(7, frames[0].getLine()); 242 | } 243 | 244 | DapUtils 245 | .disconnect(dapServer) 246 | .join(); 247 | 248 | // DapUtils.getStackTrace(dapServer, threadID).join().getStackFrames()[0]; 249 | } 250 | finally { 251 | dockerClient.stopContainerCmd(containerID).exec(); 252 | dockerClient.removeContainerCmd(containerID).exec(); 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/StepsToFinallyAndThenCatchSkippingPastUnwoundLines.java: -------------------------------------------------------------------------------- 1 | package luceedebug; 2 | 3 | import org.junit.jupiter.params.ParameterizedTest; 4 | import org.junit.jupiter.params.provider.MethodSource; 5 | 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | import java.io.IOException; 9 | import java.net.InetSocketAddress; 10 | import java.net.Socket; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import com.github.dockerjava.api.DockerClient; 14 | import com.google.api.client.http.GenericUrl; 15 | import com.google.api.client.http.HttpRequest; 16 | import com.google.api.client.http.javanet.NetHttpTransport; 17 | 18 | import luceedebug.testutils.DapUtils; 19 | import luceedebug.testutils.DockerUtils; 20 | import luceedebug.testutils.LuceeUtils; 21 | import luceedebug.testutils.TestParams.LuceeAndDockerInfo; 22 | import luceedebug.testutils.DockerUtils.HostPortBindings; 23 | 24 | import org.eclipse.lsp4j.debug.launch.DSPLauncher; 25 | 26 | class StepsToFinallyAndThenCatchSkippingPastUnwoundLines { 27 | @ParameterizedTest 28 | @MethodSource("luceedebug.testutils.TestParams#getLuceeAndDockerInfo") 29 | void a(LuceeAndDockerInfo dockerInfo) throws Throwable { 30 | final DockerClient dockerClient = DockerUtils.getDefaultDockerClient(); 31 | 32 | final String imageID = DockerUtils 33 | .buildOrGetImage(dockerClient, dockerInfo.dockerFile) 34 | .getImageID(); 35 | 36 | final String containerID = DockerUtils 37 | .getFreshDefaultContainer( 38 | dockerClient, 39 | imageID, 40 | dockerInfo.luceedebugProjectRoot.toFile(), 41 | dockerInfo.getTestWebRoot("step_to_catch_block"), 42 | new int[][]{ 43 | new int[]{8888,8888}, 44 | new int[]{10000,10000} 45 | } 46 | ) 47 | .getContainerID(); 48 | 49 | dockerClient 50 | .startContainerCmd(containerID) 51 | .exec(); 52 | 53 | HostPortBindings portBindings = DockerUtils.getPublishedHostPortBindings(dockerClient, containerID); 54 | 55 | try { 56 | LuceeUtils.pollForServerIsActive("http://localhost:" + portBindings.http + "/heartbeat.cfm"); 57 | 58 | final var dapClient = new DapUtils.MockClient(); 59 | 60 | final var FIXME_socket_needs_close = new Socket(); 61 | FIXME_socket_needs_close.connect(new InetSocketAddress("localhost", portBindings.dap)); 62 | final var launcher = DSPLauncher.createClientLauncher(dapClient, FIXME_socket_needs_close.getInputStream(), FIXME_socket_needs_close.getOutputStream()); 63 | launcher.startListening(); 64 | final var dapServer = launcher.getRemoteProxy(); 65 | 66 | DapUtils.init(dapServer).join(); 67 | DapUtils.attach(dapServer).join(); 68 | 69 | DapUtils 70 | .setBreakpoints(dapServer, "/var/www/a.cfm", 29) 71 | .join(); 72 | 73 | final var requestThreadToBeBlockedByBreakpoint = new java.lang.Thread(() -> { 74 | final var requestFactory = new NetHttpTransport().createRequestFactory(); 75 | HttpRequest request; 76 | try { 77 | request = requestFactory.buildGetRequest(new GenericUrl("http://localhost:" + portBindings.http + "/a.cfm")); 78 | request.execute().disconnect(); 79 | } 80 | catch (IOException e) { 81 | throw new RuntimeException(e); 82 | } 83 | }); 84 | 85 | final var threadID = DapUtils.doWithStoppedEventFuture( 86 | dapClient, 87 | () -> requestThreadToBeBlockedByBreakpoint.start() 88 | ).get(1000, TimeUnit.MILLISECONDS).getThreadId(); 89 | 90 | { 91 | DapUtils.doWithStoppedEventFuture( 92 | dapClient, 93 | () -> DapUtils.stepIn(dapServer, threadID) 94 | ).get(1000, TimeUnit.MILLISECONDS); 95 | 96 | // finally { <<< 97 | // 0+0; 98 | // } 99 | assertEquals( 100 | 19, 101 | DapUtils 102 | .getStackTrace(dapServer, threadID) 103 | .get(1, TimeUnit.SECONDS) 104 | .getStackFrames()[0] 105 | .getLine() 106 | ); 107 | } 108 | 109 | { 110 | DapUtils.doWithStoppedEventFuture( 111 | dapClient, 112 | () -> DapUtils.stepIn(dapServer, threadID) 113 | ).get(1000, TimeUnit.MILLISECONDS); 114 | 115 | // finally { 116 | // 0+0; <<< 117 | // } 118 | assertEquals( 119 | 20, 120 | DapUtils 121 | .getStackTrace(dapServer, threadID) 122 | .get(1, TimeUnit.SECONDS) 123 | .getStackFrames()[0] 124 | .getLine() 125 | ); 126 | } 127 | 128 | { 129 | DapUtils.doWithStoppedEventFuture( 130 | dapClient, 131 | () -> DapUtils.stepIn(dapServer, threadID) 132 | ).get(1000, TimeUnit.MILLISECONDS); 133 | 134 | // catch (any e) { <<< 135 | // 0+0; 136 | // } 137 | assertEquals( 138 | 6, 139 | DapUtils 140 | .getStackTrace(dapServer, threadID) 141 | .get(1, TimeUnit.SECONDS) 142 | .getStackFrames()[0] 143 | .getLine() 144 | ); 145 | } 146 | 147 | { 148 | DapUtils.doWithStoppedEventFuture( 149 | dapClient, 150 | () -> DapUtils.stepIn(dapServer, threadID) 151 | ).get(1000, TimeUnit.MILLISECONDS); 152 | 153 | // catch (any e) { 154 | // 0+0; <<< 155 | // } 156 | assertEquals( 157 | 7, 158 | DapUtils 159 | .getStackTrace(dapServer, threadID) 160 | .get(1, TimeUnit.SECONDS) 161 | .getStackFrames()[0] 162 | .getLine() 163 | ); 164 | } 165 | 166 | DapUtils.disconnect(dapServer).join(); 167 | } 168 | finally { 169 | dockerClient.stopContainerCmd(containerID).exec(); 170 | dockerClient.removeContainerCmd(containerID).exec(); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/testutils/DapUtils.java: -------------------------------------------------------------------------------- 1 | package luceedebug.testutils; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.function.Consumer; 8 | 9 | import org.eclipse.lsp4j.debug.*; 10 | import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; 11 | import org.eclipse.lsp4j.debug.services.IDebugProtocolServer; 12 | 13 | public class DapUtils { 14 | /** 15 | * This does not work recursively, and requires that exactly and only a single stop event (i.e. the target stop event) 16 | * be fired during the wait on the returned future. 17 | * 18 | * We might want to await this here, rather than allow the caller to do so, where if they forget to wait it's likely a bug. 19 | * 20 | * "do some work that should trigger the debugee to soon (microseconds) emit a stop event and return a future that resolves on receipt of that stopped event" 21 | */ 22 | public static CompletableFuture doWithStoppedEventFuture(MockClient client, Runnable f) { 23 | final var future = new CompletableFuture(); 24 | client.stopped_handler = stoppedEventArgs -> { 25 | client.stopped_handler = null; // concurrency issues? Callers should be synchronous with respect to this action though. 26 | future.complete(stoppedEventArgs); 27 | }; 28 | f.run(); 29 | return future; 30 | }; 31 | 32 | public static CompletableFuture setBreakpoints( 33 | IDebugProtocolServer dapServer, 34 | String filename, 35 | int ...lines 36 | ) { 37 | var source = new Source(); 38 | source.setPath(filename); 39 | 40 | var breakpoints = new ArrayList(); 41 | for (var line : lines) { 42 | var bp = new SourceBreakpoint(); 43 | bp.setLine(line); 44 | breakpoints.add(bp); 45 | } 46 | 47 | var breakpointsArgs = new SetBreakpointsArguments(); 48 | 49 | breakpointsArgs.setSource(source); 50 | breakpointsArgs.setBreakpoints( 51 | breakpoints.toArray(new SourceBreakpoint[0]) 52 | ); 53 | 54 | return dapServer.setBreakpoints(breakpointsArgs); 55 | } 56 | 57 | public static CompletableFuture init(IDebugProtocolServer dapServer) { 58 | var initArgs = new InitializeRequestArguments(); 59 | initArgs.setClientID("test"); 60 | return dapServer.initialize(initArgs); 61 | } 62 | 63 | public static CompletableFuture attach(IDebugProtocolServer dapServer) { 64 | return attach(dapServer, new HashMap()); 65 | } 66 | 67 | public static CompletableFuture attach(IDebugProtocolServer dapServer, Map config) { 68 | return dapServer.attach(config); 69 | } 70 | 71 | public static CompletableFuture getStackTrace(IDebugProtocolServer dapServer, int threadID) { 72 | var stackTraceArgs = new StackTraceArguments(); 73 | stackTraceArgs.setThreadId(threadID); 74 | return dapServer.stackTrace(stackTraceArgs); 75 | } 76 | 77 | public static CompletableFuture getScopes(IDebugProtocolServer dapServer, int frameID) { 78 | var scopesArgs = new ScopesArguments(); 79 | scopesArgs.setFrameId(frameID); 80 | return dapServer.scopes(scopesArgs); 81 | } 82 | 83 | public static CompletableFuture getVariables(IDebugProtocolServer dapServer, Scope scope) { 84 | return getVariables(dapServer, scope.getVariablesReference()); 85 | } 86 | 87 | public static CompletableFuture getVariables(IDebugProtocolServer dapServer, int variableID) { 88 | var variablesArgs = new VariablesArguments(); 89 | variablesArgs.setVariablesReference(variableID); 90 | return dapServer.variables(variablesArgs); 91 | } 92 | 93 | public static CompletableFuture continue_(IDebugProtocolServer dapServer, int threadID) { 94 | var continueArgs = new ContinueArguments(); 95 | continueArgs.setThreadId(threadID); 96 | return dapServer.continue_(continueArgs); 97 | } 98 | 99 | public static CompletableFuture disconnect(IDebugProtocolServer dapServer) { 100 | return dapServer.disconnect(new DisconnectArguments()); 101 | } 102 | 103 | public static CompletableFuture stepIn(IDebugProtocolServer dapServer, int threadID) { 104 | var args = new StepInArguments(); 105 | args.setThreadId(threadID); 106 | return dapServer.stepIn(args); 107 | } 108 | 109 | public static CompletableFuture stepOut(IDebugProtocolServer dapServer, int threadID) { 110 | var args = new StepOutArguments(); 111 | args.setThreadId(threadID); 112 | return dapServer.stepOut(args); 113 | } 114 | 115 | public static CompletableFuture stepOver(IDebugProtocolServer dapServer, int threadID) { 116 | var args = new NextArguments(); 117 | args.setThreadId(threadID); 118 | return dapServer.next(args); 119 | } 120 | 121 | public static CompletableFuture evaluate(IDebugProtocolServer dapServer, int frameID, String expr) { 122 | var args = new EvaluateArguments(); 123 | args.setFrameId(frameID); 124 | args.setExpression(expr); 125 | return dapServer.evaluate(args); 126 | } 127 | 128 | public static class MockClient implements IDebugProtocolClient { 129 | public void breakpoint(BreakpointEventArguments args) { 130 | 131 | } 132 | public void continued(ContinuedEventArguments args) { 133 | 134 | } 135 | public void exited(ExitedEventArguments args) { 136 | 137 | } 138 | public void initialized() { 139 | 140 | } 141 | public void loadedSource(LoadedSourceEventArguments args) { 142 | 143 | } 144 | public void module(ModuleEventArguments args) { 145 | 146 | } 147 | public void output(OutputEventArguments args) { 148 | 149 | } 150 | public void process(ProcessEventArguments args) { 151 | 152 | } 153 | 154 | public Consumer stopped_handler = null; 155 | public void stopped(StoppedEventArguments args) { 156 | if (stopped_handler != null) { 157 | stopped_handler.accept(args); 158 | } 159 | } 160 | 161 | public void terminated(TerminatedEventArguments args) { 162 | 163 | } 164 | public void thread(ThreadEventArguments args) { 165 | 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/testutils/DockerUtils.java: -------------------------------------------------------------------------------- 1 | package luceedebug.testutils; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.nio.charset.StandardCharsets; 7 | import java.nio.file.Files; 8 | import java.nio.file.Paths; 9 | import java.time.Duration; 10 | import java.util.ArrayList; 11 | import java.util.Map; 12 | 13 | import com.github.dockerjava.api.DockerClient; 14 | import com.github.dockerjava.api.model.Bind; 15 | import com.github.dockerjava.api.model.ExposedPort; 16 | import com.github.dockerjava.api.model.HostConfig; 17 | import com.github.dockerjava.api.model.NetworkSettings; 18 | import com.github.dockerjava.api.model.PortBinding; 19 | import com.github.dockerjava.api.model.Ports; 20 | import com.github.dockerjava.api.model.Volume; 21 | import com.github.dockerjava.api.model.Ports.Binding; 22 | import com.github.dockerjava.core.DefaultDockerClientConfig; 23 | import com.github.dockerjava.core.DockerClientConfig; 24 | import com.github.dockerjava.core.DockerClientImpl; 25 | import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; 26 | 27 | import luceedebug.generated.Constants; 28 | 29 | public class DockerUtils { 30 | public static DockerClient getDefaultDockerClient() { 31 | DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); 32 | var httpClient = new ApacheDockerHttpClient.Builder() 33 | .dockerHost(config.getDockerHost()) 34 | .sslConfig(config.getSSLConfig()) 35 | .maxConnections(100) 36 | .connectionTimeout(Duration.ofSeconds(30)) 37 | .responseTimeout(Duration.ofSeconds(45)) 38 | .build(); 39 | return DockerClientImpl.getInstance(config, httpClient); 40 | } 41 | 42 | /** 43 | * TODO: should be a record but we'd need to setup tooling for tests to be for java14, 44 | * while the main lib stays on 11. 45 | */ 46 | public static class ImageID { 47 | private final String imageID_; 48 | ImageID(String imageID) { 49 | this.imageID_ = imageID; 50 | } 51 | public String getImageID() { return imageID_; } 52 | } 53 | 54 | @SuppressWarnings("unchecked") 55 | static T rethrowUnchecked(Object e) throws T { 56 | throw (T) e; 57 | } 58 | 59 | /** 60 | * The dockerFile needs some string replacements before we send it to the dockerClient to run. 61 | * We transform it and write the results to a temp file, then offer up the temp file path to docker. 62 | */ 63 | static File mungeDockerfile(File dockerFile) { 64 | try { 65 | var s = new String(Files.readAllBytes(dockerFile.toPath())) 66 | .replaceAll("@LUCEEDEBUG_JAR", "luceedebug-" + Constants.version + ".jar"); 67 | var f = File.createTempFile("luceedebug-testing-", "", Paths.get("../test/scratch").toAbsolutePath().normalize().toFile()); 68 | f.deleteOnExit(); 69 | var bytes = s.getBytes(StandardCharsets.UTF_8); 70 | Files.write(f.toPath(), bytes); 71 | return f; 72 | } 73 | catch (Throwable e) { 74 | rethrowUnchecked(e); 75 | return null; 76 | } 77 | } 78 | 79 | /** 80 | * Docker won't actually build a new image if the dockerfile hasn't changed, is that right? 81 | * That would be the desireable behavior. 82 | */ 83 | public static ImageID buildOrGetImage(DockerClient dockerClient, File dockerFile) { 84 | return new ImageID( 85 | dockerClient 86 | .buildImageCmd(mungeDockerfile(dockerFile)) 87 | .start() 88 | .awaitImageId() 89 | ); 90 | } 91 | 92 | public static class ContainerID { 93 | private final String containerID_; 94 | ContainerID(String containerID) { 95 | this.containerID_ = containerID; 96 | } 97 | public String getContainerID() { return containerID_; } 98 | } 99 | 100 | public static ContainerID getFreshDefaultContainer( 101 | DockerClient dockerClient, 102 | String imageID, 103 | File projectRoot, 104 | File luceeTestAppRoot, 105 | int[][] portMappingPairs 106 | ) { 107 | var portBindings = new ArrayList(); 108 | var exposedPorts = new ArrayList(); 109 | 110 | for (var pair : portMappingPairs) { 111 | var host = pair[0]; 112 | var container = pair[0]; 113 | portBindings.add(new PortBinding(new Binding(null, String.valueOf(host)), new ExposedPort(container))); 114 | exposedPorts.add(new ExposedPort(container)); 115 | } 116 | 117 | var hostConfig = new HostConfig(); 118 | hostConfig.withPublishAllPorts(true); 119 | 120 | hostConfig.setBinds( 121 | new Bind( 122 | Paths.get(projectRoot.toString(), "build/libs/").toString(), 123 | new Volume("/build/") 124 | ), 125 | new Bind( 126 | luceeTestAppRoot.toString(), 127 | new Volume("/var/www/") 128 | ) 129 | ); 130 | 131 | 132 | // hostConfig.withPortBindings(portBindings); 133 | 134 | return new ContainerID( 135 | dockerClient 136 | .createContainerCmd(imageID) 137 | .withExposedPorts(exposedPorts) 138 | .withHostConfig(hostConfig) 139 | .exec() 140 | .getId() 141 | ); 142 | } 143 | 144 | public static class HostPortBindings { 145 | public final int http; 146 | public final int dap; 147 | HostPortBindings(int http, int dap) { 148 | this.http = http; 149 | this.dap = dap; 150 | } 151 | } 152 | 153 | /** 154 | * this is hardcoded to assume we've already bound the ports on the container to particular magic numbers, 155 | * which should be fixed before we go too much further. 156 | */ 157 | public static HostPortBindings getPublishedHostPortBindings(DockerClient dockerClient, String containerID) { 158 | NetworkSettings networkSettings = dockerClient 159 | .inspectContainerCmd(containerID) 160 | .exec() 161 | .getNetworkSettings(); 162 | 163 | Ports portBindings = networkSettings.getPorts(); 164 | Map bindings = portBindings.getBindings(); 165 | 166 | int http = -1; 167 | int dap = -1; 168 | for (var entry : bindings.entrySet()) { 169 | int containerPort = entry.getKey().getPort(); 170 | int hostPort = Integer.parseInt(entry.getValue()[0].getHostPortSpec()); 171 | if (containerPort == 8888) { 172 | http = hostPort; 173 | } 174 | else if (containerPort == 10000) { 175 | dap = hostPort; 176 | } 177 | } 178 | 179 | if (http == -1 || dap == -1) { 180 | throw new RuntimeException("couldn't determine host<->container port bindings"); 181 | } 182 | 183 | return new HostPortBindings(http, dap); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/testutils/LuceeUtils.java: -------------------------------------------------------------------------------- 1 | package luceedebug.testutils; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.io.IOException; 7 | import java.net.SocketException; 8 | 9 | import com.google.api.client.http.GenericUrl; 10 | import com.google.api.client.http.HttpRequest; 11 | import com.google.api.client.http.HttpRequestFactory; 12 | import com.google.api.client.http.HttpResponse; 13 | import com.google.api.client.http.javanet.NetHttpTransport; 14 | 15 | public class LuceeUtils { 16 | public static void pollForServerIsActive(String url) throws IOException, InterruptedException { 17 | HttpRequestFactory requestFactory = new NetHttpTransport().createRequestFactory(); 18 | boolean serverUp = false; 19 | for (int i = 0; i < 100; i++) { 20 | HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url)); 21 | try { 22 | HttpResponse response = request.execute(); 23 | try { 24 | assertEquals("OK", response.parseAsString()); 25 | serverUp = true; 26 | } 27 | finally { 28 | response.disconnect(); 29 | } 30 | } 31 | catch (SocketException s) { 32 | // discard, server's not serving yet 33 | Thread.sleep(25); 34 | } 35 | } 36 | assertTrue(serverUp, "server is up"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/testutils/TestParams.java: -------------------------------------------------------------------------------- 1 | package luceedebug.testutils; 2 | 3 | import java.io.File; 4 | import java.nio.file.Path; 5 | import java.nio.file.Paths; 6 | 7 | public class TestParams { 8 | public static class LuceeAndDockerInfo { 9 | // we'll probably eventually need to major/minor/patch info, but this is good enough for current needs 10 | public final int engineVersion; 11 | public final Path luceedebugProjectRoot = Paths.get("").toAbsolutePath(); 12 | public final File dockerFile; 13 | 14 | LuceeAndDockerInfo(int engineVersion, String projectRelativeDockerRoot) { 15 | this.engineVersion = engineVersion; 16 | Path v = luceedebugProjectRoot.resolve(projectRelativeDockerRoot).normalize(); 17 | this.dockerFile = v.resolve("Dockerfile").toFile(); 18 | } 19 | 20 | public File getTestWebRoot(String webRoot) { 21 | File f = luceedebugProjectRoot.resolve("../test/docker/" + webRoot).normalize().toFile(); 22 | assert f.exists() : "No such file: '" + f + "'"; 23 | return f; 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "{engineVersion=" + engineVersion + ", dockerFile=" + dockerFile + "}"; 29 | } 30 | } 31 | 32 | public static LuceeAndDockerInfo[] getLuceeAndDockerInfo() { 33 | return new LuceeAndDockerInfo[] { 34 | new LuceeAndDockerInfo(5, "../test/docker/5.3.10.120"), 35 | new LuceeAndDockerInfo(6, "../test/docker/6.1.0.243") 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /luceedebug/src/test/java/luceedebug/testutils/Utils.java: -------------------------------------------------------------------------------- 1 | package luceedebug.testutils; 2 | 3 | public class Utils { 4 | public static T unreachable() { 5 | throw new RuntimeException("unreachable"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # luceedebug 2 | 3 | luceedebug is a step debugger for Lucee. 4 | 5 | ![misc. features of a debug session indicating that luceedebug is a step debugger for Lucee.](assets/whatisit.png) 6 | 7 | There are two components: 8 | 9 | - A Java agent 10 | - A VS Code extension 11 | 12 | The java agent needs a particular invocation and needs to be run as part of the JVM/Lucee server startup. 13 | 14 | The VS Code client extension is available as `luceedebug` when searching in the VS Code extensions pane (or it can be built locally, see subsequent instructions). 15 | 16 | ## Java Agent 17 | Min supported JDK is JDK11. Building requires JDK17 or higher. 18 | 19 | Built jars are available via github 'releases'. The most recent build is https://github.com/softwareCobbler/luceedebug/releases/latest 20 | 21 | ### Build Agent Jar 22 | 23 | The following steps will build to: `./luceedebug/build/libs/luceedebug.jar` 24 | 25 | #### Build Agent Jar on Mac / Linux 26 | 27 | ``` 28 | cd luceedebug 29 | ./gradlew shadowjar 30 | ``` 31 | 32 | #### Build Agent Jar on Windows 33 | 34 | ``` 35 | cd luceedebug 36 | gradlew.bat shadowjar 37 | ``` 38 | 39 | ### Install and Configure Agent 40 | 41 | Note that you must be running a JDK version of your java release (a common error on startup when running "just" a JRE is `java.lang.NoClassDefFoundError: com/sun/jdi/Bootstrap`). 42 | 43 | Add the following to your java invocation. (Tomcat users can use the `setenv.sh` file for this purpose.) 44 | 45 | ``` 46 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:9999 47 | 48 | -javaagent:/abspath/to/luceedebug.jar=jdwpHost=localhost,jdwpPort=9999,debugHost=0.0.0.0,debugPort=10000,jarPath=/abspath/to/luceedebug.jar 49 | ``` 50 | 51 | * `agentlib`: Configures JDWP, which is the lower-level Java debugging protocol that the luceedebug agent connects with. (Note: The VS Code debugger connects to the _luceedebug_ agent, not JDWP, so JDWP/`agentlib` usually doesn't need to be modified/customized.) 52 | * `address`: Leave this as `localhost:9999`, unless you have a compelling reason to change it (e.g., if some other service is already listening on port 9999). 53 | * All other arguments should be used verbatim unless you have a compelling reason to change them. 54 | * `javaagent`: Configures the luceedebug agent, itself. 55 | * `/abspath/to/luceedebug.jar` (the first token in the `javaagent`): The absolute path by which your server can find the luceedebug agent library. You must change this to match your environment. 56 | * `jdwpHost`/`jdwpPort`: The luceedebug agent connects to JDWP via this host/port. These values must match those in `agentlib`'s `address`. 57 | * `debugHost`/`debugPort`: These configure the host/port that the VS Code debugger attaches to. 58 | 59 | Set this to the interface on which you want the debugger to listen. In non-docker environments, this would be the IP address of a particular interface. 60 | 61 | If Lucee is running in a docker container, the `debugHost` _must_ be `0.0.0.0` (i.e., "listen on all interfaces"). However, be careful not to use this value on a publicly-accessible, unprotected server, as you could expose the debugger to the public (which would be a major security vulnerability). 62 | * `jarPath`: This value must be identical to the first token in the `javaagent` arguments. Unfortunately, we have to specify the path twice! One tells the JVM which jar to use as a java agent, the second is an argument specifying from where the java agent will load debugging instrumentation. 63 | 64 | (There didn't seem to be an immediately obvious way to pull the name of "the current" jar file from an agent's `premain`, but maybe it's just been overlooked. If you know let us know!) 65 | 66 | ### VS Code luceedebug Debugger Extension 67 | 68 | #### Install and Run from VS Code Marketplace 69 | 70 | The VS Code luceedebug extension is available on the VS Code Marketplace. If you are an end-user who just wants to start debugging your CFML, install the luceedebug extension from the Marketplace. 71 | 72 | ##### Run the Extension 73 | 74 | - Go to the "run and debug" menu (looks like a bug with a play button) 75 | - Add a CFML debug configuration (if you haven't already--it only needs to be done once): Run > Open Configurations. (See the [configuration example, below](#vs-code-extension-configuration).) 76 | - Attach to the Lucee server 77 | - With a CFML file open, click the "Run and Debug" icon in the left menu. 78 | - In the select list labeled "Run and Debug," choose the name of the configuration you used in the `name` key of the debug configuration. (In the [configuration example, below](#vs-code-extension-configuration), it would be `Project A`.) 79 | - Click the green "play" icon next to the select list, above. 80 | - General step debugging is documented [here](https://code.visualstudio.com/docs/editor/debugging), but the following is a synopsis. 81 | - With a CFML file open, click in the margin to the left of the line number, which will set a breakpoint (represented by a red dot). 82 | - Use your application in a way that would reach that line of code. 83 | - The application will pause execution at that line of code and allow you to inspect the current state. 84 | - The debug navigation buttons will allow you to continue execution or step into and out of functions, etc. 85 | 86 | #### Hacking the luceedebug Extension 87 | 88 | If you want to hack the extension, itself, build/run instructions follow. 89 | 90 | ##### Build Extension 91 | 92 | Prerequisites: 93 | * `npm` 94 | * `typescript` 95 | * Mac: `brew install typescript` 96 | 97 | ``` 98 | # vs code client 99 | cd vscode-client 100 | npm install 101 | 102 | npm run build-dev-windows # windows 103 | npm run build-dev-linux # mac/linux 104 | ``` 105 | 106 | ##### Run the Self-Built Extension 107 | 108 | Steps to run the extension in VS Code's "extension development host": 109 | - Open VS Code in this dir 110 | ``` 111 | cd vscode-client 112 | code . # open vs code in this dir 113 | ``` 114 | - Go to the "run and debug" menu (looks like a bug with a play button) 115 | - In the select list labeled "Run and Debug," choose the "Launch luceedebug in Extension Development Host" option and click the green "play" icon to launch. 116 | - The extension development host window opens 117 | - Load your Lucee project from that VS Code instance 118 | - Continue on to [Run the Extension](#run-the-extension) 119 | 120 | 121 | ### VS Code Extension Configuration 122 | 123 | A CFML debug configuration looks like: 124 | ```json5 125 | { 126 | "type": "cfml", 127 | "request": "attach", 128 | "name": "Project A", 129 | "hostName": "localhost", 130 | "port": 10000, 131 | // optional; only necessary when ide and lucee paths don't match 132 | "pathTransforms": [ 133 | { 134 | "idePrefix": "${workspaceFolder}", 135 | "serverPrefix": "/app" 136 | } 137 | ], 138 | // optional; controls how paths returned from the debugger are normalized in the client. 139 | // options: 140 | // "none" - (default) no normalization; use paths exactly as returned from the debugger 141 | // "auto" - use the platform default (e.g., "/" on macOS/Linux, "\" on Windows) 142 | // "posix" - always use forward slashes ("/") 143 | // "windows" - always use backslashes ("\") 144 | "pathSeparator": "auto" 145 | } 146 | ``` 147 | `hostName`/`port` should match the `debugHost`/`debugPort` of the Java agent's configuration. (There are exceptions; e.g., on remote hosts where DNS and/or port forwarding are in play.) 148 | 149 | Use the `pathSeparator` option to control how file paths returned from the server are interpreted on your client machine. This is useful when debugging across different operating systems or dealing with platform-specific path formats. 150 | 151 | #### Mapping Paths with `pathTransforms` 152 | 153 | 154 | `pathTransforms` maps between "IDE paths" and "Lucee server paths". For example, in your editor, you may be working on a file called `/foo/bar/baz/TheThing.cfc`, but it runs in a container and Lucee sees it as `/serverAppRoot/bar/baz/TheThing.cfc`. 155 | 156 | In the case of local debugging (when there are no virtual machines or containers involved), you may not need a `pathTransforms` configuration, because both your IDE and Lucee probably know any given CFML file by the same path name. 157 | 158 | However, in environments where the IDE path of a CFML file isn't identical to the Lucee path, luceedebug needs to know how to transform these paths. 159 | 160 | Currently, it is a simple prefix replacement, e.g.: 161 | 162 | ```json 163 | "pathTransforms": [ 164 | { 165 | "idePrefix": "/foo", 166 | "serverPrefix": "/serverAppRoot" 167 | } 168 | ] 169 | ``` 170 | 171 | In the above example, the IDE would announce, "set a breakpoint in `/foo/bar/baz/TheThing.cfc`, which the server will understand as "set a breakpoint in `/serverAppRoot/bar/baz/TheThing.cfc`". 172 | 173 | Omitting `pathTransforms` means no path transformation will take place. (It can be omitted when IDE paths match server paths.) 174 | 175 | Multiple `pathTransforms` may be specified if more than one mapping is needed. The first match wins. 176 | 177 | Example: 178 | 179 | ```json 180 | "pathTransforms": [ 181 | { 182 | "idePrefix": "/Users/sc/projects/subapp_b_helper", 183 | "serverPrefix": "/var/www/subapp/b/helper" 184 | }, 185 | { 186 | "idePrefix": "/Users/sc/projects/subapp_b", 187 | "serverPrefix": "/var/www/subapp/b" 188 | }, 189 | { 190 | "idePrefix": "/Users/sc/projects/app", 191 | "serverPrefix": "/var/www" 192 | } 193 | ] 194 | ``` 195 | 196 | In this example: 197 | 198 | * A breakpoint set on `/Users/sc/projects/app/Application.cfc` will match the last transform and map to `/var/www/Application.cfc` on the server. 199 | * A breakpoint set on `/Users/sc/projects/subapp_b_helper/HelpUtil.cfc` will match the first transform and map to `/var/www/subapp/b/helper/HelpUtil.cfc` on the server. 200 | 201 | --- 202 | ## Misc. 203 | ### writedump / serializeJSON 204 | 205 | `writeDump(x)` and `serializeJSON(x)` data visualizations are made available as context menu items from within the debug variables pane. Right-clicking on a variable brings up the menu: 206 | 207 | ![misc. features of a debug session indicating that luceedebug is a step debugger for Lucee.](assets/dumpvar-context-menu.png) 208 | 209 | and results are placed into an editor tab. 210 | 211 | --- 212 | 213 | ### Watch expressions 214 | 215 | Support for conditional breakpoints, watch expressions, and REPL evaluation. 216 | 217 | ![misc. watch features being used](assets/watch.png) 218 | 219 | - Conditional breakpoints evaluate to "false" if they fail (aren't convertible to boolean by CF conversion rules, or throw an exception), so conditional breakpoints on something like `request.xxx`, where `request.xxx` is usually null but is sometimes set to true, is a sensible thing. 220 | - Footgun -- a conditional breakpoint on `x = 42` (an assignment, as opposed to the equality check `x == 42`) will assign `x` the value of `42`. 221 | - watch/repl/conditional expression evaluation which results in additional breakpoints being fired is undefined behavior. The most likely outcome is a deadlock. 222 | 223 | --- 224 | 225 | ### Debug breakpoint bindings 226 | If breakpoints aren't binding, you can inspect what's going using the "luceedebug: show class and breakpoint info" command. Surface this by typing "show class and breakpoint info" into the [command palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette). 227 | 228 | ### Scan luceedebug Agent for Security Vulnerabilities 229 | 230 | ```sh 231 | ./gradlew dependencyCheckAnalyze 232 | ``` -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "luceedebug-root" 2 | 3 | include("luceedebug") 4 | 5 | pluginManagement { 6 | includeBuild("buildPlugins") 7 | } 8 | -------------------------------------------------------------------------------- /test/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "cfml", 9 | "request": "attach", 10 | "name": "Attach to server", 11 | "hostName": "localhost", 12 | "pathTransforms": [ 13 | { 14 | "idePrefix": "c:\\Users\\anon\\dev\\luceedebug\\java-agent\\test\\docker", 15 | "serverPrefix": "/var/www" 16 | } 17 | ], 18 | "port": 10000 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /test/docker/5.3.10.120/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lucee/lucee:5.3.10.120-tomcat9.0-jdk11-openjdk-2303 2 | 3 | # "@LUCEEDEBUG_JAR" replaced programmatically with filename 4 | ENV LUCEEDEBUG_JAR /build/@LUCEEDEBUG_JAR 5 | ENV SETENV_FILE /usr/local/tomcat/bin/setenv.sh 6 | 7 | #RUN apt-get update 8 | #RUN apt-get -y install vim 9 | 10 | # build up catalina opts to include jdwp and luceedebug 11 | RUN echo export CATALINA_OPTS='"''$CATALINA_OPTS' -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:9999'"' >> ${SETENV_FILE} 12 | RUN echo export CATALINA_OPTS='"''$CATALINA_OPTS' -javaagent:${LUCEEDEBUG_JAR}=jdwpHost=localhost,jdwpPort=9999,cfHost=0.0.0.0,cfPort=10000,jarPath=${LUCEEDEBUG_JAR}'"' >> ${SETENV_FILE} 13 | -------------------------------------------------------------------------------- /test/docker/5.3.10.120/luceedebug.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | # this is done programmatically from the tests, we might not need it 4 | # or maybe we should read this and build the docker container from java with this def 5 | 6 | services: 7 | lucee: 8 | #image: lucee/lucee:5.3.10.120-tomcat9.0-jdk11-openjdk-2303 9 | image: luceedebug 10 | container_name: luceedebug 11 | ports: 12 | - 8888:8888 13 | - 10000:10000 14 | volumes: 15 | - type: bind 16 | source: ../../../luceedebug/build/libs/ 17 | target: /build/ 18 | - type: bind 19 | source: ../ 20 | target: /var/www/ 21 | -------------------------------------------------------------------------------- /test/docker/6.1.0.243/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lucee/lucee:6.1.0.243-light-nginx-tomcat9.0-jdk21-temurin-jammy 2 | 3 | # "@LUCEEDEBUG_JAR" replaced programmatically with filename 4 | ENV LUCEEDEBUG_JAR /build/@LUCEEDEBUG_JAR 5 | ENV SETENV_FILE /usr/local/tomcat/bin/setenv.sh 6 | 7 | RUN echo export CATALINA_OPTS='"''$CATALINA_OPTS' -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:9999'"' >> ${SETENV_FILE} 8 | RUN echo export CATALINA_OPTS='"''$CATALINA_OPTS' -javaagent:${LUCEEDEBUG_JAR}=jdwpHost=localhost,jdwpPort=9999,cfHost=0.0.0.0,cfPort=10000,jarPath=${LUCEEDEBUG_JAR}'"' >> ${SETENV_FILE} 9 | -------------------------------------------------------------------------------- /test/docker/app1/a.cfm: -------------------------------------------------------------------------------- 1 | 2 | function foo(n) { 3 | var e = "bar"; 4 | return n; 5 | } 6 | 7 | writedump(foo(42)) 8 | -------------------------------------------------------------------------------- /test/docker/app1/heartbeat.cfm: -------------------------------------------------------------------------------- 1 | OK -------------------------------------------------------------------------------- /test/docker/step_to_catch_block/a.cfm: -------------------------------------------------------------------------------- 1 | 2 | function foo() { 3 | try { 4 | bar(); 5 | } 6 | catch (any e) { 7 | 0+0; 8 | } 9 | } 10 | 11 | function bar() { 12 | baz(); 13 | } 14 | 15 | function baz() { 16 | try { 17 | qux(); 18 | } 19 | finally { 20 | 0+0; 21 | } 22 | } 23 | 24 | function qux() { 25 | last(); 26 | } 27 | 28 | function last() { 29 | throw "e"; 30 | } 31 | 32 | foo(); 33 | -------------------------------------------------------------------------------- /test/docker/step_to_catch_block/heartbeat.cfm: -------------------------------------------------------------------------------- 1 | OK -------------------------------------------------------------------------------- /test/docker/stepping_through_default_args/a.cfm: -------------------------------------------------------------------------------- 1 | 2 | function foo( 3 | a = 0, b = 0, 4 | c = {a:1}, d = 0, 5 | e = {b:1}, f = 0 6 | ) { 7 | return 0; 8 | } 9 | 10 | foo(d = 42); 11 | -------------------------------------------------------------------------------- /test/docker/stepping_through_default_args/heartbeat.cfm: -------------------------------------------------------------------------------- 1 | OK -------------------------------------------------------------------------------- /test/docker/stepping_works_as_expected_on_singleline_statement_with_many_subexpressions/a.cfm: -------------------------------------------------------------------------------- 1 | 2 | function foo(n) { 3 | return {foo:foo} 4 | } 5 | 6 | foo(1).foo(2).foo(3).foo(4); 7 | foo(6); 8 | foo(7); 9 | -------------------------------------------------------------------------------- /test/docker/stepping_works_as_expected_on_singleline_statement_with_many_subexpressions/heartbeat.cfm: -------------------------------------------------------------------------------- 1 | OK -------------------------------------------------------------------------------- /test/scratch/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwareCobbler/luceedebug/2de98fdaad6f3e2b2a2c5a56a2e169841a470548/test/scratch/.gitkeep -------------------------------------------------------------------------------- /vscode-client/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | node_modules 3 | /out 4 | -------------------------------------------------------------------------------- /vscode-client/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch luceedebug in Extension Development Host", 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "args": [ 12 | "--extensionDevelopmentPath=${workspaceFolder}" 13 | ], 14 | "outFiles": [ 15 | "${workspaceFolder}/dist/**/*.js" 16 | ], 17 | }, 18 | { 19 | "name": "attach to java-server", 20 | "type": "java", 21 | "request": "attach", 22 | "hostName": "localhost", 23 | "port": "8999", 24 | }, 25 | ], 26 | "compounds": [ 27 | { 28 | "name": "Extension + attach to java server", 29 | "configurations": ["Extension", "attach to java-server"] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /vscode-client/README.md: -------------------------------------------------------------------------------- 1 | This is a barebones client for the 'luceedebug' backend, enabling breakpoints for {.cfm,.cfc,.cfml} files. 2 | 3 | This requires the associated java agent be run as part of your Lucee JVM startup. You can download the agent jar from https://github.com/softwareCobbler/luceedebug/releases/latest or clone https://github.com/softwareCobbler/luceedebug and build it yourself. 4 | -------------------------------------------------------------------------------- /vscode-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luceedebug", 3 | "publisher": "DavidRogers", 4 | "version": "2.0.11", 5 | "description": "VS Code client for luceedebug backend.", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build-dev-windows": "tsc && node \"node_modules/esbuild/bin/esbuild\" ./src/extension.ts --bundle --sourcemap --tsconfig=./tsconfig.json --external:vscode --format=cjs --platform=node --outfile=dist/extension.js", 9 | "build-dev-linux": "tsc && ./node_modules/esbuild/bin/esbuild ./src/extension.ts --bundle --sourcemap --tsconfig=./tsconfig.json --external:vscode --format=cjs --platform=node --outfile=dist/extension.js" 10 | }, 11 | "keywords": ["ColdFusion", "debug", "debugger", "Lucee"], 12 | "author": "David Rogers", 13 | "license": "ISC", 14 | "main": "./dist/extension.js", 15 | "activationEvents": [ 16 | "onDebugResolve:cfml" 17 | ], 18 | "categories": ["Debuggers"], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/softwareCobbler/luceedebug" 22 | }, 23 | "engines": { 24 | "vscode": "^1.63.0" 25 | }, 26 | "contributes": { 27 | "customEditors": [ 28 | { 29 | "viewType": "luceedebug.dumpView", 30 | "displayName": "luceedebug var dump", 31 | "selector": [ 32 | { 33 | "filenamePattern": "*.luceedebugdumpview" 34 | } 35 | ] 36 | } 37 | ], 38 | "menus": { 39 | "debug/variables/context": [ 40 | { 41 | "when": "debugType == 'cfml'", 42 | "command": "luceedebug.dump" 43 | }, 44 | { 45 | "when": "debugType == 'cfml'", 46 | "command": "luceedebug.dumpAsJSON" 47 | }, 48 | { 49 | "when": "debugType == 'cfml'", 50 | "command": "luceedebug.openFileForVariableSourcePath" 51 | } 52 | ] 53 | }, 54 | "languages": [ 55 | { 56 | "id": "cfml", 57 | "extensions": [".cfm", ".cfc", ".cfml"], 58 | "aliases": ["ColdFusion", "CFML"], 59 | "filenames": [] 60 | } 61 | ], 62 | "breakpoints": [ 63 | { 64 | "language": "cfml" 65 | } 66 | ], 67 | "commands": [ 68 | { 69 | "command": "luceedebug.dump", 70 | "title": "luceedebug: dump", 71 | "enablement": "debugType == 'cfml'" 72 | }, 73 | { 74 | "command": "luceedebug.dumpAsJSON", 75 | "title": "luceedebug: dump as JSON", 76 | "enablement": "debugType == 'cfml'" 77 | }, 78 | { 79 | "command": "luceedebug.debugBreakpointBindings", 80 | "title": "luceedebug: show class and breakpoint info" 81 | }, 82 | { 83 | "command": "luceedebug.openFileForVariableSourcePath", 84 | "title": "luceedebug: open defining file", 85 | "enablement": "debugType == 'cfml'" 86 | } 87 | ], 88 | "debuggers": [ 89 | { 90 | "type": "cfml", 91 | "languages": [ 92 | "cfml" 93 | ], 94 | "label": "CFML Debug Adapter", 95 | "configurationAttributes": { 96 | "attach": { 97 | "required": [ 98 | "hostName", 99 | "port" 100 | ], 101 | "properties": { 102 | "port": { 103 | "type": "number", 104 | "description": "Port that has been configured to accept luceedebug connections.", 105 | "default": 10000 106 | }, 107 | "hostName": { 108 | "type": "string", 109 | "description": "Hostname (i.e. `localhost`) or address, of server on which the target Lucee server is running." 110 | }, 111 | "pathTransforms": { 112 | "type": "array", 113 | "description": "Ordered list of source file path transforms (e.g. if in your IDE, you are editing '/foo/bar/baz.cfc', but the cf engine knows it as '/app/bar/baz.cfc', it needs to be transformed). The first one that matches for a particular need 'wins'. If none match (or the list is empty), no paths are transformed.", 114 | "items": { 115 | "type": "object", 116 | "properties": { 117 | "idePrefix": {"type": "string", "default": "${workspaceFolder}"}, 118 | "serverPrefix": {"type": "string", "default": "/container-root/app"} 119 | }, 120 | "required": ["idePrefix", "serverPrefix"] 121 | }, 122 | "default": [ 123 | { 124 | "idePrefix": "${workspaceFolder}", 125 | "serverPrefix": "/container-root/app" 126 | } 127 | ] 128 | }, 129 | "pathSeparator": { 130 | "type": "string", 131 | "enum": ["none", "auto", "posix", "windows"], 132 | "default": "auto", 133 | "description": "How paths returned from the debugger should be normalized (none, auto, posix, or windows)." 134 | } 135 | } 136 | } 137 | }, 138 | "initialConfigurations": [ 139 | { 140 | "type": "cfml", 141 | "request": "attach", 142 | "name": "Attach to server", 143 | "hostName": "localhost", 144 | "pathTransforms": [ 145 | { 146 | "idePrefix": "${workspaceFolder}", 147 | "serverPrefix": "/app" 148 | } 149 | ], 150 | "pathSeparator": "auto", 151 | "port": 8000 152 | } 153 | ], 154 | "configurationSnippets": [ 155 | { 156 | "label": "luceedebug: Attach", 157 | "description": "Default configuration to connect to Lucee", 158 | "body": { 159 | "type": "cfml", 160 | "request": "attach", 161 | "name": "Attach to server", 162 | "hostName": "localhost", 163 | "pathTransforms": [ 164 | { 165 | "idePrefix": "^\"\\${workspaceFolder}\"", 166 | "serverPrefix": "/app" 167 | } 168 | ], 169 | "pathSeparator": "auto", 170 | "port": 8000 171 | } 172 | } 173 | ], 174 | "variables": {} 175 | } 176 | ] 177 | }, 178 | "dependencies": { 179 | "@types/node": "^17.0.0", 180 | "@types/vscode": "^1.63.1", 181 | "vscode-debugadapter": "^1.51.0" 182 | }, 183 | "devDependencies": { 184 | "esbuild": "^0.14.5" 185 | }, 186 | "$defs": { 187 | "foo": { 188 | 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /vscode-client/sampleWorkspace/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "cfml", 9 | "request": "attach", 10 | "name": "Project A", 11 | "hostName": "localhost", 12 | "port": 10000, 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /vscode-client/sampleWorkspace/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | x = 42; 3 | -------------------------------------------------------------------------------- /vscode-client/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | let currentDebugSession : vscode.DebugSession | null = null; 4 | 5 | class CfDebugAdapter implements vscode.DebugAdapterDescriptorFactory { 6 | createDebugAdapterDescriptor(session: vscode.DebugSession, _executable: vscode.DebugAdapterExecutable | undefined): vscode.ProviderResult { 7 | currentDebugSession = session; 8 | 9 | const host = session.configuration.hostName; 10 | const port = parseInt(session.configuration.port); 11 | 12 | return new vscode.DebugAdapterServer(port, host); 13 | } 14 | 15 | } 16 | 17 | export function activate(context: vscode.ExtensionContext) { 18 | const outputChannel = vscode.window.createOutputChannel("lucee-debugger"); 19 | context.subscriptions.push(outputChannel); 20 | context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("cfml", new CfDebugAdapter())); 21 | 22 | // is there an official type for this? 23 | // this is just gleaned from observed runtime behavior on dev machine 24 | interface DebugPaneContextMenuArgs { 25 | container: { 26 | expensive: boolean, 27 | name: string, 28 | variablesReference: number 29 | }, 30 | sessionId: string, 31 | variable: { 32 | name: string, 33 | value: string, 34 | variablesReference: number 35 | } 36 | } 37 | 38 | interface DumpResponse { 39 | content: string 40 | } 41 | 42 | const webviewPanelByUri : {[uri: string]: vscode.WebviewPanel} = {} 43 | const updateOrCreateWebview = (uri: vscode.Uri, html: string) => { 44 | const uriString = uri.toString(); 45 | const panel = webviewPanelByUri[uriString]; 46 | if (panel) { 47 | panel.webview.html = html; 48 | panel.reveal(undefined, true); 49 | } 50 | else { 51 | const panel = vscode.window.createWebviewPanel( 52 | 'luceedebug', 53 | uri.path, 54 | vscode.ViewColumn.One, 55 | { 56 | enableScripts: true, 57 | } 58 | ); 59 | panel.webview.html = html; 60 | webviewPanelByUri[uriString] = panel; 61 | panel.onDidDispose(() => { 62 | delete webviewPanelByUri[uriString]; 63 | }); 64 | } 65 | } 66 | 67 | const normalizePathFromSession = (session: vscode.DebugSession, path: string): string => { 68 | const pathSeparator = session.configuration?.pathSeparator ?? "auto"; 69 | if (pathSeparator === "none") return path; 70 | 71 | const platformDefault = process.platform === "win32" ? "\\" : "/"; 72 | const normalizedSeparator = pathSeparator === "posix" 73 | ? "/" 74 | : pathSeparator === "windows" 75 | ? "\\" 76 | : platformDefault; 77 | return path.replace(/[\\/]/g, normalizedSeparator); 78 | }; 79 | 80 | context.subscriptions.push( 81 | vscode.commands.registerCommand("luceedebug.dump", async (args?: Partial) => { 82 | if (args?.variable === undefined || args.variable.variablesReference === 0) { 83 | // This could be called from the command pallette (press F1 and type it) 84 | // rather than from the debug variables pane context menu. Maybe there is a better 85 | // way to determine where this was called from, or prevent it from being called anywhere 86 | // except the debug variables pane context menu. 87 | // 88 | // If variablesReference is 0, then the value is a primitive value and we don't service the request 89 | // 90 | return; 91 | } 92 | 93 | // need a timeout? or does this cb get wrapped in a timeout by whoever we're passing it to 94 | const result : DumpResponse = await currentDebugSession?.customRequest("dump", {variablesReference: args.variable.variablesReference}); 95 | const uri = vscode.Uri.from({scheme: "luceedebug", path: args.variable.name, fragment: args.variable.variablesReference.toString()}); 96 | const html = result.content; 97 | updateOrCreateWebview(uri, html); 98 | }), 99 | vscode.commands.registerCommand("luceedebug.dumpAsJSON", async (args?: Partial) => { 100 | if (!currentDebugSession || args?.variable === undefined || args.variable.variablesReference === 0) { 101 | return; 102 | } 103 | 104 | const result : DumpResponse = await currentDebugSession.customRequest("dumpAsJSON", {variablesReference: args.variable.variablesReference}); 105 | 106 | let obj : any; 107 | try { 108 | obj = JSON.parse(result.content); 109 | } 110 | catch { 111 | obj = "Failed to parse the following JSON:\n" + result.content; 112 | } 113 | 114 | const uri = vscode.Uri.from({scheme: "luceedebug", path: args.variable.name, fragment: args.variable.variablesReference.toString()}); 115 | const text = JSON.stringify(obj, undefined, 4); 116 | 117 | luceedebugTextDocumentProvider.addOrReplaceTextDoc(uri, text); 118 | 119 | const doc = await vscode.workspace.openTextDocument(uri); 120 | await vscode.window.showTextDocument(doc); 121 | }) 122 | ); 123 | 124 | // add hover for debugger 125 | // TODO: replace naive regex matcher with better implementation 126 | context.subscriptions.push(vscode.languages.registerEvaluatableExpressionProvider('cfml', { 127 | provideEvaluatableExpression(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult { 128 | /** 129 | * will match most variable declaration styles: 130 | * local.varName 131 | * varName 132 | * local.varName.subKey 133 | * local.varName['subKey'] 134 | * local['varName'].subKey 135 | * local['varNam'][subKey] 136 | * local.varName["subKey"] 137 | * -> queryName 138 | * 139 | * however will also match: 140 | * local.varName.functionCall() 141 | * "somestring.that.looks.like[a]variable" -> somestring.that.looks.like[a]variable 142 | * 143 | */ 144 | const varRange = document.getWordRangeAtPosition(position, /[\w_][\w\[\]"'\._\-]+[\w\]]/ig) 145 | ?? document.getWordRangeAtPosition(position); 146 | if(varRange !== undefined) { 147 | return new vscode.EvaluatableExpression(varRange); 148 | } 149 | return undefined; 150 | } 151 | })); 152 | 153 | // context.subscriptions.push( 154 | // vscode.commands.registerCommand("luceeDebugger.showLoadedClasses", () => { 155 | // currentDebugSession?.customRequest("showLoadedClasses"); 156 | // }) 157 | // ); 158 | 159 | const luceedebugTextDocumentProvider = new (class implements vscode.TextDocumentContentProvider { 160 | private docs : {[uri: string]: string} = {}; 161 | 162 | private onDidChangeEmitter = new vscode.EventEmitter(); 163 | onDidChange = this.onDidChangeEmitter.event; 164 | 165 | addOrReplaceTextDoc(uri: vscode.Uri, text: string) { 166 | this.docs[uri.toString()] = text; 167 | this.onDidChangeEmitter.fire(uri); 168 | } 169 | 170 | provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult { 171 | return this.docs[uri.toString()] ?? null; 172 | } 173 | })(); 174 | 175 | vscode.workspace.registerTextDocumentContentProvider("luceedebug", luceedebugTextDocumentProvider); 176 | context.subscriptions.push( 177 | vscode.commands.registerCommand("luceedebug.debugBreakpointBindings", async () => { 178 | if (!currentDebugSession) { 179 | throw Error("luceedebug is not currently connected to Lucee, cannot debug breakpoints.") 180 | } 181 | 182 | interface DebugBreakpointBindingsResponse { 183 | canonicalFilenames: string[], 184 | breakpoints: [string, string][], 185 | pathTransforms: string[], 186 | } 187 | const data : DebugBreakpointBindingsResponse = await currentDebugSession?.customRequest("debugBreakpointBindings"); 188 | 189 | const uri = vscode.Uri.from({scheme: "luceedebug", path: "debugBreakpointBindings"}); 190 | const text = "Breakpoints luceedebug has:\n" 191 | + data 192 | .breakpoints 193 | .sort(([l_idePath],[r_idePath]) => l_idePath < r_idePath ? -1 : 1) 194 | .map(([idePath, serverPath]) => ` (ide) ${idePath}\n (server) ${serverPath}`).join("\n\n") 195 | + "\n\nPath transforms:\n" 196 | + (data.pathTransforms.length === 0 ? "<>" : data.pathTransforms.map(v => ` ${v}`).join("\n")) 197 | + "\n\nFiles luceedebug knows about (all filenames are as the server sees them, and match against breakpoint 'server' paths):\n" 198 | + data.canonicalFilenames.sort().map(s => ` ${s}`).join("\n"); 199 | 200 | luceedebugTextDocumentProvider.addOrReplaceTextDoc(uri, text); 201 | 202 | const doc = await vscode.workspace.openTextDocument(uri); 203 | await vscode.window.showTextDocument(doc); 204 | }) 205 | ) 206 | 207 | context.subscriptions.push( 208 | vscode.commands.registerCommand("luceedebug.openFileForVariableSourcePath", async (args?: Partial) => { 209 | if (!currentDebugSession || !args || args.variable === undefined || args.variable.variablesReference === 0) { 210 | // doesn't exist or represents a primitive value 211 | return; 212 | } 213 | 214 | interface GetSourcePathResponse { 215 | path: string | null 216 | } 217 | 218 | const data : GetSourcePathResponse = await currentDebugSession.customRequest("getSourcePath", {variablesReference: args.variable.variablesReference}); 219 | if (!data.path) { 220 | return; 221 | } 222 | const uri = vscode.Uri.from({scheme: "file", path: data.path}); 223 | const doc = await vscode.workspace.openTextDocument(uri); 224 | await vscode.window.showTextDocument(doc); 225 | }) 226 | ) 227 | 228 | vscode.debug.registerDebugAdapterTrackerFactory("cfml", { 229 | createDebugAdapterTracker(session: vscode.DebugSession) { 230 | return { 231 | onWillReceiveMessage(message: any) : void { 232 | outputChannel.append(JSON.stringify(message, null, 4) + "\n"); 233 | }, 234 | onDidSendMessage(message: any) : void { 235 | if (message.command === "stackTrace" || (message.type === "response" && message.body?.stackFrames)) { 236 | for (const frame of message.body.stackFrames) { 237 | if (frame.source?.path) { 238 | frame.source.path = normalizePathFromSession(session, frame.source.path); 239 | } 240 | } 241 | } 242 | outputChannel.append(JSON.stringify(message, null, 4) + "\n"); 243 | } 244 | } 245 | } 246 | }) 247 | } 248 | 249 | export function deactivate() { 250 | currentDebugSession = null; 251 | } 252 | -------------------------------------------------------------------------------- /vscode-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | "rootDir": "./src", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./out", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | "sourceRoot": "./src", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 74 | 75 | /* Type Checking */ 76 | "strict": true, /* Enable all strict type-checking options. */ 77 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 78 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 79 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 80 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 81 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 82 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 83 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 84 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 85 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 90 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 95 | 96 | /* Completeness */ 97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 99 | } 100 | } 101 | --------------------------------------------------------------------------------