From c126cf51c0bba1f86de147bc00696c00ff028205 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:38:27 -0500 Subject: [PATCH] GP-3823: TraceRmi Launcher framework + dbgeng for Windows. --- .../certification.manifest | 3 +- .../data/debugger-launchers/local-dbgeng.bat | 31 + .../data/support/local-dbgeng.py | 30 + .../src/javaprovider/cpp/javaprovider.cpp | 224 ------- .../src/javaprovider/def/javaprovider.def | 13 - .../src/javaprovider/headers/afxres.h | 16 - .../src/javaprovider/headers/resource.h | 31 - .../src/javaprovider/rc/javaprovider.rc | 0 .../src/main/py/src/ghidradbg/commands.py | 36 +- .../src/main/py/src/ghidradbg/methods.py | 4 +- .../src/sctldbg/cpp/sctldbg.cpp | 315 --------- .../AbstractScriptTraceRmiLaunchOffer.java | 109 +++ .../launcher/AbstractTraceRmiLaunchOffer.java | 13 +- .../AbstractTraceRmiLaunchOpinion.java | 60 ++ .../BatchScriptTraceRmiLaunchOffer.java | 70 ++ .../BatchScriptTraceRmiLaunchOpinion.java | 49 ++ .../gui/tracermi/launcher/LaunchAction.java | 66 +- .../launcher/ScriptAttributesParser.java | 569 ++++++++++++++++ .../UnixShellScriptTraceRmiLaunchOffer.java | 624 +----------------- .../UnixShellScriptTraceRmiLaunchOpinion.java | 61 +- .../service/rmi/trace/TraceRmiHandler.java | 20 +- .../gui/modules/DebuggerModulesProvider.java | 16 +- .../modules/DefaultModuleMapProposal.java | 9 +- .../main/java/ghidra/dbg/util/ShellUtils.java | 11 +- .../memory/DBTraceObjectMemoryRegion.java | 24 +- .../model/memory/TraceObjectMemoryRegion.java | 7 + .../src/main/java/ghidra/pty/PtyChild.java | 18 +- .../java/ghidra/pty/linux/LinuxPtyChild.java | 11 +- .../main/java/ghidra/pty/ssh/SshPtyChild.java | 7 +- .../java/ghidra/pty/windows/ConPtyChild.java | 11 +- .../ghidra/pty/windows/HandleInputStream.java | 4 +- .../pty/windows/jna/ConsoleApiNative.java | 2 +- .../TraceRmiLauncherServicePluginTest.java | 45 +- 33 files changed, 1206 insertions(+), 1303 deletions(-) create mode 100644 Ghidra/Debug/Debugger-agent-dbgeng/data/debugger-launchers/local-dbgeng.bat create mode 100644 Ghidra/Debug/Debugger-agent-dbgeng/data/support/local-dbgeng.py delete mode 100644 Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/cpp/javaprovider.cpp delete mode 100644 Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/def/javaprovider.def delete mode 100644 Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/headers/afxres.h delete mode 100644 Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/headers/resource.h delete mode 100644 Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/rc/javaprovider.rc delete mode 100644 Ghidra/Debug/Debugger-agent-dbgeng/src/sctldbg/cpp/sctldbg.cpp create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOpinion.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOpinion.java create mode 100644 Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest b/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest index 7689ea7e59..fc57b11e64 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest +++ b/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest @@ -1,8 +1,7 @@ ##VERSION: 2.0 ##MODULE IP: Apache License 2.0 Module.manifest||GHIDRA||||END| -src/javaprovider/def/javaprovider.def||GHIDRA||||END| -src/javaprovider/rc/javaprovider.rc||GHIDRA||||END| +data/debugger-launchers/local-dbgeng.bat||GHIDRA||||END| src/main/py/LICENSE||GHIDRA||||END| src/main/py/README.md||GHIDRA||||END| src/main/py/pyproject.toml||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/data/debugger-launchers/local-dbgeng.bat b/Ghidra/Debug/Debugger-agent-dbgeng/data/debugger-launchers/local-dbgeng.bat new file mode 100644 index 0000000000..4321b2a224 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/data/debugger-launchers/local-dbgeng.bat @@ -0,0 +1,31 @@ +::@title dbgeng +::@desc +::@desc

Launch with dbgeng (in a Python interpreter)

+::@desc

This will launch the target on the local machine using dbgeng.dll. Typically, +::@desc Windows systems have this library pre-installed, but it may have limitations, e.g., you +::@desc cannot use .server. For the full capabilities, you must install WinDbg.

+::@desc

Furthermore, you must have Python 3 installed on your system, and it must have the +::@desc pybag and protobuf packages installed.

+::@desc +::@menu-group local +::@icon icon.debugger +::@help TraceRmiLauncherServicePlugin#dbgeng +::@env OPT_PYTHON_EXE:str="python" "Path to python" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH." +:: Use env instead of args, because "all args except first" is terrible to implement in batch +::@env OPT_TARGET_IMG:str="" "Image" "The target binary executable image" +::@env OPT_TARGET_ARGS:str="" "Arguments" "Command-line arguments to pass to the target" + +@echo off + +if exist "%GHIDRA_HOME%\ghidra\.git\" ( + set PYTHONPATH=%GHIDRA_HOME%\ghidra\Ghidra\Debug\Debugger-agent-dbgeng\build\pypkg\src;%GHIDRA_HOME%\ghidra\Ghidra\Debug\Debugger-rmi-trace\build\pypkg\src;%PYTHONPATH% +) else if exist "%GHIDRA_HOME%\.git\" ( + set PYTHONPATH=%GHIDRA_HOME%\Ghidra\Debug\Debugger-agent-dbgeng\build\pypkg\src;%GHIDRA_HOME%\Ghidra\Debug\Debugger-rmi-trace\build\pypkg\src;%PYTHONPATH% +) else ( + set PYTHONPATH=%GHIDRA_HOME%\Ghidra\Debug\Debugger-agent-dbgeng\pypkg\src;%GHIDRA_HOME%\Ghidra\Debug\Debugger-rmi-trace\pypkg\src;%PYTHONPATH% +) + +echo PYTHONPATH is %PYTHONPATH% +echo bat OPT_TARGET_IMG is [%OPT_TARGET_IMG%] + +"%OPT_PYTHON_EXE%" -i ..\support\local-dbgeng.py diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/data/support/local-dbgeng.py b/Ghidra/Debug/Debugger-agent-dbgeng/data/support/local-dbgeng.py new file mode 100644 index 0000000000..cba532de5c --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/data/support/local-dbgeng.py @@ -0,0 +1,30 @@ +## ### +# IP: GHIDRA +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## +import os +from ghidradbg.commands import * + +ghidra_trace_connect(os.getenv('GHIDRA_TRACE_RMI_ADDR')) +args = os.getenv('OPT_TARGET_ARGS') +if args: + args = ' ' + args +ghidra_trace_create(os.getenv('OPT_TARGET_IMG') + args, start_trace=False) +ghidra_trace_start(os.getenv('OPT_TARGET_IMG')) +ghidra_trace_sync_enable() + +# TODO: HACK +dbg().wait() + +repl() diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/cpp/javaprovider.cpp b/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/cpp/javaprovider.cpp deleted file mode 100644 index 2bb02ec96b..0000000000 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/cpp/javaprovider.cpp +++ /dev/null @@ -1,224 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#define INITGUID - -#include -#include - -#include "resource.h" - -#define CHECK_RESULT(x, y) do { \ - HRESULT hr = (x); \ - if (hr != S_OK) { \ - fprintf(stderr, "HRESULT of %s = %x\n", ##x, hr); \ - return y; \ - } \ -} while (0) - -class EXT_CLASS : public ExtExtension { -public: - virtual HRESULT Initialize(); - virtual void Uninitialize(); - - //virtual void OnSessionAccessible(ULONG64 Argument); - - EXT_COMMAND_METHOD(java_add_cp); - EXT_COMMAND_METHOD(java_set); - EXT_COMMAND_METHOD(java_get); - EXT_COMMAND_METHOD(java_run); - - void run_command(PCSTR name); -}; - -EXT_DECLARE_GLOBALS(); - -JavaVM* jvm = NULL; -JNIEnv* env = NULL; -jclass clsCommands = NULL; - -char JDK_JVM_DLL_PATH[] = "\\jre\\bin\\server\\jvm.dll"; -char JRE_JVM_DLL_PATH[] = "\\bin\\server\\jvm.dll"; - -typedef jint (_cdecl *CreateJavaVMFunc)(JavaVM**, void**, void*); - -HRESULT EXT_CLASS::Initialize() { - HRESULT result = ExtExtension::Initialize(); - if (result != S_OK) { - return result; - } - - char* env_java_home = getenv("JAVA_HOME"); - if (env_java_home == NULL) { - fprintf(stderr, "JAVA_HOME is not set\n"); - fflush(stderr); - return E_FAIL; - } - char* java_home = strdup(env_java_home); - size_t home_len = strlen(java_home); - if (java_home[home_len - 1] == '\\') { - java_home[home_len - 1] = '\0'; - } - size_t full_len = home_len + sizeof(JDK_JVM_DLL_PATH); - char* full_path = new char[full_len]; - HMODULE jvmDll = NULL; - // Try the JRE path first; - strcpy_s(full_path, full_len, java_home); - strcat_s(full_path, full_len, JRE_JVM_DLL_PATH); - fprintf(stderr, "Trying to find jvm.dll at %s\n", full_path); - fflush(stderr); - jvmDll = LoadLibraryA(full_path); - if (jvmDll == NULL) { - // OK, then try the JDK path - strcpy_s(full_path, full_len, java_home); - strcat_s(full_path, full_len, JDK_JVM_DLL_PATH); - fprintf(stderr, "Trying to find jvm.dll at %s\n", full_path); - fflush(stderr); - jvmDll = LoadLibraryA(full_path); - } - free(full_path); - free(java_home); - if (jvmDll == NULL) { - fprintf(stderr, "Could not find the jvm.dll\n"); - fflush(stderr); - return E_FAIL; - } - fprintf(stderr, "Found it!\n"); - fflush(stderr); - - JavaVMOption options[2]; - JavaVMInitArgs vm_args = { 0 }; - vm_args.version = JNI_VERSION_1_8; - vm_args.nOptions = sizeof(options)/sizeof(options[0]); - vm_args.options = options; - options[0].optionString = "-Xrs"; - options[1].optionString = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"; - vm_args.ignoreUnrecognized = false; - CreateJavaVMFunc create_jvm = NULL; - //create_jvm = JNI_CreateJavaVM; - create_jvm = (CreateJavaVMFunc) GetProcAddress(jvmDll, "JNI_CreateJavaVM"); - jint jni_result = create_jvm(&jvm, (void**)&env, &vm_args); - - if (jni_result != JNI_OK) { - jvm = NULL; - fprintf(stderr, "Could not initialize JVM: %d: ", jni_result); - switch (jni_result) { - case JNI_ERR: - fprintf(stderr, "unknown error"); - break; - case JNI_EDETACHED: - fprintf(stderr, "thread detached from the VM"); - break; - case JNI_EVERSION: - fprintf(stderr, "JNI version error"); - break; - case JNI_ENOMEM: - fprintf(stderr, "not enough memory"); - break; - case JNI_EEXIST: - fprintf(stderr, "VM already created"); - break; - case JNI_EINVAL: - fprintf(stderr, "invalid arguments"); - break; - } - fprintf(stderr, "\n"); - fflush(stderr); - return E_FAIL; - } - - HMODULE hJavaProviderModule = GetModuleHandle(TEXT("javaprovider")); - HRSRC resCommandsClassfile = FindResource(hJavaProviderModule, MAKEINTRESOURCE(IDR_CLASSFILE1), TEXT("Classfile")); - HGLOBAL gblCommandsClassfile = LoadResource(hJavaProviderModule, resCommandsClassfile); - LPVOID lpCommandsClassfile = LockResource(gblCommandsClassfile); - DWORD szCommandsClassfile = SizeofResource(hJavaProviderModule, resCommandsClassfile); - - clsCommands = env->DefineClass( - "javaprovider/Commands", NULL, (jbyte*) lpCommandsClassfile, szCommandsClassfile - ); - if (clsCommands == NULL) { - fprintf(stderr, "Could not define Commands class\n"); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); - return E_FAIL; - } - } - - return S_OK; -} - -void EXT_CLASS::Uninitialize() { - if (jvm != NULL) { - jvm->DestroyJavaVM(); - } - ExtExtension::Uninitialize(); -} - -void EXT_CLASS::run_command(PCSTR name) { - // TODO: Throw an exception during load, then! - if (jvm == NULL) { - Out("javaprovider extension did not load properly.\n"); - return; - } - if (clsCommands == NULL) { - Out("javaprovider extension did not load properly.\n"); - return; - } - - PCSTR args = GetRawArgStr(); - - jmethodID mthCommand = env->GetStaticMethodID(clsCommands, name, "(Ljava/lang/String;)V"); - if (mthCommand == NULL) { - Out("INTERNAL ERROR: No such command: %s\n", name); - return; - } - - jstring argsStr = env->NewStringUTF(args); - if (argsStr == NULL) { - Out("Could not create Java string for arguments.\n"); - return; - } - - env->CallStaticVoidMethod(clsCommands, mthCommand, argsStr); - env->DeleteLocalRef(argsStr); - if (env->ExceptionCheck()) { - Out("Exception during javaprovider command:\n"); - env->ExceptionDescribe(); // TODO: Send this to output callbacks, not console. - env->ExceptionClear(); - } -} - -EXT_COMMAND(java_add_cp, "Add an element to the class path", "{{custom}}") { - run_command("java_add_cp"); -} - -EXT_COMMAND(java_set, "Set a Java system property", "{{custom}}") { - run_command("java_set"); -} - -EXT_COMMAND(java_get, "Get a Java system property", "{{custom}}") { - run_command("java_get"); -} - -EXT_COMMAND(java_run, "Execute the named java class", "{{custom}}") { - run_command("java_run"); -} - -#define JNA extern "C" __declspec(dllexport) - -JNA HRESULT createClient(PDEBUG_CLIENT* client) { - return g_ExtInstance.m_Client->CreateClient(client); -} diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/def/javaprovider.def b/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/def/javaprovider.def deleted file mode 100644 index 197aee0bbd..0000000000 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/def/javaprovider.def +++ /dev/null @@ -1,13 +0,0 @@ -EXPORTS - -; For ExtCpp - DebugExtensionInitialize - DebugExtensionUninitialize - DebugExtensionNotify - help - -; My Commands - java_add_cp - java_set - java_get - java_run diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/headers/afxres.h b/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/headers/afxres.h deleted file mode 100644 index 853b8950c1..0000000000 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/headers/afxres.h +++ /dev/null @@ -1,16 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/headers/resource.h b/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/headers/resource.h deleted file mode 100644 index 39e5f2e8e5..0000000000 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/headers/resource.h +++ /dev/null @@ -1,31 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by javaprovider.rc -// -#define IDR_CLASSFILE1 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/rc/javaprovider.rc b/Ghidra/Debug/Debugger-agent-dbgeng/src/javaprovider/rc/javaprovider.rc deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py index f40d8b5933..c88f74cbd8 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py @@ -19,6 +19,7 @@ import os.path import socket import time import sys +import re from ghidratrace import sch from ghidratrace.client import Client, Address, AddressRange, TraceObject @@ -185,7 +186,7 @@ def compute_name(progname=None): progname = buffer.decode('utf-8') except Exception: return 'pydbg/noname' - return 'pydbg/' + progname.split('/')[-1] + return 'pydbg/' + re.split(r'/|\\', progname)[-1] def start_trace(name): @@ -1301,7 +1302,36 @@ def ghidra_util_wait_stopped(timeout=1): time.sleep(0.1) if time.time() - start > timeout: raise RuntimeError('Timed out waiting for thread to stop') - - + + def dbg(): return util.get_debugger() + + +SHOULD_WAIT = ['GO', 'STEP_BRANCH', 'STEP_INTO', 'STEP_OVER'] + + +def repl(): + print("This is the dbgeng.dll (WinDbg) REPL. To drop to Python3, press Ctrl-C.") + while True: + # TODO: Implement prompt retrieval in PR to pybag? + print('dbg> ', end='') + try: + cmd = input().strip() + if not cmd: + continue + dbg().cmd(cmd, quiet=False) + stat = dbg().exec_status() + if stat != 'BREAK': + dbg().wait() + else: + pass + #dbg().dispatch_events() + except KeyboardInterrupt as e: + print("") + print("You have left the dbgeng REPL and are now at the Python3 interpreter.") + print("use repl() to re-enter.") + return + except: + # Assume cmd() has already output the error + pass diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py index ae9bd6b33a..5b4fe2421f 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py @@ -383,7 +383,7 @@ def interrupt(): @REGISTRY.method(action='step_into') def step_into(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1): - """Step on instruction exactly.""" + """Step one instruction exactly.""" find_thread_by_obj(thread) dbg().stepi(n) @@ -511,7 +511,7 @@ def write_mem(process: sch.Schema('Process'), address: Address, data: bytes): @REGISTRY.method -def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes): +def write_reg(frame: sch.Schema('StackFrame'), name: str, value: bytes): """Write a register.""" util.select_frame() nproc = pydbg.selected_process() diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/sctldbg/cpp/sctldbg.cpp b/Ghidra/Debug/Debugger-agent-dbgeng/src/sctldbg/cpp/sctldbg.cpp deleted file mode 100644 index 747c9f9203..0000000000 --- a/Ghidra/Debug/Debugger-agent-dbgeng/src/sctldbg/cpp/sctldbg.cpp +++ /dev/null @@ -1,315 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include -#include - -#define INITGUID -#include -#include - -#include - -JavaVM* jvm = NULL; -JNIEnv* env = NULL; - -char JDK_JVM_DLL_PATH[] = "\\jre\\bin\\server\\jvm.dll"; -char JRE_JVM_DLL_PATH[] = "\\bin\\server\\jvm.dll"; - -char MAIN_CLASS[] = "sctldbgeng/sctl/DbgEngSctlServer"; - -char CP_PREFIX[] = "-Djava.class.path="; - -typedef jint (_cdecl *CreateJavaVMFunc)(JavaVM**, void**, void*); - - -#define CHECK_RC(v, f, x) do { \ - HRESULT ___hr = (x); \ - if (___hr < 0) { \ - fprintf(stderr, "FAILED on line %d: HRESULT=%08x\n", __LINE__, ___hr); \ - goto f; \ - } else if (___hr == S_OK) { \ - v = 1; \ - } else { \ - v = 0; \ - } \ -} while (0) - - -#if 0 -class MyEventCallbacks : public DebugBaseEventCallbacks { -public: - STDMETHOD_(ULONG, AddRef)(THIS) { - InterlockedIncrement(&m_ulRefCount); - return m_ulRefCount; - } - - STDMETHOD_(ULONG, Release)(THIS) { - ULONG ulRefCount = InterlockedDecrement(&m_ulRefCount); - if (m_ulRefCount == 0) { - delete this; - } - return ulRefCount; - } - - STDMETHOD(GetInterestMask)(_Out_ PULONG Mask) { - *Mask = DEBUG_EVENT_CREATE_PROCESS | DEBUG_EVENT_CREATE_THREAD; - return S_OK; - } - - STDMETHOD(CreateProcess)( - THIS_ - _In_ ULONG64 ImageFileHandle, - _In_ ULONG64 Handle, - _In_ ULONG64 BaseOffset, - _In_ ULONG ModuleSize, - _In_ PCSTR ModuleName, - _In_ PCSTR ImageName, - _In_ ULONG CheckSum, - _In_ ULONG TimeDateStamp, - _In_ ULONG64 InitialThreadHandle, - _In_ ULONG64 ThreadDataOffset, - _In_ ULONG64 StartOffset - ) { - UNREFERENCED_PARAMETER(ImageFileHandle); - UNREFERENCED_PARAMETER(Handle); - UNREFERENCED_PARAMETER(BaseOffset); - UNREFERENCED_PARAMETER(ModuleSize); - UNREFERENCED_PARAMETER(ModuleName); - UNREFERENCED_PARAMETER(ImageName); - UNREFERENCED_PARAMETER(CheckSum); - UNREFERENCED_PARAMETER(TimeDateStamp); - UNREFERENCED_PARAMETER(InitialThreadHandle); - UNREFERENCED_PARAMETER(ThreadDataOffset); - UNREFERENCED_PARAMETER(StartOffset); - return DEBUG_STATUS_BREAK; - } - - STDMETHOD(CreateThread)( - THIS_ - _In_ ULONG64 Handle, - _In_ ULONG64 DataOffset, - _In_ ULONG64 StartOffset - ) { - UNREFERENCED_PARAMETER(Handle); - UNREFERENCED_PARAMETER(DataOffset); - UNREFERENCED_PARAMETER(StartOffset); - return DEBUG_STATUS_BREAK; - } -private: - ULONG m_ulRefCount = 0; -}; - -int main_exp00(int argc, char** argv) { - PDEBUG_CLIENT5 pClient5 = NULL; - PDEBUG_CONTROL4 pControl4 = NULL; - PDEBUG_SYMBOLS3 pSymbols3 = NULL; - int ok = 0; - - CHECK_RC(ok, EXIT, DebugCreate(IID_IDebugClient5, (PVOID*) &pClient5)); - CHECK_RC(ok, EXIT, pClient5->QueryInterface(IID_IDebugControl4, (PVOID*) &pControl4)); - CHECK_RC(ok, EXIT, pClient5->QueryInterface(IID_IDebugSymbols3, (PVOID*) &pSymbols3)); - - pClient5->SetEventCallbacks(new MyEventCallbacks()); - - CHECK_RC(ok, EXIT, pControl4->Execute(DEBUG_OUTCTL_ALL_CLIENTS, ".create notepad", DEBUG_EXECUTE_ECHO)); - CHECK_RC(ok, EXIT, pControl4->WaitForEvent(0, INFINITE)); - CHECK_RC(ok, EXIT, pControl4->Execute(DEBUG_OUTCTL_ALL_CLIENTS, "g", DEBUG_EXECUTE_ECHO)); - CHECK_RC(ok, EXIT, pControl4->WaitForEvent(0, INFINITE)); - - ULONG64 ul64MatchHandle = 0; - CHECK_RC(ok, EXIT, pSymbols3->StartSymbolMatch("*", &ul64MatchHandle)); - while (true) { - char aBuffer[1024] = { 0 }; - ULONG64 ul64Offset = 0; - CHECK_RC(ok, FINISH, pSymbols3->GetNextSymbolMatch(ul64MatchHandle, aBuffer, sizeof(aBuffer), NULL, &ul64Offset)); - printf("%016x: %s\n", ul64Offset, aBuffer); - } -FINISH: - - fprintf(stderr, "SUCCESS\n"); -EXIT: - pClient5->SetEventCallbacks(NULL); - pControl4->Release(); - pClient5->Release(); - return 0; -} - -int main_exp01(int argc, char** argv) { - PDEBUG_CLIENT5 pClient5 = NULL; - int ok = 0; - - CHECK_RC(ok, EXIT, DebugCreate(IID_IDebugClient5, (PVOID*) &pClient5)); - - CHECK_RC(ok, EXIT, pClient5->StartProcessServerWide(DEBUG_CLASS_USER_WINDOWS, L"tcp:port=11200", NULL)); - CHECK_RC(ok, EXIT, pClient5->WaitForProcessServerEnd(INFINITE)); -EXIT: - if (pClient5 != NULL) { - pClient5->Release(); - } - return 0; -} -#endif - -int main_sctldbg(int argc, char** argv) { - if (argc < 1) { - fprintf(stderr, "Something is terribly wrong: argc == 0\n"); - } - char* env_java_home = getenv("JAVA_HOME"); - if (env_java_home == NULL) { - fprintf(stderr, "JAVA_HOME is not set\n"); - fflush(stderr); - return -1; - } - char* java_home = strdup(env_java_home); - size_t home_len = strlen(java_home); - if (java_home[home_len - 1] == '\\') { - java_home[home_len - 1] = '\0'; - } - size_t full_len = home_len + sizeof(JDK_JVM_DLL_PATH); - char* full_path = new char[full_len]; - HMODULE jvmDll = NULL; - // Try the JRE path first; - strcpy_s(full_path, full_len, java_home); - strcat_s(full_path, full_len, JRE_JVM_DLL_PATH); - fprintf(stderr, "Trying to find jvm.dll at %s\n", full_path); - fflush(stderr); - jvmDll = LoadLibraryA(full_path); - if (jvmDll == NULL) { - // OK, then try the JDK path - strcpy_s(full_path, full_len, java_home); - strcat_s(full_path, full_len, JDK_JVM_DLL_PATH); - fprintf(stderr, "Trying to find jvm.dll at %s\n", full_path); - fflush(stderr); - jvmDll = LoadLibraryA(full_path); - } - - free(full_path); - free(java_home); - - if (jvmDll == NULL) { - fprintf(stderr, "Could not find the jvm.dll\n"); - fflush(stderr); - return -1; - } - fprintf(stderr, "Found it!\n"); - fflush(stderr); - -#define USE_EXE_AS_JAR -#ifdef USE_EXE_AS_JAR - DWORD fullpath_len = GetFullPathNameA(argv[0], 0, NULL, NULL); - char* fullpath = new char[fullpath_len]; - GetFullPathNameA(argv[0], fullpath_len, fullpath, NULL); - size_t cp_opt_len = sizeof(CP_PREFIX) + strlen(fullpath); - char* cp_opt = new char[cp_opt_len]; - strcpy_s(cp_opt, cp_opt_len, CP_PREFIX); - strcat_s(cp_opt, cp_opt_len, fullpath); - fflush(stderr); -#endif - - JavaVMOption options[2]; - JavaVMInitArgs vm_args = { 0 }; - vm_args.version = JNI_VERSION_1_8; - vm_args.nOptions = sizeof(options)/sizeof(options[0]); - vm_args.options = options; - options[0].optionString = "-Xrs"; -#ifdef USE_EXE_AS_JAR - fprintf(stderr, "Classpath: %s\n", cp_opt); - options[1].optionString = cp_opt; -#else - options[1].optionString = "-Djava.class.path=sctldbgeng.jar"; -#endif - //options[2].optionString = "-verbose:class"; - vm_args.ignoreUnrecognized = false; - CreateJavaVMFunc create_jvm = NULL; - //create_jvm = JNI_CreateJavaVM; - create_jvm = (CreateJavaVMFunc) GetProcAddress(jvmDll, "JNI_CreateJavaVM"); - jint jni_result = create_jvm(&jvm, (void**)&env, &vm_args); - -#ifdef USE_EXE_AS_JAR - free(cp_opt); -#endif - - if (jni_result != JNI_OK) { - jvm = NULL; - fprintf(stderr, "Could not initialize JVM: %d: ", jni_result); - switch (jni_result) { - case JNI_ERR: - fprintf(stderr, "unknown error"); - break; - case JNI_EDETACHED: - fprintf(stderr, "thread detached from the VM"); - break; - case JNI_EVERSION: - fprintf(stderr, "JNI version error"); - break; - case JNI_ENOMEM: - fprintf(stderr, "not enough memory"); - break; - case JNI_EEXIST: - fprintf(stderr, "VM already created"); - break; - case JNI_EINVAL: - fprintf(stderr, "invalid arguments"); - break; - } - fprintf(stderr, "\n"); - fflush(stderr); - return -1; - } - - jclass mainCls = env->FindClass(MAIN_CLASS); - if (mainCls == NULL) { - fprintf(stderr, "Could not find main class: %s\n", MAIN_CLASS); - jvm->DestroyJavaVM(); - return -1; - } - - jmethodID mainMeth = env->GetStaticMethodID(mainCls, "main", "([Ljava/lang/String;)V"); - if (mainMeth == NULL) { - fprintf(stderr, "No main(String[] args) method in main class\n"); - jvm->DestroyJavaVM(); - return -1; - } - - jclass stringCls = env->FindClass("java/lang/String"); - - jobjectArray jargs = env->NewObjectArray(argc - 1, stringCls, NULL); - for (int i = 1; i < argc; i++) { - jstring a = env->NewStringUTF(argv[i]); - if (a == NULL) { - fprintf(stderr, "Could not create Java string for arguments.\n"); - jvm->DestroyJavaVM(); - return -1; - } - env->SetObjectArrayElement(jargs, i - 1, a); - } - - env->CallStaticVoidMethod(mainCls, mainMeth, (jvalue*) jargs); - - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); - } - - jvm->DestroyJavaVM(); - - return 0; -} - -int main(int argc, char** argv) { - main_sctldbg(argc, argv); -} - diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java new file mode 100644 index 0000000000..ce12b293c9 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractScriptTraceRmiLaunchOffer.java @@ -0,0 +1,109 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.tracermi.launcher; + +import java.io.File; +import java.net.SocketAddress; +import java.util.*; + +import javax.swing.Icon; + +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes; +import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.debug.api.tracermi.TerminalSession; +import ghidra.program.model.listing.Program; +import ghidra.util.HelpLocation; +import ghidra.util.task.TaskMonitor; + +public abstract class AbstractScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer { + + protected final File script; + protected final String configName; + protected final ScriptAttributes attrs; + + public AbstractScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program, + File script, String configName, ScriptAttributes attrs) { + super(plugin, program); + this.script = script; + this.configName = configName; + this.attrs = attrs; + } + + @Override + public String getConfigName() { + return configName; + } + + @Override + public String getTitle() { + return attrs.title(); + } + + @Override + public String getDescription() { + return attrs.description(); + } + + @Override + public List getMenuPath() { + return attrs.menuPath(); + } + + @Override + public String getMenuGroup() { + return attrs.menuGroup(); + } + + @Override + public String getMenuOrder() { + return attrs.menuOrder(); + } + + @Override + public Icon getIcon() { + return attrs.icon(); + } + + @Override + public HelpLocation getHelpLocation() { + return attrs.helpLocation(); + } + + @Override + public Map> getParameters() { + return attrs.parameters(); + } + + protected abstract void prepareSubprocess(List commandLine, Map env, + Map args, SocketAddress address); + + @Override + protected void launchBackEnd(TaskMonitor monitor, Map sessions, + Map args, SocketAddress address) throws Exception { + List commandLine = new ArrayList<>(); + Map env = new HashMap<>(System.getenv()); + prepareSubprocess(commandLine, env, args, address); + + for (String tty : attrs.extraTtys()) { + NullPtyTerminalSession ns = nullPtyTerminal(); + env.put(tty, ns.name()); + sessions.put(ns.name(), ns); + } + + sessions.put("Shell", + runInTerminal(commandLine, env, script.getParentFile(), sessions.values())); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java index a223d5a894..dfcab73170 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java @@ -420,8 +420,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } protected PtyTerminalSession runInTerminal(List commandLine, Map env, - Collection subordinates) - throws IOException { + File workingDirectory, Collection subordinates) throws IOException { PtyFactory factory = getPtyFactory(); Pty pty = factory.openpty(); @@ -432,13 +431,19 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer TerminalListener resizeListener = new TerminalListener() { @Override public void resized(short cols, short rows) { - parent.setWindowSize(cols, rows); + try { + parent.setWindowSize(cols, rows); + } + catch (Exception e) { + Msg.error(this, "Could not resize pty: " + e); + } } }; terminal.addTerminalListener(resizeListener); env.put("TERM", "xterm-256color"); - PtySession session = pty.getChild().session(commandLine.toArray(String[]::new), env); + PtySession session = + pty.getChild().session(commandLine.toArray(String[]::new), env, workingDirectory); Thread waiter = new Thread(() -> { try { diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOpinion.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOpinion.java new file mode 100644 index 0000000000..def81a2db9 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOpinion.java @@ -0,0 +1,60 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.tracermi.launcher; + +import java.util.stream.Stream; + +import generic.jar.ResourceFile; +import ghidra.app.plugin.core.debug.DebuggerPluginPackage; +import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion; +import ghidra.framework.Application; +import ghidra.framework.options.OptionType; +import ghidra.framework.options.Options; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginUtils; +import ghidra.util.HelpLocation; + +public abstract class AbstractTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion { + + @Override + public void registerOptions(Options options) { + String pluginName = PluginUtils.getPluginNameFromClass(TraceRmiLauncherServicePlugin.class); + options.registerOption(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS, + OptionType.STRING_TYPE, "", new HelpLocation(pluginName, "options"), + "Paths to search for user-created debugger launchers", new ScriptPathsPropertyEditor()); + } + + @Override + public boolean requiresRefresh(String optionName) { + return TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS.equals(optionName); + } + + protected Stream getModuleScriptPaths() { + return Application.findModuleSubDirectories("data/debugger-launchers").stream(); + } + + protected Stream getUserScriptPaths(PluginTool tool) { + Options options = tool.getOptions(DebuggerPluginPackage.NAME); + String scriptPaths = + options.getString(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS, ""); + return scriptPaths.lines().filter(d -> !d.isBlank()).map(ResourceFile::new); + } + + protected Stream getScriptPaths(PluginTool tool) { + return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool)); + } + +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java new file mode 100644 index 0000000000..b883ae3d01 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOffer.java @@ -0,0 +1,70 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.tracermi.launcher; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.SocketAddress; +import java.util.List; +import java.util.Map; + +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes; +import ghidra.program.model.listing.Program; + +/** + * A launcher implemented by a simple DOS/Windows batch file. + * + *

+ * The script must start with an attributes header in a comment block. + */ +public class BatchScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer { + public static final String REM = "::"; + public static final int REM_LEN = REM.length(); + + public static BatchScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin, + Program program, File script) throws FileNotFoundException { + ScriptAttributesParser parser = new ScriptAttributesParser() { + @Override + protected boolean ignoreLine(int lineNo, String line) { + return line.isBlank(); + } + + @Override + protected String removeDelimiter(String line) { + String stripped = line.stripLeading(); + if (!stripped.startsWith(REM)) { + return null; + } + return stripped.substring(REM_LEN); + } + }; + ScriptAttributes attrs = parser.parseFile(script); + return new BatchScriptTraceRmiLaunchOffer(plugin, program, script, + "BATCH_FILE:" + script.getName(), attrs); + } + + private BatchScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program, + File script, String configName, ScriptAttributes attrs) { + super(plugin, program, script, configName, attrs); + } + + @Override + protected void prepareSubprocess(List commandLine, Map env, + Map args, SocketAddress address) { + ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args, + address); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOpinion.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOpinion.java new file mode 100644 index 0000000000..c3bc5da027 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/BatchScriptTraceRmiLaunchOpinion.java @@ -0,0 +1,49 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.tracermi.launcher; + +import java.util.Collection; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import generic.jar.ResourceFile; +import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; + +public class BatchScriptTraceRmiLaunchOpinion extends AbstractTraceRmiLaunchOpinion { + + @Override + public Collection getOffers(TraceRmiLauncherServicePlugin plugin, + Program program) { + return getScriptPaths(plugin.getTool()) + .flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".bat")))) + .flatMap(sf -> createOffer(plugin, program, sf)) + .collect(Collectors.toList()); + } + + protected Stream createOffer(TraceRmiLauncherServicePlugin plugin, + Program program, ResourceFile scriptFile) { + try { + return Stream.of( + BatchScriptTraceRmiLaunchOffer.create(plugin, program, scriptFile.getFile(false))); + } + catch (Exception e) { + Msg.error(this, "Could not offer " + scriptFile + ": " + e.getMessage(), e); + return Stream.of(); + } + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java index 09bbcb8957..c032773ce1 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/LaunchAction.java @@ -88,6 +88,9 @@ public class LaunchAction extends MultiActionDockingAction { ConfigLast findMostRecentConfig() { Program program = plugin.currentProgram; + if (program == null) { + return null; + } ConfigLast best = null; ProgramUserData userData = program.getProgramUserData(); @@ -113,14 +116,16 @@ public class LaunchAction extends MultiActionDockingAction { List actions = new ArrayList<>(); - ProgramUserData userData = program.getProgramUserData(); Map saved = new HashMap<>(); - for (String propName : userData.getStringPropertyNames()) { - ConfigLast check = checkSavedConfig(userData, propName); - if (check == null) { - continue; + if (program != null) { + ProgramUserData userData = program.getProgramUserData(); + for (String propName : userData.getStringPropertyNames()) { + ConfigLast check = checkSavedConfig(userData, propName); + if (check == null) { + continue; + } + saved.put(check.configName, check.last); } - saved.put(check.configName, check.last); } for (TraceRmiLaunchOffer offer : offers) { @@ -134,6 +139,8 @@ public class LaunchAction extends MultiActionDockingAction { .build()); Long last = saved.get(offer.getConfigName()); if (last == null) { + // NB. If program == null, this will always happen. + // Thus, no worries about program.getName() below. continue; } actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName()) @@ -172,6 +179,11 @@ public class LaunchAction extends MultiActionDockingAction { // Make accessible to this file return super.showPopup(); } + + @Override + public String getToolTipText() { + return getDescription(); + } } @Override @@ -180,19 +192,45 @@ public class LaunchAction extends MultiActionDockingAction { } @Override - public void actionPerformed(ActionContext context) { - // See comment on super method about use of runLater - ConfigLast last = findMostRecentConfig(); + public boolean isEnabledForContext(ActionContext context) { + return plugin.currentProgram != null; + } + + protected TraceRmiLaunchOffer findOffer(ConfigLast last) { if (last == null) { - Swing.runLater(() -> button.showPopup()); - return; + return null; } for (TraceRmiLaunchOffer offer : plugin.getOffers(plugin.currentProgram)) { if (offer.getConfigName().equals(last.configName)) { - plugin.relaunch(offer); - return; + return offer; } } - Swing.runLater(() -> button.showPopup()); + return null; + } + + @Override + public void actionPerformed(ActionContext context) { + // See comment on super method about use of runLater + ConfigLast last = findMostRecentConfig(); + TraceRmiLaunchOffer offer = findOffer(last); + if (offer == null) { + Swing.runLater(() -> button.showPopup()); + return; + } + plugin.relaunch(offer); + } + + @Override + public String getDescription() { + Program program = plugin.currentProgram; + if (program == null) { + return "Launch (program required)"; + } + ConfigLast last = findMostRecentConfig(); + TraceRmiLaunchOffer offer = findOffer(last); + if (last == null) { + return "Configure and launch " + program.getName(); + } + return "Re-launch " + program.getName() + " using " + offer.getTitle(); } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java new file mode 100644 index 0000000000..cf22772c62 --- /dev/null +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/ScriptAttributesParser.java @@ -0,0 +1,569 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.debug.gui.tracermi.launcher; + +import java.io.*; +import java.math.BigInteger; +import java.net.*; +import java.util.*; +import java.util.Map.Entry; + +import javax.swing.Icon; + +import generic.theme.GIcon; +import generic.theme.Gui; +import ghidra.dbg.target.TargetMethod.ParameterDescription; +import ghidra.dbg.util.ShellUtils; +import ghidra.framework.Application; +import ghidra.util.HelpLocation; +import ghidra.util.Msg; + +/** + * Some attributes are required. Others are optional: + *

    + *
  • {@code @menu-path}: (Required)
  • + *
+ * + */ +public abstract class ScriptAttributesParser { + public static final String AT_TITLE = "@title"; + public static final String AT_DESC = "@desc"; + public static final String AT_MENU_PATH = "@menu-path"; + public static final String AT_MENU_GROUP = "@menu-group"; + public static final String AT_MENU_ORDER = "@menu-order"; + public static final String AT_ICON = "@icon"; + public static final String AT_HELP = "@help"; + public static final String AT_ENUM = "@enum"; + public static final String AT_ENV = "@env"; + public static final String AT_ARG = "@arg"; + public static final String AT_ARGS = "@args"; + public static final String AT_TTY = "@tty"; + + public static final String PREFIX_ENV = "env:"; + public static final String PREFIX_ARG = "arg:"; + public static final String KEY_ARGS = "args"; + + public static final String MSGPAT_INVALID_HELP_SYNTAX = + "%s: Invalid %s syntax. Use Topic#anchor"; + public static final String MSGPAT_INVALID_ENUM_SYNTAX = + "%s: Invalid %s syntax. Use NAME:type Choice1 [ChoiceN...]"; + public static final String MSGPAT_INVALID_ENV_SYNTAX = + "%s: Invalid %s syntax. Use NAME:type=default \"Display\" \"Tool Tip\""; + public static final String MSGPAT_INVALID_ARG_SYNTAX = + "%s: Invalid %s syntax. Use :type \"Display\" \"Tool Tip\""; + public static final String MSGPAT_INVALID_ARGS_SYNTAX = + "%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\""; + + protected record Location(String fileName, int lineNo) { + @Override + public String toString() { + return "%s:%d".formatted(fileName, lineNo); + } + } + + protected interface OptType { + static OptType parse(Location loc, String typeName, + Map> userEnums) { + OptType type = switch (typeName) { + case "str" -> BaseType.STRING; + case "int" -> BaseType.INT; + case "bool" -> BaseType.BOOL; + default -> userEnums.get(typeName); + }; + if (type == null) { + Msg.error(ScriptAttributesParser.class, + "%s: Invalid type %s".formatted(loc, typeName)); + return null; + } + return type; + } + + default TypeAndDefault withCastDefault(Object defaultValue) { + return new TypeAndDefault<>(this, cls().cast(defaultValue)); + } + + Class cls(); + + T decode(Location loc, String str); + + ParameterDescription createParameter(String name, T defaultValue, String display, + String description); + } + + protected interface BaseType extends OptType { + public static BaseType parse(Location loc, String typeName) { + BaseType type = switch (typeName) { + case "str" -> BaseType.STRING; + case "int" -> BaseType.INT; + case "bool" -> BaseType.BOOL; + default -> null; + }; + if (type == null) { + Msg.error(ScriptAttributesParser.class, + "%s: Invalid base type %s".formatted(loc, typeName)); + return null; + } + return type; + } + + public static final BaseType STRING = new BaseType<>() { + @Override + public Class cls() { + return String.class; + } + + @Override + public String decode(Location loc, String str) { + return str; + } + }; + + public static final BaseType INT = new BaseType<>() { + @Override + public Class cls() { + return BigInteger.class; + } + + @Override + public BigInteger decode(Location loc, String str) { + try { + if (str.startsWith("0x")) { + return new BigInteger(str.substring(2), 16); + } + return new BigInteger(str); + } + catch (NumberFormatException e) { + Msg.error(ScriptAttributesParser.class, + ("%s: Invalid int for %s: %s. You may prefix with 0x for hexadecimal. " + + "Otherwise, decimal is used.").formatted(loc, AT_ENV, str)); + return null; + } + } + }; + + public static final BaseType BOOL = new BaseType<>() { + @Override + public Class cls() { + return Boolean.class; + } + + @Override + public Boolean decode(Location loc, String str) { + Boolean result = switch (str) { + case "true" -> true; + case "false" -> false; + default -> null; + }; + if (result == null) { + Msg.error(ScriptAttributesParser.class, + "%s: Invalid bool for %s: %s. Only true or false (in lower case) is allowed." + .formatted(loc, AT_ENV, str)); + return null; + } + return result; + } + }; + + default UserType withCastChoices(List choices) { + return new UserType<>(this, choices.stream().map(cls()::cast).toList()); + } + + @Override + default ParameterDescription createParameter(String name, T defaultValue, String display, + String description) { + return ParameterDescription.create(cls(), name, false, defaultValue, display, + description); + } + } + + protected record UserType(BaseType base, List choices) implements OptType { + @Override + public Class cls() { + return base.cls(); + } + + @Override + public T decode(Location loc, String str) { + return base.decode(loc, str); + } + + @Override + public ParameterDescription createParameter(String name, T defaultValue, String display, + String description) { + return ParameterDescription.choices(cls(), name, choices, defaultValue, display, + description); + } + } + + protected record TypeAndDefault(OptType type, T defaultValue) { + public static TypeAndDefault parse(Location loc, String typeName, String defaultString, + Map> userEnums) { + OptType tac = OptType.parse(loc, typeName, userEnums); + if (tac == null) { + return null; + } + Object value = tac.decode(loc, defaultString); + if (value == null) { + return null; + } + return tac.withCastDefault(value); + } + + public ParameterDescription createParameter(String name, String display, + String description) { + return type.createParameter(name, defaultValue, display, description); + } + } + + protected static String addrToString(InetAddress address) { + if (address.isAnyLocalAddress()) { + return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost. + } + return address.getHostAddress(); + } + + protected static String sockToString(SocketAddress address) { + if (address instanceof InetSocketAddress tcp) { + return addrToString(tcp.getAddress()) + ":" + tcp.getPort(); + } + throw new AssertionError("Unhandled address type " + address); + } + + public record ScriptAttributes(String title, String description, List menuPath, + String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation, + Map> parameters, Collection extraTtys) { + } + + /** + * Convert an arguments map into a command line and environment variables + * + * @param commandLine a mutable list to add command line parameters into + * @param env a mutable map to place environment variables into. This should likely be + * initialized to {@link System#getenv()} so that Ghidra's environment is inherited + * by the script's process. + * @param script the script file + * @param parameters the descriptions of the parameters + * @param args the arguments to process + * @param address the address of the listening TraceRmi socket + */ + public static void processArguments(List commandLine, Map env, + File script, Map> parameters, Map args, + SocketAddress address) { + + commandLine.add(script.getAbsolutePath()); + env.put("GHIDRA_HOME", Application.getInstallationDirectory().getAbsolutePath()); + if (address != null) { + env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address)); + if (address instanceof InetSocketAddress tcp) { + env.put("GHIDRA_TRACE_RMI_HOST", tcp.getAddress().toString()); + env.put("GHIDRA_TRACE_RMI_PORT", Integer.toString(tcp.getPort())); + } + } + + ParameterDescription paramDesc; + for (int i = 1; (paramDesc = parameters.get("arg:" + i)) != null; i++) { + commandLine.add(Objects.toString(paramDesc.get(args))); + } + + paramDesc = parameters.get("args"); + if (paramDesc != null) { + commandLine.addAll(ShellUtils.parseArgs((String) paramDesc.get(args))); + } + + for (Entry> ent : parameters.entrySet()) { + String key = ent.getKey(); + if (key.startsWith(PREFIX_ENV)) { + String varName = key.substring(PREFIX_ENV.length()); + env.put(varName, Objects.toString(ent.getValue().get(args))); + } + } + } + + private int argc = 0; + private String title; + private StringBuilder description; + private List menuPath; + private String menuGroup; + private String menuOrder; + private String iconId; + private HelpLocation helpLocation; + private final Map> userTypes = new HashMap<>(); + private final Map> parameters = new LinkedHashMap<>(); + private final Set extraTtys = new LinkedHashSet<>(); + + /** + * Check if a line should just be ignored, e.g., blank lines, or the "shebang" line on UNIX. + * + * @param lineNo the line number, counting 1 up + * @param line the full line, excluding the new-line characters + * @return true to ignore, false to parse + */ + protected abstract boolean ignoreLine(int lineNo, String line); + + /** + * Check if a line is a comment and extract just the comment + * + *

+ * If null is returned, the parser assumes the attributes header is ended + * + * @param line the full line, excluding the new-line characters + * @return the comment, or null if the line is not a comment + */ + protected abstract String removeDelimiter(String line); + + public ScriptAttributes parseFile(File script) throws FileNotFoundException { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(new FileInputStream(script)))) { + String line; + for (int lineNo = 1; (line = reader.readLine()) != null; lineNo++) { + if (ignoreLine(lineNo, line)) { + continue; + } + String comment = removeDelimiter(line); + if (comment == null) { + break; + } + parseComment(new Location(script.getName(), lineNo), comment); + } + return validate(script.getName()); + } + catch (FileNotFoundException e) { + // Avoid capture by IOException + throw e; + } + catch (IOException e) { + throw new AssertionError(e); + } + } + + /** + * Process a line in the metadata comment block + * + * @param line the line, excluding any comment delimiters + */ + public void parseComment(Location loc, String comment) { + if (comment.isBlank()) { + return; + } + String[] parts = comment.split("\\s+", 2); + if (!parts[0].startsWith("@")) { + return; + } + if (parts.length < 2) { + Msg.error(this, "%s: Too few tokens: %s".formatted(loc, comment)); + return; + } + switch (parts[0].trim()) { + case AT_TITLE -> parseTitle(loc, parts[1]); + case AT_DESC -> parseDesc(loc, parts[1]); + case AT_MENU_PATH -> parseMenuPath(loc, parts[1]); + case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]); + case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]); + case AT_ICON -> parseIcon(loc, parts[1]); + case AT_HELP -> parseHelp(loc, parts[1]); + case AT_ENUM -> parseEnum(loc, parts[1]); + case AT_ENV -> parseEnv(loc, parts[1]); + case AT_ARG -> parseArg(loc, parts[1], ++argc); + case AT_ARGS -> parseArgs(loc, parts[1]); + case AT_TTY -> parseTty(loc, parts[1]); + default -> parseUnrecognized(loc, comment); + } + } + + protected void parseTitle(Location loc, String str) { + if (title != null) { + Msg.warn(this, "%s: Duplicate @title".formatted(loc)); + } + title = str; + } + + protected void parseDesc(Location loc, String str) { + if (description == null) { + description = new StringBuilder(); + } + description.append(str); + description.append("\n"); + } + + protected void parseMenuPath(Location loc, String str) { + if (menuPath != null) { + Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_PATH)); + } + menuPath = List.of(str.trim().split("\\.")); + if (menuPath.isEmpty()) { + Msg.error(this, + "%s: Empty %s. Ignoring.".formatted(loc, AT_MENU_PATH)); + } + } + + protected void parseMenuGroup(Location loc, String str) { + if (menuGroup != null) { + Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_GROUP)); + } + menuGroup = str; + } + + protected void parseMenuOrder(Location loc, String str) { + if (menuOrder != null) { + Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_ORDER)); + } + menuOrder = str; + } + + protected void parseIcon(Location loc, String str) { + if (iconId != null) { + Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_ICON)); + } + iconId = str.trim(); + if (!Gui.hasIcon(iconId)) { + Msg.error(this, + "%s: Icon id %s not registered in the theme".formatted(loc, iconId)); + } + } + + protected void parseHelp(Location loc, String str) { + if (helpLocation != null) { + Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_HELP)); + } + String[] parts = str.trim().split("#", 2); + if (parts.length != 2) { + Msg.error(this, MSGPAT_INVALID_HELP_SYNTAX.formatted(loc, AT_HELP)); + return; + } + helpLocation = new HelpLocation(parts[0].trim(), parts[1].trim()); + } + + protected void parseEnum(Location loc, String str) { + List parts = ShellUtils.parseArgs(str); + if (parts.size() < 2) { + Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM)); + return; + } + String[] nameParts = parts.get(0).split(":", 2); + if (nameParts.length != 2) { + Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM)); + return; + } + String name = nameParts[0].trim(); + BaseType baseType = BaseType.parse(loc, nameParts[1]); + if (baseType == null) { + return; + } + List choices = parts.stream().skip(1).map(s -> baseType.decode(loc, s)).toList(); + if (choices.contains(null)) { + return; + } + UserType userType = baseType.withCastChoices(choices); + if (userTypes.put(name, userType) != null) { + Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENUM, name)); + } + } + + protected void parseEnv(Location loc, String str) { + List parts = ShellUtils.parseArgs(str); + if (parts.size() != 3) { + Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); + return; + } + String[] nameParts = parts.get(0).split(":", 2); + if (nameParts.length != 2) { + Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); + return; + } + String trimmed = nameParts[0].trim(); + String name = PREFIX_ENV + trimmed; + String[] tadParts = nameParts[1].split("=", 2); + if (tadParts.length != 2) { + Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); + return; + } + TypeAndDefault tad = + TypeAndDefault.parse(loc, tadParts[0].trim(), tadParts[1].trim(), userTypes); + ParameterDescription param = tad.createParameter(name, parts.get(1), parts.get(2)); + if (parameters.put(name, param) != null) { + Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENV, trimmed)); + } + } + + protected void parseArg(Location loc, String str, int argNum) { + List parts = ShellUtils.parseArgs(str); + if (parts.size() != 3) { + Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG)); + return; + } + String colonType = parts.get(0).trim(); + if (!colonType.startsWith(":")) { + Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG)); + return; + } + OptType type = OptType.parse(loc, colonType.substring(1), userTypes); + if (type == null) { + return; + } + String name = PREFIX_ARG + argNum; + parameters.put(name, ParameterDescription.create(type.cls(), name, true, null, + parts.get(1), parts.get(2))); + } + + protected void parseArgs(Location loc, String str) { + List parts = ShellUtils.parseArgs(str); + if (parts.size() != 2) { + Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS)); + return; + } + ParameterDescription parameter = ParameterDescription.create(String.class, + "args", false, "", parts.get(0), parts.get(1)); + if (parameters.put(KEY_ARGS, parameter) != null) { + Msg.warn(this, "%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS)); + } + } + + protected void parseTty(Location loc, String str) { + if (!extraTtys.add(str)) { + Msg.warn(this, "%s: Duplicate %s. Ignored".formatted(loc, AT_TTY)); + } + } + + protected void parseUnrecognized(Location loc, String line) { + Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line)); + } + + protected ScriptAttributes validate(String fileName) { + if (title == null) { + Msg.error(this, "%s is required. Using script file name.".formatted(AT_TITLE)); + title = fileName; + } + if (menuPath == null) { + menuPath = List.of(title); + } + if (menuGroup == null) { + menuGroup = ""; + } + if (menuOrder == null) { + menuOrder = ""; + } + if (iconId == null) { + iconId = "icon.debugger"; + } + return new ScriptAttributes(title, getDescription(), List.copyOf(menuPath), menuGroup, + menuOrder, new GIcon(iconId), helpLocation, + Collections.unmodifiableMap(new LinkedHashMap<>(parameters)), List.copyOf(extraTtys)); + } + + private String getDescription() { + return description == null ? null : description.toString(); + } +} diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java index 787eb7a54a..bb9a379cd0 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOffer.java @@ -15,454 +15,25 @@ */ package ghidra.app.plugin.core.debug.gui.tracermi.launcher; -import java.io.*; -import java.math.BigInteger; -import java.net.*; -import java.util.*; -import java.util.Map.Entry; +import java.io.File; +import java.io.FileNotFoundException; +import java.net.SocketAddress; +import java.util.List; +import java.util.Map; -import javax.swing.Icon; - -import generic.theme.GIcon; -import generic.theme.Gui; -import ghidra.dbg.target.TargetMethod.ParameterDescription; -import ghidra.dbg.util.ShellUtils; -import ghidra.debug.api.tracermi.TerminalSession; -import ghidra.framework.Application; +import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes; import ghidra.program.model.listing.Program; -import ghidra.util.HelpLocation; -import ghidra.util.Msg; -import ghidra.util.task.TaskMonitor; /** * A launcher implemented by a simple UNIX shell script. * *

- * The script must start with an attributes header in a comment block. Some attributes are required. - * Others are optional: - *

    - *
  • {@code @menu-path}: (Required)
  • - *
+ * The script must start with an attributes header in a comment block. See + * {@link ScriptAttributesParser}. */ -public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer { +public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer { public static final String SHEBANG = "#!"; - public static final String AT_TITLE = "@title"; - public static final String AT_DESC = "@desc"; - public static final String AT_MENU_PATH = "@menu-path"; - public static final String AT_MENU_GROUP = "@menu-group"; - public static final String AT_MENU_ORDER = "@menu-order"; - public static final String AT_ICON = "@icon"; - public static final String AT_HELP = "@help"; - public static final String AT_ENUM = "@enum"; - public static final String AT_ENV = "@env"; - public static final String AT_ARG = "@arg"; - public static final String AT_ARGS = "@args"; - public static final String AT_TTY = "@tty"; - - public static final String PREFIX_ENV = "env:"; - public static final String PREFIX_ARG = "arg:"; - public static final String KEY_ARGS = "args"; - - public static final String MSGPAT_INVALID_HELP_SYNTAX = - "%s: Invalid %s syntax. Use Topic#anchor"; - public static final String MSGPAT_INVALID_ENUM_SYNTAX = - "%s: Invalid %s syntax. Use NAME:type Choice1 [ChoiceN...]"; - public static final String MSGPAT_INVALID_ENV_SYNTAX = - "%s: Invalid %s syntax. Use NAME:type=default \"Display\" \"Tool Tip\""; - public static final String MSGPAT_INVALID_ARG_SYNTAX = - "%s: Invalid %s syntax. Use :type \"Display\" \"Tool Tip\""; - public static final String MSGPAT_INVALID_ARGS_SYNTAX = - "%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\""; - - protected record Location(String fileName, int lineNo) { - @Override - public String toString() { - return "%s:%d".formatted(fileName, lineNo); - } - } - - protected interface OptType { - static OptType parse(Location loc, String typeName, - Map> userEnums) { - OptType type = switch (typeName) { - case "str" -> BaseType.STRING; - case "int" -> BaseType.INT; - case "bool" -> BaseType.BOOL; - default -> userEnums.get(typeName); - }; - if (type == null) { - Msg.error(AttributesParser.class, "%s: Invalid type %s".formatted(loc, typeName)); - return null; - } - return type; - } - - default TypeAndDefault withCastDefault(Object defaultValue) { - return new TypeAndDefault<>(this, cls().cast(defaultValue)); - } - - Class cls(); - - T decode(Location loc, String str); - - ParameterDescription createParameter(String name, T defaultValue, String display, - String description); - } - - protected interface BaseType extends OptType { - static BaseType parse(Location loc, String typeName) { - BaseType type = switch (typeName) { - case "str" -> BaseType.STRING; - case "int" -> BaseType.INT; - case "bool" -> BaseType.BOOL; - default -> null; - }; - if (type == null) { - Msg.error(AttributesParser.class, - "%s: Invalid base type %s".formatted(loc, typeName)); - return null; - } - return type; - } - - public static final BaseType STRING = new BaseType<>() { - @Override - public Class cls() { - return String.class; - } - - @Override - public String decode(Location loc, String str) { - return str; - } - }; - public static final BaseType INT = new BaseType<>() { - @Override - public Class cls() { - return BigInteger.class; - } - - @Override - public BigInteger decode(Location loc, String str) { - try { - if (str.startsWith("0x")) { - return new BigInteger(str.substring(2), 16); - } - return new BigInteger(str); - } - catch (NumberFormatException e) { - Msg.error(AttributesParser.class, - ("%s: Invalid int for %s: %s. You may prefix with 0x for hexadecimal. " + - "Otherwise, decimal is used.").formatted(loc, AT_ENV, str)); - return null; - } - } - }; - public static final BaseType BOOL = new BaseType<>() { - @Override - public Class cls() { - return Boolean.class; - } - - @Override - public Boolean decode(Location loc, String str) { - Boolean result = switch (str) { - case "true" -> true; - case "false" -> false; - default -> null; - }; - if (result == null) { - Msg.error(AttributesParser.class, - "%s: Invalid bool for %s: %s. Only true or false (in lower case) is allowed." - .formatted(loc, AT_ENV, str)); - return null; - } - return result; - } - }; - - default UserType withCastChoices(List choices) { - return new UserType<>(this, choices.stream().map(cls()::cast).toList()); - } - - @Override - default ParameterDescription createParameter(String name, T defaultValue, String display, - String description) { - return ParameterDescription.create(cls(), name, false, defaultValue, display, - description); - } - } - - protected record UserType (BaseType base, List choices) implements OptType { - @Override - public Class cls() { - return base.cls(); - } - - @Override - public T decode(Location loc, String str) { - return base.decode(loc, str); - } - - @Override - public ParameterDescription createParameter(String name, T defaultValue, String display, - String description) { - return ParameterDescription.choices(cls(), name, choices, defaultValue, display, - description); - } - } - - protected record TypeAndDefault (OptType type, T defaultValue) { - public static TypeAndDefault parse(Location loc, String typeName, String defaultString, - Map> userEnums) { - OptType tac = OptType.parse(loc, typeName, userEnums); - if (tac == null) { - return null; - } - Object value = tac.decode(loc, defaultString); - if (value == null) { - return null; - } - return tac.withCastDefault(value); - } - - public ParameterDescription createParameter(String name, String display, - String description) { - return type.createParameter(name, defaultValue, display, description); - } - } - - protected static class AttributesParser { - protected int argc = 0; - protected String title; - protected StringBuilder description; - protected List menuPath; - protected String menuGroup; - protected String menuOrder; - protected String iconId; - protected HelpLocation helpLocation; - protected final Map> userTypes = new HashMap<>(); - protected final Map> parameters = new LinkedHashMap<>(); - protected final Set extraTtys = new LinkedHashSet<>(); - - /** - * Process a line in the metadata comment block - * - * @param line the line, excluding any comment delimiters - */ - public void parseLine(Location loc, String line) { - String afterHash = line.stripLeading().substring(1); - if (afterHash.isBlank()) { - return; - } - String[] parts = afterHash.split("\\s+", 2); - if (!parts[0].startsWith("@")) { - return; - } - if (parts.length < 2) { - Msg.error(this, "%s: Too few tokens: %s".formatted(loc, line)); - return; - } - switch (parts[0].trim()) { - case AT_TITLE -> parseTitle(loc, parts[1]); - case AT_DESC -> parseDesc(loc, parts[1]); - case AT_MENU_PATH -> parseMenuPath(loc, parts[1]); - case AT_MENU_GROUP -> parseMenuGroup(loc, parts[1]); - case AT_MENU_ORDER -> parseMenuOrder(loc, parts[1]); - case AT_ICON -> parseIcon(loc, parts[1]); - case AT_HELP -> parseHelp(loc, parts[1]); - case AT_ENUM -> parseEnum(loc, parts[1]); - case AT_ENV -> parseEnv(loc, parts[1]); - case AT_ARG -> parseArg(loc, parts[1], ++argc); - case AT_ARGS -> parseArgs(loc, parts[1]); - case AT_TTY -> parseTty(loc, parts[1]); - default -> parseUnrecognized(loc, line); - } - } - - protected void parseTitle(Location loc, String str) { - if (title != null) { - Msg.warn(this, "%s: Duplicate @title".formatted(loc)); - } - title = str; - } - - protected void parseDesc(Location loc, String str) { - if (description == null) { - description = new StringBuilder(); - } - description.append(str); - description.append("\n"); - } - - protected void parseMenuPath(Location loc, String str) { - if (menuPath != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_PATH)); - } - menuPath = List.of(str.trim().split("\\.")); - if (menuPath.isEmpty()) { - Msg.error(this, - "%s: Empty %s. Ignoring.".formatted(loc, AT_MENU_PATH)); - } - } - - protected void parseMenuGroup(Location loc, String str) { - if (menuGroup != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_GROUP)); - } - menuGroup = str; - } - - protected void parseMenuOrder(Location loc, String str) { - if (menuOrder != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_MENU_ORDER)); - } - menuOrder = str; - } - - protected void parseIcon(Location loc, String str) { - if (iconId != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_ICON)); - } - iconId = str.trim(); - if (!Gui.hasIcon(iconId)) { - Msg.error(this, - "%s: Icon id %s not registered in the theme".formatted(loc, iconId)); - } - } - - protected void parseHelp(Location loc, String str) { - if (helpLocation != null) { - Msg.warn(this, "%s: Duplicate %s".formatted(loc, AT_HELP)); - } - String[] parts = str.trim().split("#", 2); - if (parts.length != 2) { - Msg.error(this, MSGPAT_INVALID_HELP_SYNTAX.formatted(loc, AT_HELP)); - return; - } - helpLocation = new HelpLocation(parts[0].trim(), parts[1].trim()); - } - - protected void parseEnum(Location loc, String str) { - List parts = ShellUtils.parseArgs(str); - if (parts.size() < 2) { - Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM)); - return; - } - String[] nameParts = parts.get(0).split(":", 2); - if (nameParts.length != 2) { - Msg.error(this, MSGPAT_INVALID_ENUM_SYNTAX.formatted(loc, AT_ENUM)); - return; - } - String name = nameParts[0].trim(); - BaseType baseType = BaseType.parse(loc, nameParts[1]); - if (baseType == null) { - return; - } - List choices = parts.stream().skip(1).map(s -> baseType.decode(loc, s)).toList(); - if (choices.contains(null)) { - return; - } - UserType userType = baseType.withCastChoices(choices); - if (userTypes.put(name, userType) != null) { - Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENUM, name)); - } - } - - protected void parseEnv(Location loc, String str) { - List parts = ShellUtils.parseArgs(str); - if (parts.size() != 3) { - Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); - return; - } - String[] nameParts = parts.get(0).split(":", 2); - if (nameParts.length != 2) { - Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); - return; - } - String trimmed = nameParts[0].trim(); - String name = PREFIX_ENV + trimmed; - String[] tadParts = nameParts[1].split("=", 2); - if (tadParts.length != 2) { - Msg.error(this, MSGPAT_INVALID_ENV_SYNTAX.formatted(loc, AT_ENV)); - return; - } - TypeAndDefault tad = - TypeAndDefault.parse(loc, tadParts[0].trim(), tadParts[1].trim(), userTypes); - ParameterDescription param = tad.createParameter(name, parts.get(1), parts.get(2)); - if (parameters.put(name, param) != null) { - Msg.warn(this, "%s: Duplicate %s %s. Replaced.".formatted(loc, AT_ENV, trimmed)); - } - } - - protected void parseArg(Location loc, String str, int argNum) { - List parts = ShellUtils.parseArgs(str); - if (parts.size() != 3) { - Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG)); - return; - } - String colonType = parts.get(0).trim(); - if (!colonType.startsWith(":")) { - Msg.error(this, MSGPAT_INVALID_ARG_SYNTAX.formatted(loc, AT_ARG)); - return; - } - OptType type = OptType.parse(loc, colonType.substring(1), userTypes); - if (type == null) { - return; - } - String name = PREFIX_ARG + argNum; - parameters.put(name, ParameterDescription.create(type.cls(), name, true, null, - parts.get(1), parts.get(2))); - } - - protected void parseArgs(Location loc, String str) { - List parts = ShellUtils.parseArgs(str); - if (parts.size() != 2) { - Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS)); - return; - } - ParameterDescription parameter = ParameterDescription.create(String.class, - "args", false, "", parts.get(0), parts.get(1)); - if (parameters.put(KEY_ARGS, parameter) != null) { - Msg.warn(this, "%s: Duplicate %s. Replaced".formatted(loc, AT_ARGS)); - } - } - - protected void parseTty(Location loc, String str) { - if (!extraTtys.add(str)) { - Msg.warn(this, "%s: Duplicate %s. Ignored".formatted(loc, AT_TTY)); - } - } - - protected void parseUnrecognized(Location loc, String line) { - Msg.warn(this, "%s: Unrecognized metadata: %s".formatted(loc, line)); - } - - protected void validate(String fileName) { - if (title == null) { - Msg.error(this, "%s is required. Using script file name.".formatted(AT_TITLE)); - title = fileName; - } - if (menuPath == null) { - menuPath = List.of(title); - } - if (menuGroup == null) { - menuGroup = ""; - } - if (menuOrder == null) { - menuOrder = ""; - } - if (iconId == null) { - iconId = "icon.debugger"; - } - } - - public String getDescription() { - return description == null ? null : description.toString(); - } - } - /** * Create a launch offer from the given shell script. * @@ -473,161 +44,36 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf */ public static UnixShellScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin, Program program, File script) throws FileNotFoundException { - try (BufferedReader reader = - new BufferedReader(new InputStreamReader(new FileInputStream(script)))) { - AttributesParser attrs = new AttributesParser(); - String line; - for (int lineNo = 1; (line = reader.readLine()) != null; lineNo++) { - if (line.startsWith(SHEBANG) && lineNo == 1) { - } - else if (line.isBlank()) { - continue; - } - else if (line.stripLeading().startsWith("#")) { - attrs.parseLine(new Location(script.getName(), lineNo), line); - } - else { - break; - } + ScriptAttributesParser parser = new ScriptAttributesParser() { + @Override + protected boolean ignoreLine(int lineNo, String line) { + return line.isBlank() || line.startsWith(SHEBANG) && lineNo == 1; } - attrs.validate(script.getName()); - return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, script, - "UNIX_SHELL:" + script.getName(), attrs.title, attrs.getDescription(), - attrs.menuPath, attrs.menuGroup, attrs.menuOrder, new GIcon(attrs.iconId), - attrs.helpLocation, attrs.parameters, attrs.extraTtys); - } - catch (FileNotFoundException e) { - // Avoid capture by IOException - throw e; - } - catch (IOException e) { - throw new AssertionError(e); - } - } - protected final File script; - protected final String configName; - protected final String title; - protected final String description; - protected final List menuPath; - protected final String menuGroup; - protected final String menuOrder; - protected final Icon icon; - protected final HelpLocation helpLocation; - protected final Map> parameters; - protected final List extraTtys; - - public UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program, - File script, String configName, String title, String description, List menuPath, - String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation, - Map> parameters, Collection extraTtys) { - super(plugin, program); - this.script = script; - this.configName = configName; - this.title = title; - this.description = description; - this.menuPath = List.copyOf(menuPath); - this.menuGroup = menuGroup; - this.menuOrder = menuOrder; - this.icon = icon; - this.helpLocation = helpLocation; - this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(parameters)); - this.extraTtys = List.copyOf(extraTtys); - } - - @Override - public String getConfigName() { - return configName; - } - - @Override - public String getTitle() { - return title; - } - - @Override - public String getDescription() { - return description; - } - - @Override - public List getMenuPath() { - return menuPath; - } - - @Override - public String getMenuGroup() { - return menuGroup; - } - - @Override - public String getMenuOrder() { - return menuOrder; - } - - @Override - public Icon getIcon() { - return icon; - } - - @Override - public HelpLocation getHelpLocation() { - return helpLocation; - } - - @Override - public Map> getParameters() { - return parameters; - } - - protected static String addrToString(InetAddress address) { - if (address.isAnyLocalAddress()) { - return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost. - } - return address.getHostAddress(); - } - - protected static String sockToString(SocketAddress address) { - if (address instanceof InetSocketAddress tcp) { - return addrToString(tcp.getAddress()) + ":" + tcp.getPort(); - } - throw new AssertionError("Unhandled address type " + address); - } - - @Override - protected void launchBackEnd(TaskMonitor monitor, Map sessions, - Map args, SocketAddress address) throws Exception { - List commandLine = new ArrayList<>(); - Map env = new HashMap<>(System.getenv()); - - commandLine.add(script.getAbsolutePath()); - env.put("GHIDRA_HOME", Application.getInstallationDirectory().getAbsolutePath()); - env.put("GHIDRA_TRACE_RMI_ADDR", sockToString(address)); - - ParameterDescription paramDesc; - for (int i = 1; (paramDesc = parameters.get("arg:" + i)) != null; i++) { - commandLine.add(Objects.toString(paramDesc.get(args))); - } - - paramDesc = parameters.get("args"); - if (paramDesc != null) { - commandLine.addAll(ShellUtils.parseArgs((String) paramDesc.get(args))); - } - - for (Entry> ent : parameters.entrySet()) { - String key = ent.getKey(); - if (key.startsWith(PREFIX_ENV)) { - String varName = key.substring(PREFIX_ENV.length()); - env.put(varName, Objects.toString(ent.getValue().get(args))); + @Override + protected String removeDelimiter(String line) { + String stripped = line.stripLeading(); + if (!stripped.startsWith("#")) { + return null; + } + return stripped.substring(1); } - } + }; + ScriptAttributes attrs = parser.parseFile(script); + return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, script, + "UNIX_SHELL:" + script.getName(), attrs); + } - for (String tty : extraTtys) { - NullPtyTerminalSession ns = nullPtyTerminal(); - env.put(tty, ns.name()); - sessions.put(ns.name(), ns); - } + private UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, + Program program, + File script, String configName, ScriptAttributes attrs) { + super(plugin, program, script, configName, attrs); + } - sessions.put("Shell", runInTerminal(commandLine, env, sessions.values())); + @Override + protected void prepareSubprocess(List commandLine, Map env, + Map args, SocketAddress address) { + ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args, + address); } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOpinion.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOpinion.java index d1ac0dc9f5..aebfa87aa2 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOpinion.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/UnixShellScriptTraceRmiLaunchOpinion.java @@ -20,63 +20,30 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import generic.jar.ResourceFile; -import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; -import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion; -import ghidra.framework.Application; -import ghidra.framework.options.OptionType; -import ghidra.framework.options.Options; -import ghidra.framework.plugintool.PluginTool; -import ghidra.framework.plugintool.util.PluginUtils; import ghidra.program.model.listing.Program; -import ghidra.util.HelpLocation; import ghidra.util.Msg; -public class UnixShellScriptTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion { - - @Override - public void registerOptions(Options options) { - String pluginName = PluginUtils.getPluginNameFromClass(TraceRmiLauncherServicePlugin.class); - options.registerOption(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS, - OptionType.STRING_TYPE, "", new HelpLocation(pluginName, "options"), - "Paths to search for user-created debugger launchers", new ScriptPathsPropertyEditor()); - } - - @Override - public boolean requiresRefresh(String optionName) { - return TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS.equals(optionName); - } - - protected Stream getModuleScriptPaths() { - return Application.findModuleSubDirectories("data/debugger-launchers").stream(); - } - - protected Stream getUserScriptPaths(PluginTool tool) { - Options options = tool.getOptions(DebuggerPluginPackage.NAME); - String scriptPaths = - options.getString(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS, ""); - return scriptPaths.lines().filter(d -> !d.isBlank()).map(ResourceFile::new); - } - - protected Stream getScriptPaths(PluginTool tool) { - return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool)); - } +public class UnixShellScriptTraceRmiLaunchOpinion extends AbstractTraceRmiLaunchOpinion { @Override public Collection getOffers(TraceRmiLauncherServicePlugin plugin, Program program) { return getScriptPaths(plugin.getTool()) .flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".sh")))) - .flatMap(sf -> { - try { - return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(plugin, program, - sf.getFile(false))); - } - catch (Exception e) { - Msg.error(this, "Could not offer " + sf + ":" + e.getMessage(), e); - return Stream.of(); - } - }) + .flatMap(sf -> createOffer(plugin, program, sf)) .collect(Collectors.toList()); } + + protected Stream createOffer(TraceRmiLauncherServicePlugin plugin, + Program program, ResourceFile scriptFile) { + try { + return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(plugin, program, + scriptFile.getFile(false))); + } + catch (Exception e) { + Msg.error(this, "Could not offer " + scriptFile + ": " + e.getMessage(), e); + return Stream.of(); + } + } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java index ba7cfec453..821e8d96db 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java @@ -20,8 +20,6 @@ import java.math.BigInteger; import java.net.Socket; import java.net.SocketAddress; import java.nio.ByteBuffer; -import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.*; @@ -329,17 +327,17 @@ public class TraceRmiHandler implements TraceRmiConnection { } } - protected DomainFolder createFolders(DomainFolder parent, Path path) + protected DomainFolder createFolders(DomainFolder parent, List path) throws InvalidNameException, IOException { return createFolders(parent, path, 0); } - protected DomainFolder createFolders(DomainFolder parent, Path path, int index) + protected DomainFolder createFolders(DomainFolder parent, List path, int index) throws InvalidNameException, IOException { - if (path == null && index == 0 || index == path.getNameCount()) { + if (path == null && index == 0 || index == path.size()) { return parent; } - String name = path.getName(index).toString(); + String name = path.get(index); return createFolders(getOrCreateFolder(parent, name), path, index + 1); } @@ -859,10 +857,10 @@ public class TraceRmiHandler implements TraceRmiConnection { protected ReplyCreateTrace handleCreateTrace(RequestCreateTrace req) throws InvalidNameException, IOException, CancelledException { DomainFolder traces = getOrCreateNewTracesFolder(); - Path path = Paths.get(req.getPath().getPath()); - DomainFolder folder = createFolders(traces, path.getParent()); + List path = sanitizePath(req.getPath().getPath()); + DomainFolder folder = createFolders(traces, path.subList(0, path.size() - 1)); CompilerSpec cs = requireCompilerSpec(req.getLanguage(), req.getCompiler()); - DBTrace trace = new DBTrace(path.getFileName().toString(), cs, this); + DBTrace trace = new DBTrace(path.get(path.size() - 1), cs, this); TraceRmiTarget target = new TraceRmiTarget(plugin.getTool(), this, trace); DoId doId = requireAvailableDoId(req.getOid()); openTraces.put(new OpenTrace(doId, trace, target)); @@ -870,6 +868,10 @@ public class TraceRmiHandler implements TraceRmiConnection { return ReplyCreateTrace.getDefaultInstance(); } + protected static List sanitizePath(String path) { + return Stream.of(path.split("\\\\|/")).filter(p -> !p.isBlank()).toList(); + } + protected ReplyDeleteBytes handleDeleteBytes(RequestDeleteBytes req) throws AddressOverflowException { OpenTrace open = requireOpenTrace(req.getOid()); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java index b29fa6cfec..aad2a60ee3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProvider.java @@ -251,7 +251,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { if (context instanceof DebuggerObjectActionContext ctx) { return DebuggerModulesPanel.getSelectedModulesFromContext(ctx); } - return null; + return Set.of(); } protected static Set getSelectedSections(ActionContext context) { @@ -264,7 +264,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { if (context instanceof DebuggerObjectActionContext ctx) { return DebuggerModulesPanel.getSelectedSectionsFromContext(ctx); } - return null; + return Set.of(); } protected static AddressSetView getSelectedAddresses(ActionContext context) { @@ -299,7 +299,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { } AddressSetView sel = getSelectedAddresses(context); - if (sel == null) { + if (sel == null || sel.isEmpty()) { return; } @@ -540,9 +540,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { .onAction(this::activatedMapModules) .buildAndInstallLocal(this); actionMapModuleTo = MapModuleToAction.builder(plugin) - .withContext(DebuggerModuleActionContext.class) - .enabledWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1) - .popupWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1) + .enabledWhen(ctx -> currentProgram != null && getSelectedModules(ctx).size() == 1) + .popupWhen(ctx -> currentProgram != null && getSelectedModules(ctx).size() == 1) .onAction(this::activatedMapModuleTo) .buildAndInstallLocal(this); actionMapSections = MapSectionsAction.builder(plugin) @@ -551,9 +550,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter { .onAction(this::activatedMapSections) .buildAndInstallLocal(this); actionMapSectionTo = MapSectionToAction.builder(plugin) - .withContext(DebuggerSectionActionContext.class) - .enabledWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1) - .popupWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1) + .enabledWhen(ctx -> currentProgram != null && getSelectedSections(ctx).size() == 1) + .popupWhen(ctx -> currentProgram != null && getSelectedSections(ctx).size() == 1) .onAction(this::activatedMapSectionTo) .buildAndInstallLocal(this); actionMapSectionsTo = MapSectionsToAction.builder(plugin) diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java index a8c45398cf..3a3465f593 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/DefaultModuleMapProposal.java @@ -25,6 +25,7 @@ import ghidra.program.model.listing.Program; import ghidra.program.model.mem.MemoryBlock; import ghidra.trace.model.Lifespan; import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.memory.TraceObjectMemoryRegion; import ghidra.trace.model.modules.TraceModule; public class DefaultModuleMapProposal @@ -207,10 +208,14 @@ public class DefaultModuleMapProposal catch (AddressOverflowException e) { return; // Just score it as having no matches? } + Lifespan lifespan = module.getLifespan(); for (TraceMemoryRegion region : module.getTrace() .getMemoryManager() - .getRegionsIntersecting(module.getLifespan(), moduleRange)) { - getMatcher(region.getMinAddress().subtract(moduleBase)).region = region; + .getRegionsIntersecting(lifespan, moduleRange)) { + Address min = region instanceof TraceObjectMemoryRegion objReg + ? objReg.getMinAddress(lifespan.lmin()) + : region.getMinAddress(); + getMatcher(min.subtract(moduleBase)).region = region; } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ShellUtils.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ShellUtils.java index c4c9df8d61..5164c557d8 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ShellUtils.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ShellUtils.java @@ -15,8 +15,8 @@ */ package ghidra.dbg.util; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; public class ShellUtils { enum State { @@ -139,4 +139,11 @@ public class ShellUtils { } return line.toString(); } + + public static String generateEnvBlock(Map env) { + return env.entrySet() + .stream() + .map(e -> e.getKey() + "=" + e.getValue() + "\0") + .collect(Collectors.joining()); // NB. JNA adds final terminator + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java index 04f8c3e5ad..b84f1e1e9a 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/memory/DBTraceObjectMemoryRegion.java @@ -195,14 +195,22 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra } } + @Override + public AddressRange getRange(long snap) { + try (LockHold hold = object.getTrace().lockRead()) { + // TODO: Caching without regard to snap seems bad + return range = TraceObjectInterfaceUtils.getValue(object, snap, + TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, AddressRange.class, range); + } + } + @Override public AddressRange getRange() { try (LockHold hold = object.getTrace().lockRead()) { if (object.getLife().isEmpty()) { return range; } - return range = TraceObjectInterfaceUtils.getValue(object, getCreationSnap(), - TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, AddressRange.class, range); + return getRange(getCreationSnap()); } } @@ -213,6 +221,12 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra } } + @Override + public Address getMinAddress(long snap) { + AddressRange range = getRange(snap); + return range == null ? null : range.getMinAddress(); + } + @Override public Address getMinAddress() { AddressRange range = getRange(); @@ -226,6 +240,12 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra } } + @Override + public Address getMaxAddress(long snap) { + AddressRange range = getRange(snap); + return range == null ? null : range.getMaxAddress(); + } + @Override public Address getMaxAddress() { AddressRange range = getRange(); diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceObjectMemoryRegion.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceObjectMemoryRegion.java index 5ebc7a7d88..effd53dbe7 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceObjectMemoryRegion.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/memory/TraceObjectMemoryRegion.java @@ -20,6 +20,7 @@ import java.util.Set; import ghidra.dbg.target.TargetMemoryRegion; import ghidra.dbg.target.TargetObject; +import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressRange; import ghidra.trace.model.Lifespan; import ghidra.trace.model.target.TraceObjectInterface; @@ -39,6 +40,12 @@ public interface TraceObjectMemoryRegion extends TraceMemoryRegion, TraceObjectI void setRange(Lifespan lifespan, AddressRange range); + AddressRange getRange(long snap); + + Address getMinAddress(long snap); + + Address getMaxAddress(long snap); + void setFlags(Lifespan lifespan, Collection flags); void addFlags(Lifespan lifespan, Collection flags); diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java index 32f6a3aec8..43314a2c6f 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java @@ -15,6 +15,7 @@ */ package ghidra.pty; +import java.io.File; import java.io.IOException; import java.util.*; @@ -51,19 +52,28 @@ public interface PtyChild extends PtyEndpoint { * * @param args the image path and arguments * @param env the environment + * @param workingDirectory the working directory * @param mode the terminal mode. If a mode is not implemented, it may be silently ignored. * @return a handle to the subprocess * @throws IOException if the session could not be started */ - PtySession session(String[] args, Map env, Collection mode) - throws IOException; + PtySession session(String[] args, Map env, File workingDirectory, + Collection mode) throws IOException; /** - * @see #session(String[], Map, Collection) + * @see #session(String[], Map, File, Collection) + */ + default PtySession session(String[] args, Map env, File workingDirectory, + TermMode... mode) throws IOException { + return session(args, env, workingDirectory, List.of(mode)); + } + + /** + * @see #session(String[], Map, File, Collection) */ default PtySession session(String[] args, Map env, TermMode... mode) throws IOException { - return session(args, env, List.of(mode)); + return session(args, env, null, List.of(mode)); } /** diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java index 8f0089ec77..8a89555d15 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java @@ -57,13 +57,13 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild { * program is active before sending special characters. */ @Override - public PtySession session(String[] args, Map env, Collection mode) - throws IOException { - return sessionUsingJavaLeader(args, env, mode); + public PtySession session(String[] args, Map env, File workingDirectory, + Collection mode) throws IOException { + return sessionUsingJavaLeader(args, env, workingDirectory, mode); } protected PtySession sessionUsingJavaLeader(String[] args, Map env, - Collection mode) throws IOException { + File workingDirectory, Collection mode) throws IOException { final List argsList = new ArrayList<>(); String javaCommand = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; @@ -78,6 +78,9 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild { if (env != null) { builder.environment().putAll(env); } + if (workingDirectory != null) { + builder.directory(workingDirectory); + } builder.inheritIO(); applyMode(mode); diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java index b3421fa2c5..9d792a097b 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java @@ -48,8 +48,11 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild { } @Override - public SshPtySession session(String[] args, Map env, Collection mode) - throws IOException { + public SshPtySession session(String[] args, Map env, File workingDirectory, + Collection mode) throws IOException { + if (workingDirectory != null) { + throw new UnsupportedOperationException(); + } /** * TODO: This syntax assumes a UNIX-style shell, and even among them, this may not be * universal. This certainly works for my version of bash :) diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java index 7f84402b86..76ff4a3f39 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java @@ -15,6 +15,7 @@ */ package ghidra.pty.windows; +import java.io.File; import java.io.IOException; import java.util.*; @@ -75,7 +76,7 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild { @Override public LocalWindowsNativeProcessPtySession session(String[] args, Map env, - Collection mode) throws IOException { + File workingDirectory, Collection mode) throws IOException { /** * TODO: How to incorporate environment into CreateProcess? * @@ -91,9 +92,11 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild { null /*lpProcessAttributes*/, null /*lpThreadAttributes*/, false /*bInheritHandles*/, - ConPty.EXTENDED_STARTUPINFO_PRESENT /*dwCreationFlags*/, - null /*lpEnvironment*/, - null /*lpCurrentDirectory*/, + new DWORD(Kernel32.EXTENDED_STARTUPINFO_PRESENT | + Kernel32.CREATE_UNICODE_ENVIRONMENT) /*dwCreationFlags*/, + env == null ? null : new WString(ShellUtils.generateEnvBlock(env)), + workingDirectory == null ? null + : new WString(workingDirectory.getAbsolutePath()) /*lpCurrentDirectory*/, si /*lpStartupInfo*/, pi /*lpProcessInformation*/).booleanValue()) { throw new LastErrorException(Kernel32.INSTANCE.GetLastError()); diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java index 3df225168c..312e6b9ccf 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/HandleInputStream.java @@ -83,7 +83,9 @@ public class HandleInputStream extends InputStream { public synchronized int read(byte[] b, int off, int len) throws IOException { byte[] temp = new byte[len]; int read = read(temp); - System.arraycopy(temp, 0, b, off, read); + if (read > 0) { + System.arraycopy(temp, 0, b, off, read); + } return read; } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java index baf59de4be..72efc340b4 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/jna/ConsoleApiNative.java @@ -57,7 +57,7 @@ public interface ConsoleApiNative extends StdCallLibrary { WinBase.SECURITY_ATTRIBUTES lpThreadAttributes, boolean bInheritHandles, DWORD dwCreationFlags, - Pointer lpEnvironment, + WString lpEnvironment, WString lpCurrentDirectory, STARTUPINFOEX lpStartupInfo, WinBase.PROCESS_INFORMATION lpProcessInformation); diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java index ced7165f55..8d90b7050a 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/TraceRmiLauncherServicePluginTest.java @@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.tracermi.launcher; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assume.assumeTrue; import java.util.*; @@ -28,6 +29,7 @@ import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; import ghidra.app.services.TraceRmiLauncherService; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*; +import ghidra.framework.OperatingSystem; import ghidra.util.task.ConsoleTaskMonitor; public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebuggerTest { @@ -50,7 +52,7 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug assertFalse(launcherService.getOffers(program).isEmpty()); } - protected LaunchConfigurator fileOnly(String file) { + protected LaunchConfigurator gdbFileOnly(String file) { return new LaunchConfigurator() { @Override public Map configureLauncher(TraceRmiLaunchOffer offer, @@ -65,16 +67,18 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug // @Test // This is currently hanging the test machine. The gdb process is left running public void testLaunchLocalGdb() throws Exception { + assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.LINUX); + createProgram(getSLEIGH_X86_64_LANGUAGE()); try (Transaction tx = program.openTransaction("Rename")) { program.setName("bash"); } programManager.openProgram(program); - TraceRmiLaunchOffer gdbOffer = findByTitle(launcherService.getOffers(program), "gdb"); + TraceRmiLaunchOffer offer = findByTitle(launcherService.getOffers(program), "gdb"); try (LaunchResult result = - gdbOffer.launchProgram(new ConsoleTaskMonitor(), fileOnly("/usr/bin/bash"))) { + offer.launchProgram(new ConsoleTaskMonitor(), gdbFileOnly("/usr/bin/bash"))) { if (result.exception() != null) { throw new AssertionError(result.exception()); } @@ -83,4 +87,39 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug assertEquals(getSLEIGH_X86_64_LANGUAGE(), result.trace().getBaseLanguage()); } } + + protected LaunchConfigurator dbgengFileOnly(String file) { + return new LaunchConfigurator() { + @Override + public Map configureLauncher(TraceRmiLaunchOffer offer, + Map arguments, RelPrompt relPrompt) { + Map args = new HashMap<>(arguments); + args.put("env:OPT_TARGET_IMG", file); + return args; + } + }; + } + + @Test + public void testLaunchLocalDbgeng() throws Exception { + assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS); + + createProgram(getSLEIGH_X86_64_LANGUAGE()); + try (Transaction tx = program.openTransaction("Rename")) { + program.setName("notepad.exe"); + } + programManager.openProgram(program); + + TraceRmiLaunchOffer offer = findByTitle(launcherService.getOffers(program), "dbgeng"); + + try (LaunchResult result = + offer.launchProgram(new ConsoleTaskMonitor(), dbgengFileOnly("notepad.exe"))) { + if (result.exception() != null) { + throw new AssertionError(result.exception()); + } + + assertEquals("notepad.exe", result.trace().getName()); + assertEquals(getSLEIGH_X86_64_LANGUAGE(), result.trace().getBaseLanguage()); + } + } }