GP-3823: TraceRmi Launcher framework + dbgeng for Windows.

This commit is contained in:
Dan 2023-11-28 10:38:27 -05:00
parent 80d92aa32f
commit c126cf51c0
33 changed files with 1206 additions and 1303 deletions

View file

@ -1,8 +1,7 @@
##VERSION: 2.0 ##VERSION: 2.0
##MODULE IP: Apache License 2.0 ##MODULE IP: Apache License 2.0
Module.manifest||GHIDRA||||END| Module.manifest||GHIDRA||||END|
src/javaprovider/def/javaprovider.def||GHIDRA||||END| data/debugger-launchers/local-dbgeng.bat||GHIDRA||||END|
src/javaprovider/rc/javaprovider.rc||GHIDRA||||END|
src/main/py/LICENSE||GHIDRA||||END| src/main/py/LICENSE||GHIDRA||||END|
src/main/py/README.md||GHIDRA||||END| src/main/py/README.md||GHIDRA||||END|
src/main/py/pyproject.toml||GHIDRA||||END| src/main/py/pyproject.toml||GHIDRA||||END|

View file

@ -0,0 +1,31 @@
::@title dbgeng
::@desc <html><body width="300px">
::@desc <h3>Launch with <tt>dbgeng</tt> (in a Python interpreter)</h3>
::@desc <p>This will launch the target on the local machine using <tt>dbgeng.dll</tt>. Typically,
::@desc Windows systems have this library pre-installed, but it may have limitations, e.g., you
::@desc cannot use <tt>.server</tt>. For the full capabilities, you must install WinDbg.</p>
::@desc <p>Furthermore, you must have Python 3 installed on your system, and it must have the
::@desc <tt>pybag</tt> and <tt>protobuf</tt> packages installed.</p>
::@desc </body></html>
::@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

View file

@ -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()

View file

@ -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 <engextcpp.hpp>
#include <jni.h>
#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);
}

View file

@ -1,13 +0,0 @@
EXPORTS
; For ExtCpp
DebugExtensionInitialize
DebugExtensionUninitialize
DebugExtensionNotify
help
; My Commands
java_add_cp
java_set
java_get
java_run

View file

@ -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 <Windows.h>

View file

@ -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

View file

@ -19,6 +19,7 @@ import os.path
import socket import socket
import time import time
import sys import sys
import re
from ghidratrace import sch from ghidratrace import sch
from ghidratrace.client import Client, Address, AddressRange, TraceObject from ghidratrace.client import Client, Address, AddressRange, TraceObject
@ -185,7 +186,7 @@ def compute_name(progname=None):
progname = buffer.decode('utf-8') progname = buffer.decode('utf-8')
except Exception: except Exception:
return 'pydbg/noname' return 'pydbg/noname'
return 'pydbg/' + progname.split('/')[-1] return 'pydbg/' + re.split(r'/|\\', progname)[-1]
def start_trace(name): def start_trace(name):
@ -1301,7 +1302,36 @@ def ghidra_util_wait_stopped(timeout=1):
time.sleep(0.1) time.sleep(0.1)
if time.time() - start > timeout: if time.time() - start > timeout:
raise RuntimeError('Timed out waiting for thread to stop') raise RuntimeError('Timed out waiting for thread to stop')
def dbg(): def dbg():
return util.get_debugger() 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

View file

@ -383,7 +383,7 @@ def interrupt():
@REGISTRY.method(action='step_into') @REGISTRY.method(action='step_into')
def step_into(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1): 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) find_thread_by_obj(thread)
dbg().stepi(n) dbg().stepi(n)
@ -511,7 +511,7 @@ def write_mem(process: sch.Schema('Process'), address: Address, data: bytes):
@REGISTRY.method @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.""" """Write a register."""
util.select_frame() util.select_frame()
nproc = pydbg.selected_process() nproc = pydbg.selected_process()

View file

@ -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 <stdlib.h>
#include <string.h>
#define INITGUID
#include <dbgeng.h>
#include <Windows.h>
#include <jni.h>
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);
}

View file

@ -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<String> 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<String, ParameterDescription<?>> getParameters() {
return attrs.parameters();
}
protected abstract void prepareSubprocess(List<String> commandLine, Map<String, String> env,
Map<String, ?> args, SocketAddress address);
@Override
protected void launchBackEnd(TaskMonitor monitor, Map<String, TerminalSession> sessions,
Map<String, ?> args, SocketAddress address) throws Exception {
List<String> commandLine = new ArrayList<>();
Map<String, String> 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()));
}
}

View file

@ -420,8 +420,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
} }
protected PtyTerminalSession runInTerminal(List<String> commandLine, Map<String, String> env, protected PtyTerminalSession runInTerminal(List<String> commandLine, Map<String, String> env,
Collection<TerminalSession> subordinates) File workingDirectory, Collection<TerminalSession> subordinates) throws IOException {
throws IOException {
PtyFactory factory = getPtyFactory(); PtyFactory factory = getPtyFactory();
Pty pty = factory.openpty(); Pty pty = factory.openpty();
@ -432,13 +431,19 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
TerminalListener resizeListener = new TerminalListener() { TerminalListener resizeListener = new TerminalListener() {
@Override @Override
public void resized(short cols, short rows) { 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); terminal.addTerminalListener(resizeListener);
env.put("TERM", "xterm-256color"); 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(() -> { Thread waiter = new Thread(() -> {
try { try {

View file

@ -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<ResourceFile> getModuleScriptPaths() {
return Application.findModuleSubDirectories("data/debugger-launchers").stream();
}
protected Stream<ResourceFile> 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<ResourceFile> getScriptPaths(PluginTool tool) {
return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool));
}
}

View file

@ -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.
*
* <p>
* 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<String> commandLine, Map<String, String> env,
Map<String, ?> args, SocketAddress address) {
ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args,
address);
}
}

View file

@ -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<TraceRmiLaunchOffer> 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<TraceRmiLaunchOffer> 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();
}
}
}

View file

@ -88,6 +88,9 @@ public class LaunchAction extends MultiActionDockingAction {
ConfigLast findMostRecentConfig() { ConfigLast findMostRecentConfig() {
Program program = plugin.currentProgram; Program program = plugin.currentProgram;
if (program == null) {
return null;
}
ConfigLast best = null; ConfigLast best = null;
ProgramUserData userData = program.getProgramUserData(); ProgramUserData userData = program.getProgramUserData();
@ -113,14 +116,16 @@ public class LaunchAction extends MultiActionDockingAction {
List<DockingActionIf> actions = new ArrayList<>(); List<DockingActionIf> actions = new ArrayList<>();
ProgramUserData userData = program.getProgramUserData();
Map<String, Long> saved = new HashMap<>(); Map<String, Long> saved = new HashMap<>();
for (String propName : userData.getStringPropertyNames()) { if (program != null) {
ConfigLast check = checkSavedConfig(userData, propName); ProgramUserData userData = program.getProgramUserData();
if (check == null) { for (String propName : userData.getStringPropertyNames()) {
continue; 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) { for (TraceRmiLaunchOffer offer : offers) {
@ -134,6 +139,8 @@ public class LaunchAction extends MultiActionDockingAction {
.build()); .build());
Long last = saved.get(offer.getConfigName()); Long last = saved.get(offer.getConfigName());
if (last == null) { if (last == null) {
// NB. If program == null, this will always happen.
// Thus, no worries about program.getName() below.
continue; continue;
} }
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName()) actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
@ -172,6 +179,11 @@ public class LaunchAction extends MultiActionDockingAction {
// Make accessible to this file // Make accessible to this file
return super.showPopup(); return super.showPopup();
} }
@Override
public String getToolTipText() {
return getDescription();
}
} }
@Override @Override
@ -180,19 +192,45 @@ public class LaunchAction extends MultiActionDockingAction {
} }
@Override @Override
public void actionPerformed(ActionContext context) { public boolean isEnabledForContext(ActionContext context) {
// See comment on super method about use of runLater return plugin.currentProgram != null;
ConfigLast last = findMostRecentConfig(); }
protected TraceRmiLaunchOffer findOffer(ConfigLast last) {
if (last == null) { if (last == null) {
Swing.runLater(() -> button.showPopup()); return null;
return;
} }
for (TraceRmiLaunchOffer offer : plugin.getOffers(plugin.currentProgram)) { for (TraceRmiLaunchOffer offer : plugin.getOffers(plugin.currentProgram)) {
if (offer.getConfigName().equals(last.configName)) { if (offer.getConfigName().equals(last.configName)) {
plugin.relaunch(offer); return offer;
return;
} }
} }
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();
} }
} }

View file

@ -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:
* <ul>
* <li>{@code @menu-path}: <b>(Required)</b></li>
* </ul>
*
*/
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<T> {
static OptType<?> parse(Location loc, String typeName,
Map<String, UserType<?>> 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<T> withCastDefault(Object defaultValue) {
return new TypeAndDefault<>(this, cls().cast(defaultValue));
}
Class<T> cls();
T decode(Location loc, String str);
ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description);
}
protected interface BaseType<T> extends OptType<T> {
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> STRING = new BaseType<>() {
@Override
public Class<String> cls() {
return String.class;
}
@Override
public String decode(Location loc, String str) {
return str;
}
};
public static final BaseType<BigInteger> INT = new BaseType<>() {
@Override
public Class<BigInteger> 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<Boolean> BOOL = new BaseType<>() {
@Override
public Class<Boolean> 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<T> withCastChoices(List<?> choices) {
return new UserType<>(this, choices.stream().map(cls()::cast).toList());
}
@Override
default ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.create(cls(), name, false, defaultValue, display,
description);
}
}
protected record UserType<T>(BaseType<T> base, List<T> choices) implements OptType<T> {
@Override
public Class<T> cls() {
return base.cls();
}
@Override
public T decode(Location loc, String str) {
return base.decode(loc, str);
}
@Override
public ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.choices(cls(), name, choices, defaultValue, display,
description);
}
}
protected record TypeAndDefault<T>(OptType<T> type, T defaultValue) {
public static TypeAndDefault<?> parse(Location loc, String typeName, String defaultString,
Map<String, UserType<?>> 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<T> 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<String> menuPath,
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
Map<String, ParameterDescription<?>> parameters, Collection<String> 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<String> commandLine, Map<String, String> env,
File script, Map<String, ParameterDescription<?>> parameters, Map<String, ?> 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<String, ParameterDescription<?>> 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<String> menuPath;
private String menuGroup;
private String menuOrder;
private String iconId;
private HelpLocation helpLocation;
private final Map<String, UserType<?>> userTypes = new HashMap<>();
private final Map<String, ParameterDescription<?>> parameters = new LinkedHashMap<>();
private final Set<String> 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
*
* <p>
* 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<String> 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<String> 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<String> 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<String> parts = ShellUtils.parseArgs(str);
if (parts.size() != 2) {
Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS));
return;
}
ParameterDescription<String> 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();
}
}

View file

@ -15,454 +15,25 @@
*/ */
package ghidra.app.plugin.core.debug.gui.tracermi.launcher; package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import java.io.*; import java.io.File;
import java.math.BigInteger; import java.io.FileNotFoundException;
import java.net.*; import java.net.SocketAddress;
import java.util.*; import java.util.List;
import java.util.Map.Entry; import java.util.Map;
import javax.swing.Icon; import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes;
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.program.model.listing.Program; 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. * A launcher implemented by a simple UNIX shell script.
* *
* <p> * <p>
* The script must start with an attributes header in a comment block. Some attributes are required. * The script must start with an attributes header in a comment block. See
* Others are optional: * {@link ScriptAttributesParser}.
* <ul>
* <li>{@code @menu-path}: <b>(Required)</b></li>
* </ul>
*/ */
public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer { public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer {
public static final String SHEBANG = "#!"; 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<T> {
static OptType<?> parse(Location loc, String typeName,
Map<String, UserType<?>> 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<T> withCastDefault(Object defaultValue) {
return new TypeAndDefault<>(this, cls().cast(defaultValue));
}
Class<T> cls();
T decode(Location loc, String str);
ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description);
}
protected interface BaseType<T> extends OptType<T> {
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> STRING = new BaseType<>() {
@Override
public Class<String> cls() {
return String.class;
}
@Override
public String decode(Location loc, String str) {
return str;
}
};
public static final BaseType<BigInteger> INT = new BaseType<>() {
@Override
public Class<BigInteger> 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<Boolean> BOOL = new BaseType<>() {
@Override
public Class<Boolean> 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<T> withCastChoices(List<?> choices) {
return new UserType<>(this, choices.stream().map(cls()::cast).toList());
}
@Override
default ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.create(cls(), name, false, defaultValue, display,
description);
}
}
protected record UserType<T> (BaseType<T> base, List<T> choices) implements OptType<T> {
@Override
public Class<T> cls() {
return base.cls();
}
@Override
public T decode(Location loc, String str) {
return base.decode(loc, str);
}
@Override
public ParameterDescription<T> createParameter(String name, T defaultValue, String display,
String description) {
return ParameterDescription.choices(cls(), name, choices, defaultValue, display,
description);
}
}
protected record TypeAndDefault<T> (OptType<T> type, T defaultValue) {
public static TypeAndDefault<?> parse(Location loc, String typeName, String defaultString,
Map<String, UserType<?>> 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<T> 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<String> menuPath;
protected String menuGroup;
protected String menuOrder;
protected String iconId;
protected HelpLocation helpLocation;
protected final Map<String, UserType<?>> userTypes = new HashMap<>();
protected final Map<String, ParameterDescription<?>> parameters = new LinkedHashMap<>();
protected final Set<String> 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<String> 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<String> 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<String> 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<String> parts = ShellUtils.parseArgs(str);
if (parts.size() != 2) {
Msg.error(this, MSGPAT_INVALID_ARGS_SYNTAX.formatted(loc, AT_ARGS));
return;
}
ParameterDescription<String> 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. * Create a launch offer from the given shell script.
* *
@ -473,161 +44,36 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
*/ */
public static UnixShellScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin, public static UnixShellScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin,
Program program, File script) throws FileNotFoundException { Program program, File script) throws FileNotFoundException {
try (BufferedReader reader = ScriptAttributesParser parser = new ScriptAttributesParser() {
new BufferedReader(new InputStreamReader(new FileInputStream(script)))) { @Override
AttributesParser attrs = new AttributesParser(); protected boolean ignoreLine(int lineNo, String line) {
String line; return line.isBlank() || line.startsWith(SHEBANG) && lineNo == 1;
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;
}
} }
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; @Override
protected final String configName; protected String removeDelimiter(String line) {
protected final String title; String stripped = line.stripLeading();
protected final String description; if (!stripped.startsWith("#")) {
protected final List<String> menuPath; return null;
protected final String menuGroup; }
protected final String menuOrder; return stripped.substring(1);
protected final Icon icon;
protected final HelpLocation helpLocation;
protected final Map<String, ParameterDescription<?>> parameters;
protected final List<String> extraTtys;
public UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin, Program program,
File script, String configName, String title, String description, List<String> menuPath,
String menuGroup, String menuOrder, Icon icon, HelpLocation helpLocation,
Map<String, ParameterDescription<?>> parameters, Collection<String> 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<String> 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<String, ParameterDescription<?>> 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<String, TerminalSession> sessions,
Map<String, ?> args, SocketAddress address) throws Exception {
List<String> commandLine = new ArrayList<>();
Map<String, String> 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<String, ParameterDescription<?>> 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)));
} }
} };
ScriptAttributes attrs = parser.parseFile(script);
return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, script,
"UNIX_SHELL:" + script.getName(), attrs);
}
for (String tty : extraTtys) { private UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin,
NullPtyTerminalSession ns = nullPtyTerminal(); Program program,
env.put(tty, ns.name()); File script, String configName, ScriptAttributes attrs) {
sessions.put(ns.name(), ns); super(plugin, program, script, configName, attrs);
} }
sessions.put("Shell", runInTerminal(commandLine, env, sessions.values())); @Override
protected void prepareSubprocess(List<String> commandLine, Map<String, String> env,
Map<String, ?> args, SocketAddress address) {
ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args,
address);
} }
} }

View file

@ -20,63 +20,30 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; 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.program.model.listing.Program;
import ghidra.util.HelpLocation;
import ghidra.util.Msg; import ghidra.util.Msg;
public class UnixShellScriptTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion { public class UnixShellScriptTraceRmiLaunchOpinion extends AbstractTraceRmiLaunchOpinion {
@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<ResourceFile> getModuleScriptPaths() {
return Application.findModuleSubDirectories("data/debugger-launchers").stream();
}
protected Stream<ResourceFile> 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<ResourceFile> getScriptPaths(PluginTool tool) {
return Stream.concat(getModuleScriptPaths(), getUserScriptPaths(tool));
}
@Override @Override
public Collection<TraceRmiLaunchOffer> getOffers(TraceRmiLauncherServicePlugin plugin, public Collection<TraceRmiLaunchOffer> getOffers(TraceRmiLauncherServicePlugin plugin,
Program program) { Program program) {
return getScriptPaths(plugin.getTool()) return getScriptPaths(plugin.getTool())
.flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".sh")))) .flatMap(rf -> Stream.of(rf.listFiles(crf -> crf.getName().endsWith(".sh"))))
.flatMap(sf -> { .flatMap(sf -> createOffer(plugin, program, 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();
}
})
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
protected Stream<TraceRmiLaunchOffer> 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();
}
}
} }

View file

@ -20,8 +20,6 @@ import java.math.BigInteger;
import java.net.Socket; import java.net.Socket;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant; import java.time.Instant;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; 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<String> path)
throws InvalidNameException, IOException { throws InvalidNameException, IOException {
return createFolders(parent, path, 0); return createFolders(parent, path, 0);
} }
protected DomainFolder createFolders(DomainFolder parent, Path path, int index) protected DomainFolder createFolders(DomainFolder parent, List<String> path, int index)
throws InvalidNameException, IOException { throws InvalidNameException, IOException {
if (path == null && index == 0 || index == path.getNameCount()) { if (path == null && index == 0 || index == path.size()) {
return parent; return parent;
} }
String name = path.getName(index).toString(); String name = path.get(index);
return createFolders(getOrCreateFolder(parent, name), path, index + 1); return createFolders(getOrCreateFolder(parent, name), path, index + 1);
} }
@ -859,10 +857,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
protected ReplyCreateTrace handleCreateTrace(RequestCreateTrace req) protected ReplyCreateTrace handleCreateTrace(RequestCreateTrace req)
throws InvalidNameException, IOException, CancelledException { throws InvalidNameException, IOException, CancelledException {
DomainFolder traces = getOrCreateNewTracesFolder(); DomainFolder traces = getOrCreateNewTracesFolder();
Path path = Paths.get(req.getPath().getPath()); List<String> path = sanitizePath(req.getPath().getPath());
DomainFolder folder = createFolders(traces, path.getParent()); DomainFolder folder = createFolders(traces, path.subList(0, path.size() - 1));
CompilerSpec cs = requireCompilerSpec(req.getLanguage(), req.getCompiler()); 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); TraceRmiTarget target = new TraceRmiTarget(plugin.getTool(), this, trace);
DoId doId = requireAvailableDoId(req.getOid()); DoId doId = requireAvailableDoId(req.getOid());
openTraces.put(new OpenTrace(doId, trace, target)); openTraces.put(new OpenTrace(doId, trace, target));
@ -870,6 +868,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
return ReplyCreateTrace.getDefaultInstance(); return ReplyCreateTrace.getDefaultInstance();
} }
protected static List<String> sanitizePath(String path) {
return Stream.of(path.split("\\\\|/")).filter(p -> !p.isBlank()).toList();
}
protected ReplyDeleteBytes handleDeleteBytes(RequestDeleteBytes req) protected ReplyDeleteBytes handleDeleteBytes(RequestDeleteBytes req)
throws AddressOverflowException { throws AddressOverflowException {
OpenTrace open = requireOpenTrace(req.getOid()); OpenTrace open = requireOpenTrace(req.getOid());

View file

@ -251,7 +251,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
if (context instanceof DebuggerObjectActionContext ctx) { if (context instanceof DebuggerObjectActionContext ctx) {
return DebuggerModulesPanel.getSelectedModulesFromContext(ctx); return DebuggerModulesPanel.getSelectedModulesFromContext(ctx);
} }
return null; return Set.of();
} }
protected static Set<TraceSection> getSelectedSections(ActionContext context) { protected static Set<TraceSection> getSelectedSections(ActionContext context) {
@ -264,7 +264,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
if (context instanceof DebuggerObjectActionContext ctx) { if (context instanceof DebuggerObjectActionContext ctx) {
return DebuggerModulesPanel.getSelectedSectionsFromContext(ctx); return DebuggerModulesPanel.getSelectedSectionsFromContext(ctx);
} }
return null; return Set.of();
} }
protected static AddressSetView getSelectedAddresses(ActionContext context) { protected static AddressSetView getSelectedAddresses(ActionContext context) {
@ -299,7 +299,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
} }
AddressSetView sel = getSelectedAddresses(context); AddressSetView sel = getSelectedAddresses(context);
if (sel == null) { if (sel == null || sel.isEmpty()) {
return; return;
} }
@ -540,9 +540,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
.onAction(this::activatedMapModules) .onAction(this::activatedMapModules)
.buildAndInstallLocal(this); .buildAndInstallLocal(this);
actionMapModuleTo = MapModuleToAction.builder(plugin) actionMapModuleTo = MapModuleToAction.builder(plugin)
.withContext(DebuggerModuleActionContext.class) .enabledWhen(ctx -> currentProgram != null && getSelectedModules(ctx).size() == 1)
.enabledWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1) .popupWhen(ctx -> currentProgram != null && getSelectedModules(ctx).size() == 1)
.popupWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1)
.onAction(this::activatedMapModuleTo) .onAction(this::activatedMapModuleTo)
.buildAndInstallLocal(this); .buildAndInstallLocal(this);
actionMapSections = MapSectionsAction.builder(plugin) actionMapSections = MapSectionsAction.builder(plugin)
@ -551,9 +550,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
.onAction(this::activatedMapSections) .onAction(this::activatedMapSections)
.buildAndInstallLocal(this); .buildAndInstallLocal(this);
actionMapSectionTo = MapSectionToAction.builder(plugin) actionMapSectionTo = MapSectionToAction.builder(plugin)
.withContext(DebuggerSectionActionContext.class) .enabledWhen(ctx -> currentProgram != null && getSelectedSections(ctx).size() == 1)
.enabledWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1) .popupWhen(ctx -> currentProgram != null && getSelectedSections(ctx).size() == 1)
.popupWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1)
.onAction(this::activatedMapSectionTo) .onAction(this::activatedMapSectionTo)
.buildAndInstallLocal(this); .buildAndInstallLocal(this);
actionMapSectionsTo = MapSectionsToAction.builder(plugin) actionMapSectionsTo = MapSectionsToAction.builder(plugin)

View file

@ -25,6 +25,7 @@ import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.MemoryBlock; import ghidra.program.model.mem.MemoryBlock;
import ghidra.trace.model.Lifespan; import ghidra.trace.model.Lifespan;
import ghidra.trace.model.memory.TraceMemoryRegion; import ghidra.trace.model.memory.TraceMemoryRegion;
import ghidra.trace.model.memory.TraceObjectMemoryRegion;
import ghidra.trace.model.modules.TraceModule; import ghidra.trace.model.modules.TraceModule;
public class DefaultModuleMapProposal public class DefaultModuleMapProposal
@ -207,10 +208,14 @@ public class DefaultModuleMapProposal
catch (AddressOverflowException e) { catch (AddressOverflowException e) {
return; // Just score it as having no matches? return; // Just score it as having no matches?
} }
Lifespan lifespan = module.getLifespan();
for (TraceMemoryRegion region : module.getTrace() for (TraceMemoryRegion region : module.getTrace()
.getMemoryManager() .getMemoryManager()
.getRegionsIntersecting(module.getLifespan(), moduleRange)) { .getRegionsIntersecting(lifespan, moduleRange)) {
getMatcher(region.getMinAddress().subtract(moduleBase)).region = region; Address min = region instanceof TraceObjectMemoryRegion objReg
? objReg.getMinAddress(lifespan.lmin())
: region.getMinAddress();
getMatcher(min.subtract(moduleBase)).region = region;
} }
} }

View file

@ -15,8 +15,8 @@
*/ */
package ghidra.dbg.util; package ghidra.dbg.util;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
public class ShellUtils { public class ShellUtils {
enum State { enum State {
@ -139,4 +139,11 @@ public class ShellUtils {
} }
return line.toString(); return line.toString();
} }
public static String generateEnvBlock(Map<String, String> env) {
return env.entrySet()
.stream()
.map(e -> e.getKey() + "=" + e.getValue() + "\0")
.collect(Collectors.joining()); // NB. JNA adds final terminator
}
} }

View file

@ -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 @Override
public AddressRange getRange() { public AddressRange getRange() {
try (LockHold hold = object.getTrace().lockRead()) { try (LockHold hold = object.getTrace().lockRead()) {
if (object.getLife().isEmpty()) { if (object.getLife().isEmpty()) {
return range; return range;
} }
return range = TraceObjectInterfaceUtils.getValue(object, getCreationSnap(), return getRange(getCreationSnap());
TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, AddressRange.class, range);
} }
} }
@ -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 @Override
public Address getMinAddress() { public Address getMinAddress() {
AddressRange range = getRange(); 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 @Override
public Address getMaxAddress() { public Address getMaxAddress() {
AddressRange range = getRange(); AddressRange range = getRange();

View file

@ -20,6 +20,7 @@ import java.util.Set;
import ghidra.dbg.target.TargetMemoryRegion; import ghidra.dbg.target.TargetMemoryRegion;
import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetObject;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange; import ghidra.program.model.address.AddressRange;
import ghidra.trace.model.Lifespan; import ghidra.trace.model.Lifespan;
import ghidra.trace.model.target.TraceObjectInterface; import ghidra.trace.model.target.TraceObjectInterface;
@ -39,6 +40,12 @@ public interface TraceObjectMemoryRegion extends TraceMemoryRegion, TraceObjectI
void setRange(Lifespan lifespan, AddressRange range); void setRange(Lifespan lifespan, AddressRange range);
AddressRange getRange(long snap);
Address getMinAddress(long snap);
Address getMaxAddress(long snap);
void setFlags(Lifespan lifespan, Collection<TraceMemoryFlag> flags); void setFlags(Lifespan lifespan, Collection<TraceMemoryFlag> flags);
void addFlags(Lifespan lifespan, Collection<TraceMemoryFlag> flags); void addFlags(Lifespan lifespan, Collection<TraceMemoryFlag> flags);

View file

@ -15,6 +15,7 @@
*/ */
package ghidra.pty; package ghidra.pty;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
@ -51,19 +52,28 @@ public interface PtyChild extends PtyEndpoint {
* *
* @param args the image path and arguments * @param args the image path and arguments
* @param env the environment * @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. * @param mode the terminal mode. If a mode is not implemented, it may be silently ignored.
* @return a handle to the subprocess * @return a handle to the subprocess
* @throws IOException if the session could not be started * @throws IOException if the session could not be started
*/ */
PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode) PtySession session(String[] args, Map<String, String> env, File workingDirectory,
throws IOException; Collection<TermMode> mode) throws IOException;
/** /**
* @see #session(String[], Map, Collection) * @see #session(String[], Map, File, Collection)
*/
default PtySession session(String[] args, Map<String, String> 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<String, String> env, TermMode... mode) default PtySession session(String[] args, Map<String, String> env, TermMode... mode)
throws IOException { throws IOException {
return session(args, env, List.of(mode)); return session(args, env, null, List.of(mode));
} }
/** /**

View file

@ -57,13 +57,13 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
* program is active before sending special characters. * program is active before sending special characters.
*/ */
@Override @Override
public PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode) public PtySession session(String[] args, Map<String, String> env, File workingDirectory,
throws IOException { Collection<TermMode> mode) throws IOException {
return sessionUsingJavaLeader(args, env, mode); return sessionUsingJavaLeader(args, env, workingDirectory, mode);
} }
protected PtySession sessionUsingJavaLeader(String[] args, Map<String, String> env, protected PtySession sessionUsingJavaLeader(String[] args, Map<String, String> env,
Collection<TermMode> mode) throws IOException { File workingDirectory, Collection<TermMode> mode) throws IOException {
final List<String> argsList = new ArrayList<>(); final List<String> argsList = new ArrayList<>();
String javaCommand = String javaCommand =
System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
@ -78,6 +78,9 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
if (env != null) { if (env != null) {
builder.environment().putAll(env); builder.environment().putAll(env);
} }
if (workingDirectory != null) {
builder.directory(workingDirectory);
}
builder.inheritIO(); builder.inheritIO();
applyMode(mode); applyMode(mode);

View file

@ -48,8 +48,11 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
} }
@Override @Override
public SshPtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode) public SshPtySession session(String[] args, Map<String, String> env, File workingDirectory,
throws IOException { Collection<TermMode> 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 * 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 :) * universal. This certainly works for my version of bash :)

View file

@ -15,6 +15,7 @@
*/ */
package ghidra.pty.windows; package ghidra.pty.windows;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
@ -75,7 +76,7 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
@Override @Override
public LocalWindowsNativeProcessPtySession session(String[] args, Map<String, String> env, public LocalWindowsNativeProcessPtySession session(String[] args, Map<String, String> env,
Collection<TermMode> mode) throws IOException { File workingDirectory, Collection<TermMode> mode) throws IOException {
/** /**
* TODO: How to incorporate environment into CreateProcess? * TODO: How to incorporate environment into CreateProcess?
* *
@ -91,9 +92,11 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
null /*lpProcessAttributes*/, null /*lpProcessAttributes*/,
null /*lpThreadAttributes*/, null /*lpThreadAttributes*/,
false /*bInheritHandles*/, false /*bInheritHandles*/,
ConPty.EXTENDED_STARTUPINFO_PRESENT /*dwCreationFlags*/, new DWORD(Kernel32.EXTENDED_STARTUPINFO_PRESENT |
null /*lpEnvironment*/, Kernel32.CREATE_UNICODE_ENVIRONMENT) /*dwCreationFlags*/,
null /*lpCurrentDirectory*/, env == null ? null : new WString(ShellUtils.generateEnvBlock(env)),
workingDirectory == null ? null
: new WString(workingDirectory.getAbsolutePath()) /*lpCurrentDirectory*/,
si /*lpStartupInfo*/, si /*lpStartupInfo*/,
pi /*lpProcessInformation*/).booleanValue()) { pi /*lpProcessInformation*/).booleanValue()) {
throw new LastErrorException(Kernel32.INSTANCE.GetLastError()); throw new LastErrorException(Kernel32.INSTANCE.GetLastError());

View file

@ -83,7 +83,9 @@ public class HandleInputStream extends InputStream {
public synchronized int read(byte[] b, int off, int len) throws IOException { public synchronized int read(byte[] b, int off, int len) throws IOException {
byte[] temp = new byte[len]; byte[] temp = new byte[len];
int read = read(temp); int read = read(temp);
System.arraycopy(temp, 0, b, off, read); if (read > 0) {
System.arraycopy(temp, 0, b, off, read);
}
return read; return read;
} }

View file

@ -57,7 +57,7 @@ public interface ConsoleApiNative extends StdCallLibrary {
WinBase.SECURITY_ATTRIBUTES lpThreadAttributes, WinBase.SECURITY_ATTRIBUTES lpThreadAttributes,
boolean bInheritHandles, boolean bInheritHandles,
DWORD dwCreationFlags, DWORD dwCreationFlags,
Pointer lpEnvironment, WString lpEnvironment,
WString lpCurrentDirectory, WString lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo, STARTUPINFOEX lpStartupInfo,
WinBase.PROCESS_INFORMATION lpProcessInformation); WinBase.PROCESS_INFORMATION lpProcessInformation);

View file

@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assume.assumeTrue;
import java.util.*; import java.util.*;
@ -28,6 +29,7 @@ import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.services.TraceRmiLauncherService; import ghidra.app.services.TraceRmiLauncherService;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*; import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*;
import ghidra.framework.OperatingSystem;
import ghidra.util.task.ConsoleTaskMonitor; import ghidra.util.task.ConsoleTaskMonitor;
public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebuggerTest { public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebuggerTest {
@ -50,7 +52,7 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
assertFalse(launcherService.getOffers(program).isEmpty()); assertFalse(launcherService.getOffers(program).isEmpty());
} }
protected LaunchConfigurator fileOnly(String file) { protected LaunchConfigurator gdbFileOnly(String file) {
return new LaunchConfigurator() { return new LaunchConfigurator() {
@Override @Override
public Map<String, ?> configureLauncher(TraceRmiLaunchOffer offer, public Map<String, ?> 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 // @Test // This is currently hanging the test machine. The gdb process is left running
public void testLaunchLocalGdb() throws Exception { public void testLaunchLocalGdb() throws Exception {
assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.LINUX);
createProgram(getSLEIGH_X86_64_LANGUAGE()); createProgram(getSLEIGH_X86_64_LANGUAGE());
try (Transaction tx = program.openTransaction("Rename")) { try (Transaction tx = program.openTransaction("Rename")) {
program.setName("bash"); program.setName("bash");
} }
programManager.openProgram(program); programManager.openProgram(program);
TraceRmiLaunchOffer gdbOffer = findByTitle(launcherService.getOffers(program), "gdb"); TraceRmiLaunchOffer offer = findByTitle(launcherService.getOffers(program), "gdb");
try (LaunchResult result = try (LaunchResult result =
gdbOffer.launchProgram(new ConsoleTaskMonitor(), fileOnly("/usr/bin/bash"))) { offer.launchProgram(new ConsoleTaskMonitor(), gdbFileOnly("/usr/bin/bash"))) {
if (result.exception() != null) { if (result.exception() != null) {
throw new AssertionError(result.exception()); throw new AssertionError(result.exception());
} }
@ -83,4 +87,39 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
assertEquals(getSLEIGH_X86_64_LANGUAGE(), result.trace().getBaseLanguage()); assertEquals(getSLEIGH_X86_64_LANGUAGE(), result.trace().getBaseLanguage());
} }
} }
protected LaunchConfigurator dbgengFileOnly(String file) {
return new LaunchConfigurator() {
@Override
public Map<String, ?> configureLauncher(TraceRmiLaunchOffer offer,
Map<String, ?> arguments, RelPrompt relPrompt) {
Map<String, Object> 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());
}
}
} }