mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-22 04:05:39 +00:00
Merge remote-tracking branch 'origin/GP-3823_Dan_traceRmiWindowsLaunchers--SQUASHED'
This commit is contained in:
commit
968a36f2b5
@ -1,8 +1,7 @@
|
||||
##VERSION: 2.0
|
||||
##MODULE IP: Apache License 2.0
|
||||
Module.manifest||GHIDRA||||END|
|
||||
src/javaprovider/def/javaprovider.def||GHIDRA||||END|
|
||||
src/javaprovider/rc/javaprovider.rc||GHIDRA||||END|
|
||||
data/debugger-launchers/local-dbgeng.bat||GHIDRA||||END|
|
||||
src/main/py/LICENSE||GHIDRA||||END|
|
||||
src/main/py/README.md||GHIDRA||||END|
|
||||
src/main/py/pyproject.toml||GHIDRA||||END|
|
||||
|
@ -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
|
@ -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()
|
@ -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);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
EXPORTS
|
||||
|
||||
; For ExtCpp
|
||||
DebugExtensionInitialize
|
||||
DebugExtensionUninitialize
|
||||
DebugExtensionNotify
|
||||
help
|
||||
|
||||
; My Commands
|
||||
java_add_cp
|
||||
java_set
|
||||
java_get
|
||||
java_run
|
@ -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>
|
@ -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
|
@ -19,6 +19,7 @@ import os.path
|
||||
import socket
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
|
||||
from ghidratrace import sch
|
||||
from ghidratrace.client import Client, Address, AddressRange, TraceObject
|
||||
@ -185,7 +186,7 @@ def compute_name(progname=None):
|
||||
progname = buffer.decode('utf-8')
|
||||
except Exception:
|
||||
return 'pydbg/noname'
|
||||
return 'pydbg/' + progname.split('/')[-1]
|
||||
return 'pydbg/' + re.split(r'/|\\', progname)[-1]
|
||||
|
||||
|
||||
def start_trace(name):
|
||||
@ -1301,7 +1302,36 @@ def ghidra_util_wait_stopped(timeout=1):
|
||||
time.sleep(0.1)
|
||||
if time.time() - start > timeout:
|
||||
raise RuntimeError('Timed out waiting for thread to stop')
|
||||
|
||||
|
||||
|
||||
|
||||
def dbg():
|
||||
return util.get_debugger()
|
||||
|
||||
|
||||
SHOULD_WAIT = ['GO', 'STEP_BRANCH', 'STEP_INTO', 'STEP_OVER']
|
||||
|
||||
|
||||
def repl():
|
||||
print("This is the dbgeng.dll (WinDbg) REPL. To drop to Python3, press Ctrl-C.")
|
||||
while True:
|
||||
# TODO: Implement prompt retrieval in PR to pybag?
|
||||
print('dbg> ', end='')
|
||||
try:
|
||||
cmd = input().strip()
|
||||
if not cmd:
|
||||
continue
|
||||
dbg().cmd(cmd, quiet=False)
|
||||
stat = dbg().exec_status()
|
||||
if stat != 'BREAK':
|
||||
dbg().wait()
|
||||
else:
|
||||
pass
|
||||
#dbg().dispatch_events()
|
||||
except KeyboardInterrupt as e:
|
||||
print("")
|
||||
print("You have left the dbgeng REPL and are now at the Python3 interpreter.")
|
||||
print("use repl() to re-enter.")
|
||||
return
|
||||
except:
|
||||
# Assume cmd() has already output the error
|
||||
pass
|
||||
|
@ -383,7 +383,7 @@ def interrupt():
|
||||
|
||||
@REGISTRY.method(action='step_into')
|
||||
def step_into(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1):
|
||||
"""Step on instruction exactly."""
|
||||
"""Step one instruction exactly."""
|
||||
find_thread_by_obj(thread)
|
||||
dbg().stepi(n)
|
||||
|
||||
@ -511,7 +511,7 @@ def write_mem(process: sch.Schema('Process'), address: Address, data: bytes):
|
||||
|
||||
|
||||
@REGISTRY.method
|
||||
def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes):
|
||||
def write_reg(frame: sch.Schema('StackFrame'), name: str, value: bytes):
|
||||
"""Write a register."""
|
||||
util.select_frame()
|
||||
nproc = pydbg.selected_process()
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -420,8 +420,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
}
|
||||
|
||||
protected PtyTerminalSession runInTerminal(List<String> commandLine, Map<String, String> env,
|
||||
Collection<TerminalSession> subordinates)
|
||||
throws IOException {
|
||||
File workingDirectory, Collection<TerminalSession> subordinates) throws IOException {
|
||||
PtyFactory factory = getPtyFactory();
|
||||
Pty pty = factory.openpty();
|
||||
|
||||
@ -432,13 +431,19 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
|
||||
TerminalListener resizeListener = new TerminalListener() {
|
||||
@Override
|
||||
public void resized(short cols, short rows) {
|
||||
parent.setWindowSize(cols, rows);
|
||||
try {
|
||||
parent.setWindowSize(cols, rows);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Could not resize pty: " + e);
|
||||
}
|
||||
}
|
||||
};
|
||||
terminal.addTerminalListener(resizeListener);
|
||||
|
||||
env.put("TERM", "xterm-256color");
|
||||
PtySession session = pty.getChild().session(commandLine.toArray(String[]::new), env);
|
||||
PtySession session =
|
||||
pty.getChild().session(commandLine.toArray(String[]::new), env, workingDirectory);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
try {
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -88,6 +88,9 @@ public class LaunchAction extends MultiActionDockingAction {
|
||||
|
||||
ConfigLast findMostRecentConfig() {
|
||||
Program program = plugin.currentProgram;
|
||||
if (program == null) {
|
||||
return null;
|
||||
}
|
||||
ConfigLast best = null;
|
||||
|
||||
ProgramUserData userData = program.getProgramUserData();
|
||||
@ -113,14 +116,16 @@ public class LaunchAction extends MultiActionDockingAction {
|
||||
|
||||
List<DockingActionIf> actions = new ArrayList<>();
|
||||
|
||||
ProgramUserData userData = program.getProgramUserData();
|
||||
Map<String, Long> saved = new HashMap<>();
|
||||
for (String propName : userData.getStringPropertyNames()) {
|
||||
ConfigLast check = checkSavedConfig(userData, propName);
|
||||
if (check == null) {
|
||||
continue;
|
||||
if (program != null) {
|
||||
ProgramUserData userData = program.getProgramUserData();
|
||||
for (String propName : userData.getStringPropertyNames()) {
|
||||
ConfigLast check = checkSavedConfig(userData, propName);
|
||||
if (check == null) {
|
||||
continue;
|
||||
}
|
||||
saved.put(check.configName, check.last);
|
||||
}
|
||||
saved.put(check.configName, check.last);
|
||||
}
|
||||
|
||||
for (TraceRmiLaunchOffer offer : offers) {
|
||||
@ -134,6 +139,8 @@ public class LaunchAction extends MultiActionDockingAction {
|
||||
.build());
|
||||
Long last = saved.get(offer.getConfigName());
|
||||
if (last == null) {
|
||||
// NB. If program == null, this will always happen.
|
||||
// Thus, no worries about program.getName() below.
|
||||
continue;
|
||||
}
|
||||
actions.add(new ActionBuilder(offer.getConfigName(), plugin.getName())
|
||||
@ -172,6 +179,11 @@ public class LaunchAction extends MultiActionDockingAction {
|
||||
// Make accessible to this file
|
||||
return super.showPopup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTipText() {
|
||||
return getDescription();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -180,19 +192,45 @@ public class LaunchAction extends MultiActionDockingAction {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
// See comment on super method about use of runLater
|
||||
ConfigLast last = findMostRecentConfig();
|
||||
public boolean isEnabledForContext(ActionContext context) {
|
||||
return plugin.currentProgram != null;
|
||||
}
|
||||
|
||||
protected TraceRmiLaunchOffer findOffer(ConfigLast last) {
|
||||
if (last == null) {
|
||||
Swing.runLater(() -> button.showPopup());
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
for (TraceRmiLaunchOffer offer : plugin.getOffers(plugin.currentProgram)) {
|
||||
if (offer.getConfigName().equals(last.configName)) {
|
||||
plugin.relaunch(offer);
|
||||
return;
|
||||
return offer;
|
||||
}
|
||||
}
|
||||
Swing.runLater(() -> button.showPopup());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
// See comment on super method about use of runLater
|
||||
ConfigLast last = findMostRecentConfig();
|
||||
TraceRmiLaunchOffer offer = findOffer(last);
|
||||
if (offer == null) {
|
||||
Swing.runLater(() -> button.showPopup());
|
||||
return;
|
||||
}
|
||||
plugin.relaunch(offer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
Program program = plugin.currentProgram;
|
||||
if (program == null) {
|
||||
return "Launch (program required)";
|
||||
}
|
||||
ConfigLast last = findMostRecentConfig();
|
||||
TraceRmiLaunchOffer offer = findOffer(last);
|
||||
if (last == null) {
|
||||
return "Configure and launch " + program.getName();
|
||||
}
|
||||
return "Re-launch " + program.getName() + " using " + offer.getTitle();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -15,454 +15,25 @@
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
|
||||
|
||||
import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import generic.theme.GIcon;
|
||||
import generic.theme.Gui;
|
||||
import ghidra.dbg.target.TargetMethod.ParameterDescription;
|
||||
import ghidra.dbg.util.ShellUtils;
|
||||
import ghidra.debug.api.tracermi.TerminalSession;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.app.plugin.core.debug.gui.tracermi.launcher.ScriptAttributesParser.ScriptAttributes;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
* A launcher implemented by a simple UNIX shell script.
|
||||
*
|
||||
* <p>
|
||||
* The script must start with an attributes header in a comment block. Some attributes are required.
|
||||
* Others are optional:
|
||||
* <ul>
|
||||
* <li>{@code @menu-path}: <b>(Required)</b></li>
|
||||
* </ul>
|
||||
* The script must start with an attributes header in a comment block. See
|
||||
* {@link ScriptAttributesParser}.
|
||||
*/
|
||||
public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOffer {
|
||||
public class UnixShellScriptTraceRmiLaunchOffer extends AbstractScriptTraceRmiLaunchOffer {
|
||||
public static final String SHEBANG = "#!";
|
||||
|
||||
public static final String AT_TITLE = "@title";
|
||||
public static final String AT_DESC = "@desc";
|
||||
public static final String AT_MENU_PATH = "@menu-path";
|
||||
public static final String AT_MENU_GROUP = "@menu-group";
|
||||
public static final String AT_MENU_ORDER = "@menu-order";
|
||||
public static final String AT_ICON = "@icon";
|
||||
public static final String AT_HELP = "@help";
|
||||
public static final String AT_ENUM = "@enum";
|
||||
public static final String AT_ENV = "@env";
|
||||
public static final String AT_ARG = "@arg";
|
||||
public static final String AT_ARGS = "@args";
|
||||
public static final String AT_TTY = "@tty";
|
||||
|
||||
public static final String PREFIX_ENV = "env:";
|
||||
public static final String PREFIX_ARG = "arg:";
|
||||
public static final String KEY_ARGS = "args";
|
||||
|
||||
public static final String MSGPAT_INVALID_HELP_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use Topic#anchor";
|
||||
public static final String MSGPAT_INVALID_ENUM_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use NAME:type Choice1 [ChoiceN...]";
|
||||
public static final String MSGPAT_INVALID_ENV_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use NAME:type=default \"Display\" \"Tool Tip\"";
|
||||
public static final String MSGPAT_INVALID_ARG_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use :type \"Display\" \"Tool Tip\"";
|
||||
public static final String MSGPAT_INVALID_ARGS_SYNTAX =
|
||||
"%s: Invalid %s syntax. Use \"Display\" \"Tool Tip\"";
|
||||
|
||||
protected record Location(String fileName, int lineNo) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "%s:%d".formatted(fileName, lineNo);
|
||||
}
|
||||
}
|
||||
|
||||
protected interface OptType<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.
|
||||
*
|
||||
@ -473,161 +44,36 @@ public class UnixShellScriptTraceRmiLaunchOffer extends AbstractTraceRmiLaunchOf
|
||||
*/
|
||||
public static UnixShellScriptTraceRmiLaunchOffer create(TraceRmiLauncherServicePlugin plugin,
|
||||
Program program, File script) throws FileNotFoundException {
|
||||
try (BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(new FileInputStream(script)))) {
|
||||
AttributesParser attrs = new AttributesParser();
|
||||
String line;
|
||||
for (int lineNo = 1; (line = reader.readLine()) != null; lineNo++) {
|
||||
if (line.startsWith(SHEBANG) && lineNo == 1) {
|
||||
}
|
||||
else if (line.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
else if (line.stripLeading().startsWith("#")) {
|
||||
attrs.parseLine(new Location(script.getName(), lineNo), line);
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
ScriptAttributesParser parser = new ScriptAttributesParser() {
|
||||
@Override
|
||||
protected boolean ignoreLine(int lineNo, String line) {
|
||||
return line.isBlank() || line.startsWith(SHEBANG) && lineNo == 1;
|
||||
}
|
||||
attrs.validate(script.getName());
|
||||
return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, script,
|
||||
"UNIX_SHELL:" + script.getName(), attrs.title, attrs.getDescription(),
|
||||
attrs.menuPath, attrs.menuGroup, attrs.menuOrder, new GIcon(attrs.iconId),
|
||||
attrs.helpLocation, attrs.parameters, attrs.extraTtys);
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
// Avoid capture by IOException
|
||||
throw e;
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected final File script;
|
||||
protected final String configName;
|
||||
protected final String title;
|
||||
protected final String description;
|
||||
protected final List<String> menuPath;
|
||||
protected final String menuGroup;
|
||||
protected final String menuOrder;
|
||||
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)));
|
||||
@Override
|
||||
protected String removeDelimiter(String line) {
|
||||
String stripped = line.stripLeading();
|
||||
if (!stripped.startsWith("#")) {
|
||||
return null;
|
||||
}
|
||||
return stripped.substring(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
ScriptAttributes attrs = parser.parseFile(script);
|
||||
return new UnixShellScriptTraceRmiLaunchOffer(plugin, program, script,
|
||||
"UNIX_SHELL:" + script.getName(), attrs);
|
||||
}
|
||||
|
||||
for (String tty : extraTtys) {
|
||||
NullPtyTerminalSession ns = nullPtyTerminal();
|
||||
env.put(tty, ns.name());
|
||||
sessions.put(ns.name(), ns);
|
||||
}
|
||||
private UnixShellScriptTraceRmiLaunchOffer(TraceRmiLauncherServicePlugin plugin,
|
||||
Program program,
|
||||
File script, String configName, ScriptAttributes attrs) {
|
||||
super(plugin, program, script, configName, attrs);
|
||||
}
|
||||
|
||||
sessions.put("Shell", runInTerminal(commandLine, env, sessions.values()));
|
||||
@Override
|
||||
protected void prepareSubprocess(List<String> commandLine, Map<String, String> env,
|
||||
Map<String, ?> args, SocketAddress address) {
|
||||
ScriptAttributesParser.processArguments(commandLine, env, script, attrs.parameters(), args,
|
||||
address);
|
||||
}
|
||||
}
|
||||
|
@ -20,63 +20,30 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
|
||||
import ghidra.debug.spi.tracermi.TraceRmiLaunchOpinion;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.options.OptionType;
|
||||
import ghidra.framework.options.Options;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.plugintool.util.PluginUtils;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class UnixShellScriptTraceRmiLaunchOpinion implements TraceRmiLaunchOpinion {
|
||||
|
||||
@Override
|
||||
public void registerOptions(Options options) {
|
||||
String pluginName = PluginUtils.getPluginNameFromClass(TraceRmiLauncherServicePlugin.class);
|
||||
options.registerOption(TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS,
|
||||
OptionType.STRING_TYPE, "", new HelpLocation(pluginName, "options"),
|
||||
"Paths to search for user-created debugger launchers", new ScriptPathsPropertyEditor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresRefresh(String optionName) {
|
||||
return TraceRmiLauncherServicePlugin.OPTION_NAME_SCRIPT_PATHS.equals(optionName);
|
||||
}
|
||||
|
||||
protected Stream<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));
|
||||
}
|
||||
public class UnixShellScriptTraceRmiLaunchOpinion 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(".sh"))))
|
||||
.flatMap(sf -> {
|
||||
try {
|
||||
return Stream.of(UnixShellScriptTraceRmiLaunchOffer.create(plugin, program,
|
||||
sf.getFile(false)));
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Could not offer " + sf + ":" + e.getMessage(), e);
|
||||
return Stream.of();
|
||||
}
|
||||
})
|
||||
.flatMap(sf -> createOffer(plugin, program, sf))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
protected Stream<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,8 +20,6 @@ import java.math.BigInteger;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
@ -329,17 +327,17 @@ public class TraceRmiHandler implements TraceRmiConnection {
|
||||
}
|
||||
}
|
||||
|
||||
protected DomainFolder createFolders(DomainFolder parent, Path path)
|
||||
protected DomainFolder createFolders(DomainFolder parent, List<String> path)
|
||||
throws InvalidNameException, IOException {
|
||||
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 {
|
||||
if (path == null && index == 0 || index == path.getNameCount()) {
|
||||
if (path == null && index == 0 || index == path.size()) {
|
||||
return parent;
|
||||
}
|
||||
String name = path.getName(index).toString();
|
||||
String name = path.get(index);
|
||||
return createFolders(getOrCreateFolder(parent, name), path, index + 1);
|
||||
}
|
||||
|
||||
@ -859,10 +857,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
|
||||
protected ReplyCreateTrace handleCreateTrace(RequestCreateTrace req)
|
||||
throws InvalidNameException, IOException, CancelledException {
|
||||
DomainFolder traces = getOrCreateNewTracesFolder();
|
||||
Path path = Paths.get(req.getPath().getPath());
|
||||
DomainFolder folder = createFolders(traces, path.getParent());
|
||||
List<String> path = sanitizePath(req.getPath().getPath());
|
||||
DomainFolder folder = createFolders(traces, path.subList(0, path.size() - 1));
|
||||
CompilerSpec cs = requireCompilerSpec(req.getLanguage(), req.getCompiler());
|
||||
DBTrace trace = new DBTrace(path.getFileName().toString(), cs, this);
|
||||
DBTrace trace = new DBTrace(path.get(path.size() - 1), cs, this);
|
||||
TraceRmiTarget target = new TraceRmiTarget(plugin.getTool(), this, trace);
|
||||
DoId doId = requireAvailableDoId(req.getOid());
|
||||
openTraces.put(new OpenTrace(doId, trace, target));
|
||||
@ -870,6 +868,10 @@ public class TraceRmiHandler implements TraceRmiConnection {
|
||||
return ReplyCreateTrace.getDefaultInstance();
|
||||
}
|
||||
|
||||
protected static List<String> sanitizePath(String path) {
|
||||
return Stream.of(path.split("\\\\|/")).filter(p -> !p.isBlank()).toList();
|
||||
}
|
||||
|
||||
protected ReplyDeleteBytes handleDeleteBytes(RequestDeleteBytes req)
|
||||
throws AddressOverflowException {
|
||||
OpenTrace open = requireOpenTrace(req.getOid());
|
||||
|
@ -251,7 +251,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
if (context instanceof DebuggerObjectActionContext ctx) {
|
||||
return DebuggerModulesPanel.getSelectedModulesFromContext(ctx);
|
||||
}
|
||||
return null;
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
protected static Set<TraceSection> getSelectedSections(ActionContext context) {
|
||||
@ -264,7 +264,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
if (context instanceof DebuggerObjectActionContext ctx) {
|
||||
return DebuggerModulesPanel.getSelectedSectionsFromContext(ctx);
|
||||
}
|
||||
return null;
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
protected static AddressSetView getSelectedAddresses(ActionContext context) {
|
||||
@ -299,7 +299,7 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
}
|
||||
|
||||
AddressSetView sel = getSelectedAddresses(context);
|
||||
if (sel == null) {
|
||||
if (sel == null || sel.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -540,9 +540,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
.onAction(this::activatedMapModules)
|
||||
.buildAndInstallLocal(this);
|
||||
actionMapModuleTo = MapModuleToAction.builder(plugin)
|
||||
.withContext(DebuggerModuleActionContext.class)
|
||||
.enabledWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1)
|
||||
.popupWhen(ctx -> currentProgram != null && ctx.getSelectedModules().size() == 1)
|
||||
.enabledWhen(ctx -> currentProgram != null && getSelectedModules(ctx).size() == 1)
|
||||
.popupWhen(ctx -> currentProgram != null && getSelectedModules(ctx).size() == 1)
|
||||
.onAction(this::activatedMapModuleTo)
|
||||
.buildAndInstallLocal(this);
|
||||
actionMapSections = MapSectionsAction.builder(plugin)
|
||||
@ -551,9 +550,8 @@ public class DebuggerModulesProvider extends ComponentProviderAdapter {
|
||||
.onAction(this::activatedMapSections)
|
||||
.buildAndInstallLocal(this);
|
||||
actionMapSectionTo = MapSectionToAction.builder(plugin)
|
||||
.withContext(DebuggerSectionActionContext.class)
|
||||
.enabledWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1)
|
||||
.popupWhen(ctx -> currentProgram != null && ctx.getSelectedSections().size() == 1)
|
||||
.enabledWhen(ctx -> currentProgram != null && getSelectedSections(ctx).size() == 1)
|
||||
.popupWhen(ctx -> currentProgram != null && getSelectedSections(ctx).size() == 1)
|
||||
.onAction(this::activatedMapSectionTo)
|
||||
.buildAndInstallLocal(this);
|
||||
actionMapSectionsTo = MapSectionsToAction.builder(plugin)
|
||||
|
@ -25,6 +25,7 @@ import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.memory.TraceMemoryRegion;
|
||||
import ghidra.trace.model.memory.TraceObjectMemoryRegion;
|
||||
import ghidra.trace.model.modules.TraceModule;
|
||||
|
||||
public class DefaultModuleMapProposal
|
||||
@ -207,10 +208,14 @@ public class DefaultModuleMapProposal
|
||||
catch (AddressOverflowException e) {
|
||||
return; // Just score it as having no matches?
|
||||
}
|
||||
Lifespan lifespan = module.getLifespan();
|
||||
for (TraceMemoryRegion region : module.getTrace()
|
||||
.getMemoryManager()
|
||||
.getRegionsIntersecting(module.getLifespan(), moduleRange)) {
|
||||
getMatcher(region.getMinAddress().subtract(moduleBase)).region = region;
|
||||
.getRegionsIntersecting(lifespan, moduleRange)) {
|
||||
Address min = region instanceof TraceObjectMemoryRegion objReg
|
||||
? objReg.getMinAddress(lifespan.lmin())
|
||||
: region.getMinAddress();
|
||||
getMatcher(min.subtract(moduleBase)).region = region;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,8 +15,8 @@
|
||||
*/
|
||||
package ghidra.dbg.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ShellUtils {
|
||||
enum State {
|
||||
@ -139,4 +139,11 @@ public class ShellUtils {
|
||||
}
|
||||
return line.toString();
|
||||
}
|
||||
|
||||
public static String generateEnvBlock(Map<String, String> env) {
|
||||
return env.entrySet()
|
||||
.stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue() + "\0")
|
||||
.collect(Collectors.joining()); // NB. JNA adds final terminator
|
||||
}
|
||||
}
|
||||
|
@ -195,14 +195,22 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddressRange getRange(long snap) {
|
||||
try (LockHold hold = object.getTrace().lockRead()) {
|
||||
// TODO: Caching without regard to snap seems bad
|
||||
return range = TraceObjectInterfaceUtils.getValue(object, snap,
|
||||
TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, AddressRange.class, range);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddressRange getRange() {
|
||||
try (LockHold hold = object.getTrace().lockRead()) {
|
||||
if (object.getLife().isEmpty()) {
|
||||
return range;
|
||||
}
|
||||
return range = TraceObjectInterfaceUtils.getValue(object, getCreationSnap(),
|
||||
TargetMemoryRegion.RANGE_ATTRIBUTE_NAME, AddressRange.class, range);
|
||||
return getRange(getCreationSnap());
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,6 +221,12 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getMinAddress(long snap) {
|
||||
AddressRange range = getRange(snap);
|
||||
return range == null ? null : range.getMinAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getMinAddress() {
|
||||
AddressRange range = getRange();
|
||||
@ -226,6 +240,12 @@ public class DBTraceObjectMemoryRegion implements TraceObjectMemoryRegion, DBTra
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getMaxAddress(long snap) {
|
||||
AddressRange range = getRange(snap);
|
||||
return range == null ? null : range.getMaxAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getMaxAddress() {
|
||||
AddressRange range = getRange();
|
||||
|
@ -20,6 +20,7 @@ import java.util.Set;
|
||||
|
||||
import ghidra.dbg.target.TargetMemoryRegion;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressRange;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.target.TraceObjectInterface;
|
||||
@ -39,6 +40,12 @@ public interface TraceObjectMemoryRegion extends TraceMemoryRegion, TraceObjectI
|
||||
|
||||
void setRange(Lifespan lifespan, AddressRange range);
|
||||
|
||||
AddressRange getRange(long snap);
|
||||
|
||||
Address getMinAddress(long snap);
|
||||
|
||||
Address getMaxAddress(long snap);
|
||||
|
||||
void setFlags(Lifespan lifespan, Collection<TraceMemoryFlag> flags);
|
||||
|
||||
void addFlags(Lifespan lifespan, Collection<TraceMemoryFlag> flags);
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package ghidra.pty;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
@ -51,19 +52,28 @@ public interface PtyChild extends PtyEndpoint {
|
||||
*
|
||||
* @param args the image path and arguments
|
||||
* @param env the environment
|
||||
* @param workingDirectory the working directory
|
||||
* @param mode the terminal mode. If a mode is not implemented, it may be silently ignored.
|
||||
* @return a handle to the subprocess
|
||||
* @throws IOException if the session could not be started
|
||||
*/
|
||||
PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
|
||||
throws IOException;
|
||||
PtySession session(String[] args, Map<String, String> env, File workingDirectory,
|
||||
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)
|
||||
throws IOException {
|
||||
return session(args, env, List.of(mode));
|
||||
return session(args, env, null, List.of(mode));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,13 +57,13 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
|
||||
* program is active before sending special characters.
|
||||
*/
|
||||
@Override
|
||||
public PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
|
||||
throws IOException {
|
||||
return sessionUsingJavaLeader(args, env, mode);
|
||||
public PtySession session(String[] args, Map<String, String> env, File workingDirectory,
|
||||
Collection<TermMode> mode) throws IOException {
|
||||
return sessionUsingJavaLeader(args, env, workingDirectory, mode);
|
||||
}
|
||||
|
||||
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<>();
|
||||
String javaCommand =
|
||||
System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
|
||||
@ -78,6 +78,9 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
|
||||
if (env != null) {
|
||||
builder.environment().putAll(env);
|
||||
}
|
||||
if (workingDirectory != null) {
|
||||
builder.directory(workingDirectory);
|
||||
}
|
||||
builder.inheritIO();
|
||||
|
||||
applyMode(mode);
|
||||
|
@ -48,8 +48,11 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshPtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
|
||||
throws IOException {
|
||||
public SshPtySession session(String[] args, Map<String, String> env, File workingDirectory,
|
||||
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
|
||||
* universal. This certainly works for my version of bash :)
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package ghidra.pty.windows;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
@ -75,7 +76,7 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
|
||||
|
||||
@Override
|
||||
public LocalWindowsNativeProcessPtySession session(String[] args, Map<String, String> env,
|
||||
Collection<TermMode> mode) throws IOException {
|
||||
File workingDirectory, Collection<TermMode> mode) throws IOException {
|
||||
/**
|
||||
* TODO: How to incorporate environment into CreateProcess?
|
||||
*
|
||||
@ -91,9 +92,11 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
|
||||
null /*lpProcessAttributes*/,
|
||||
null /*lpThreadAttributes*/,
|
||||
false /*bInheritHandles*/,
|
||||
ConPty.EXTENDED_STARTUPINFO_PRESENT /*dwCreationFlags*/,
|
||||
null /*lpEnvironment*/,
|
||||
null /*lpCurrentDirectory*/,
|
||||
new DWORD(Kernel32.EXTENDED_STARTUPINFO_PRESENT |
|
||||
Kernel32.CREATE_UNICODE_ENVIRONMENT) /*dwCreationFlags*/,
|
||||
env == null ? null : new WString(ShellUtils.generateEnvBlock(env)),
|
||||
workingDirectory == null ? null
|
||||
: new WString(workingDirectory.getAbsolutePath()) /*lpCurrentDirectory*/,
|
||||
si /*lpStartupInfo*/,
|
||||
pi /*lpProcessInformation*/).booleanValue()) {
|
||||
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
|
||||
|
@ -83,7 +83,9 @@ public class HandleInputStream extends InputStream {
|
||||
public synchronized int read(byte[] b, int off, int len) throws IOException {
|
||||
byte[] temp = new byte[len];
|
||||
int read = read(temp);
|
||||
System.arraycopy(temp, 0, b, off, read);
|
||||
if (read > 0) {
|
||||
System.arraycopy(temp, 0, b, off, read);
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ public interface ConsoleApiNative extends StdCallLibrary {
|
||||
WinBase.SECURITY_ATTRIBUTES lpThreadAttributes,
|
||||
boolean bInheritHandles,
|
||||
DWORD dwCreationFlags,
|
||||
Pointer lpEnvironment,
|
||||
WString lpEnvironment,
|
||||
WString lpCurrentDirectory,
|
||||
STARTUPINFOEX lpStartupInfo,
|
||||
WinBase.PROCESS_INFORMATION lpProcessInformation);
|
||||
|
@ -17,6 +17,7 @@ package ghidra.app.plugin.core.debug.gui.tracermi.launcher;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@ -28,6 +29,7 @@ import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
|
||||
import ghidra.app.services.TraceRmiLauncherService;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer;
|
||||
import ghidra.debug.api.tracermi.TraceRmiLaunchOffer.*;
|
||||
import ghidra.framework.OperatingSystem;
|
||||
import ghidra.util.task.ConsoleTaskMonitor;
|
||||
|
||||
public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebuggerTest {
|
||||
@ -50,7 +52,7 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
|
||||
assertFalse(launcherService.getOffers(program).isEmpty());
|
||||
}
|
||||
|
||||
protected LaunchConfigurator fileOnly(String file) {
|
||||
protected LaunchConfigurator gdbFileOnly(String file) {
|
||||
return new LaunchConfigurator() {
|
||||
@Override
|
||||
public Map<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
|
||||
public void testLaunchLocalGdb() throws Exception {
|
||||
assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.LINUX);
|
||||
|
||||
createProgram(getSLEIGH_X86_64_LANGUAGE());
|
||||
try (Transaction tx = program.openTransaction("Rename")) {
|
||||
program.setName("bash");
|
||||
}
|
||||
programManager.openProgram(program);
|
||||
|
||||
TraceRmiLaunchOffer gdbOffer = findByTitle(launcherService.getOffers(program), "gdb");
|
||||
TraceRmiLaunchOffer offer = findByTitle(launcherService.getOffers(program), "gdb");
|
||||
|
||||
try (LaunchResult result =
|
||||
gdbOffer.launchProgram(new ConsoleTaskMonitor(), fileOnly("/usr/bin/bash"))) {
|
||||
offer.launchProgram(new ConsoleTaskMonitor(), gdbFileOnly("/usr/bin/bash"))) {
|
||||
if (result.exception() != null) {
|
||||
throw new AssertionError(result.exception());
|
||||
}
|
||||
@ -83,4 +87,39 @@ public class TraceRmiLauncherServicePluginTest extends AbstractGhidraHeadedDebug
|
||||
assertEquals(getSLEIGH_X86_64_LANGUAGE(), result.trace().getBaseLanguage());
|
||||
}
|
||||
}
|
||||
|
||||
protected LaunchConfigurator dbgengFileOnly(String file) {
|
||||
return new LaunchConfigurator() {
|
||||
@Override
|
||||
public Map<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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user