diff --git a/JSAPI.md b/JSAPI.md new file mode 100644 index 00000000..e1c4b5ac --- /dev/null +++ b/JSAPI.md @@ -0,0 +1,55 @@ +```js +class LLNode { + /** + * @param {string} dump path to the coredump + * @param {string} executable path to the node executable + * @returns {LLNode} an LLNode instance + */ + static fromCoreDump(dump, executable) {} + + /** + * @returns {string} SBProcess information + */ + getProcessInfo() {} + + /** + * @typedef {object} Frame + * @property {string} function + * + * @typedef {object} Thread + * @property {number} threadId + * @property {number} frameCount + * @property {Frame[]} frames + * + * @typedef {object} Process + * @property {number} pid + * @property {string} state + * @property {number} threadCount + * @property {Thread[]} threads + * + * @returns {Process} Process data + */ + getProcessobject() {} + + /** + * @typedef {object} HeapInstance + * @property {string} address + * @property {string} value + * + * @typedef {object} HeapType + * @property {string} typeName + * @property {string} instanceCount + * @property {string} totalSize + * @property {Iterator} instances + * + * @returns {HeapType[]} + */ + getHeapTypes() {} + + /** + * TODO: rematerialize object + * @returns {HeapInstance} + */ + getobjectAtAddress(address) {} +} +``` diff --git a/Makefile b/Makefile index ef90c5aa..5e3b5939 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ all: @echo "Please take a look at README.md" .PHONY: install-osx -install-osx: +install-osx: plugin mkdir -p ~/Library/Application\ Support/LLDB/PlugIns/ cp -rf ./llnode.dylib \ ~/Library/Application\ Support/LLDB/PlugIns/ @@ -15,7 +15,7 @@ uninstall-osx: rm ~/Library/Application\ Support/LLDB/PlugIns/llnode.dylib .PHONY: install-linux -install-linux: +install-linux: plugin mkdir -p /usr/lib/lldb/plugins cp -rf ./llnode.so /usr/lib/lldb/plugins @@ -37,6 +37,10 @@ plugin: configure node-gyp rebuild node scripts/cleanup.js +.PHONY: addon +addon: configure + node-gyp rebuild + .PHONY: _travis _travis: TEST_LLDB_BINARY="$(TEST_LLDB_BINARY)" \ @@ -49,4 +53,4 @@ clean: $(RM) -r build $(RM) config.gypi $(RM) lldb - $(RM) llnode.so llnode.dylib + $(RM) addon.node llnode.so llnode.dylib diff --git a/binding.gyp b/binding.gyp index 78864d48..172ad68b 100644 --- a/binding.gyp +++ b/binding.gyp @@ -7,7 +7,8 @@ "variables": { # gyp does not appear to let you test for undefined variables, so define # lldb_lib_dir as empty so we can test it later. - "lldb_lib_dir%": "" + "lldb_lib_dir%": "", + "lldb_lib_so%": "" }, "target_defaults": { @@ -63,5 +64,41 @@ "src/llv8-constants.cc", "src/llscan.cc", ] + }, { + "target_name": "addon", + "type": "loadable_module", + "include_dirs": [ + " line.includes('liblldb') && line.startsWith('/')); if (!lib) { - return { dir: undefined, name: 'lldb' }; + return { dir: undefined, name: 'lldb', so: undefined }; } console.log(`From ldd: ${lldbExe} loads ${lib}`); @@ -121,20 +121,22 @@ function getLib(lldbExe, llvmConfig) { // On Ubuntu the libraries are suffixed and installed globally const libName = path.basename(lib).match(/lib(lldb.*?)\.so/)[1]; - // TODO(joyeecheung): on RedHat there might not be a non-versioned liblldb.so + // On RedHat there might not be a non-versioned liblldb.so // in the system. It's fine in the case of plugins since the lldb executable // will load the library before loading the plugin, but we will have to link // to the versioned library file for addons. - if (!fs.existsSync(path.join(libDir, `lib${libName}.so`))) { + if (fs.existsSync(path.join(libDir, `lib${libName}.so`))) { return { - dir: undefined, - name: libName + dir: libDir, + name: libName, + so: undefined }; } return { - dir: libDir, - name: libName + dir: undefined, + name: libName, + so: lib }; } @@ -181,7 +183,9 @@ function getLldbInstallation() { const lib = getLib(lldbExe, llvmConfig); if (!lib.dir) { console.log(`Could not find non-versioned lib${lib.name}.so in the system`); - console.log(`Symbols will be resolved by the lldb executable at runtime`); + console.log('Plugin symbols will be resolved by the lldb executable ' + + 'at runtime'); + console.log(`Addon will be linked to ${lib.so}`); } else { console.log(`Found lib${lib.name}.so in ${lib.dir}`); } @@ -191,7 +195,8 @@ function getLldbInstallation() { version: lldbVersion, includeDir: includeDir, libDir: lib.dir, - libName: lib.name + libName: lib.name, + libPath: lib.so }; } diff --git a/src/addon.cc b/src/addon.cc new file mode 100644 index 00000000..a123c014 --- /dev/null +++ b/src/addon.cc @@ -0,0 +1,13 @@ +#include +#include "llnode_module.h" + +namespace llnode { + +NAN_MODULE_INIT(InitAll) { + LLNode::Init(target); + LLNodeHeapType::Init(target); +} + +NODE_MODULE(addon, InitAll) + +} // namespace llnode diff --git a/src/llnode_api.cc b/src/llnode_api.cc new file mode 100644 index 00000000..2119b621 --- /dev/null +++ b/src/llnode_api.cc @@ -0,0 +1,227 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "src/llnode_api.h" +#include "src/llscan.h" +#include "src/llv8.h" + +namespace llnode { + +LLNodeApi::LLNodeApi() + : loaded(false), + debugger(new lldb::SBDebugger()), + target(new lldb::SBTarget()), + process(new lldb::SBProcess()), + llv8(new v8::LLV8()), + llscan(new LLScan(llv8.get())) {} +LLNodeApi::~LLNodeApi() = default; +LLNodeApi::LLNodeApi(LLNodeApi&&) = default; +LLNodeApi& LLNodeApi::operator=(LLNodeApi&&) = default; + +bool LLNodeApi::debugger_initialized_ = false; + +/* Initialize the SB API and load the core dump */ +bool LLNodeApi::Init(const char* filename, const char* executable) { + if (!LLNodeApi::debugger_initialized_) { + lldb::SBDebugger::Initialize(); + LLNodeApi::debugger_initialized_ = true; + } + + if (loaded) { + return false; + } + + *debugger = lldb::SBDebugger::Create(); + loaded = true; + + // Single instance target for now + *target = debugger->CreateTarget(executable); + if (!target->IsValid()) { + return false; + } + + *process = target->LoadCore(filename); + // Load V8 constants from postmortem data + llscan->v8()->Load(*target); + return true; +} + +std::string LLNodeApi::GetProcessInfo() { + lldb::SBStream info; + process->GetDescription(info); + return std::string(info.GetData()); +} + +uint32_t LLNodeApi::GetProcessID() { return process->GetProcessID(); } + +std::string LLNodeApi::GetProcessState() { + int state = process->GetState(); + + switch (state) { + case lldb::StateType::eStateInvalid: + return "invalid"; + case lldb::StateType::eStateUnloaded: + return "unloaded"; + case lldb::StateType::eStateConnected: + return "connected"; + case lldb::StateType::eStateAttaching: + return "attaching"; + case lldb::StateType::eStateLaunching: + return "launching"; + case lldb::StateType::eStateStopped: + return "stopped"; + case lldb::StateType::eStateRunning: + return "running"; + case lldb::StateType::eStateStepping: + return "stepping"; + case lldb::StateType::eStateCrashed: + return "crashed"; + case lldb::StateType::eStateDetached: + return "detached"; + case lldb::StateType::eStateExited: + return "exited"; + case lldb::StateType::eStateSuspended: + return "suspended"; + default: + return "unknown"; + } +} + +uint32_t LLNodeApi::GetThreadCount() { return process->GetNumThreads(); } + +uint32_t LLNodeApi::GetFrameCount(size_t thread_index) { + lldb::SBThread thread = process->GetThreadAtIndex(thread_index); + if (!thread.IsValid()) { + return 0; + } + return thread.GetNumFrames(); +} + +// TODO: should return a class with +// functionName, directory, file, complieUnitDirectory, compileUnitFile +std::string LLNodeApi::GetFrame(size_t thread_index, size_t frame_index) { + lldb::SBThread thread = process->GetThreadAtIndex(thread_index); + lldb::SBFrame frame = thread.GetFrameAtIndex(frame_index); + lldb::SBSymbol symbol = frame.GetSymbol(); + + std::string result; + char buf[4096]; + if (symbol.IsValid()) { + snprintf(buf, sizeof(buf), "Native: %s", frame.GetFunctionName()); + result += buf; + + lldb::SBModule module = frame.GetModule(); + lldb::SBFileSpec moduleFileSpec = module.GetFileSpec(); + snprintf(buf, sizeof(buf), " [%s/%s]", moduleFileSpec.GetDirectory(), + moduleFileSpec.GetFilename()); + result += buf; + + lldb::SBCompileUnit compileUnit = frame.GetCompileUnit(); + lldb::SBFileSpec compileUnitFileSpec = compileUnit.GetFileSpec(); + if (compileUnitFileSpec.GetDirectory() != nullptr || + compileUnitFileSpec.GetFilename() != nullptr) { + snprintf(buf, sizeof(buf), "\n\t [%s: %s]", + compileUnitFileSpec.GetDirectory(), + compileUnitFileSpec.GetFilename()); + result += buf; + } + } else { + // V8 frame + llnode::v8::Error err; + llnode::v8::JSFrame v8_frame(llscan->v8(), + static_cast(frame.GetFP())); + std::string frame_str = v8_frame.Inspect(true, err); + + // Skip invalid frames + if (err.Fail() || frame_str.length() == 0 || frame_str[0] == '<') { + if (frame_str[0] == '<') { + snprintf(buf, sizeof(buf), "Unknown: %s", frame_str.c_str()); + result += buf; + } else { + result += "???"; + } + } else { + // V8 symbol + snprintf(buf, sizeof(buf), "JavaScript: %s", frame_str.c_str()); + result += buf; + } + } + return result; +} + +void LLNodeApi::ScanHeap() { + lldb::SBCommandReturnObject result; + // Initial scan to create the JavaScript object map + // TODO: make it possible to create multiple instances + // of llscan and llnode + if (!llscan->ScanHeapForObjects(*target, result)) { + return; + } + object_types.clear(); + + // Load the object types into a vector + for (const auto& kv : llscan->GetMapsToInstances()) { + object_types.push_back(kv.second); + } + + // Sort by instance count + std::sort(object_types.begin(), object_types.end(), + TypeRecord::CompareInstanceCounts); +} + +uint32_t LLNodeApi::GetTypeCount() { return object_types.size(); } + +std::string LLNodeApi::GetTypeName(size_t type_index) { + if (object_types.size() <= type_index) { + return ""; + } + return object_types[type_index]->GetTypeName(); +} + +uint32_t LLNodeApi::GetTypeInstanceCount(size_t type_index) { + if (object_types.size() <= type_index) { + return 0; + } + return object_types[type_index]->GetInstanceCount(); +} + +uint32_t LLNodeApi::GetTypeTotalSize(size_t type_index) { + if (object_types.size() <= type_index) { + return 0; + } + return object_types[type_index]->GetTotalInstanceSize(); +} + +std::set* LLNodeApi::GetTypeInstances(size_t type_index) { + if (object_types.size() <= type_index) { + return nullptr; + } + return &(object_types[type_index]->GetInstances()); +} + +std::string LLNodeApi::GetObject(uint64_t address) { + v8::Value v8_value(llscan->v8(), address); + v8::Value::InspectOptions inspect_options; + inspect_options.detailed = true; + inspect_options.length = 16; + + v8::Error err; + std::string result = v8_value.Inspect(&inspect_options, err); + if (err.Fail()) { + return "Failed to get object"; + } + return result; +} +} // namespace llnode diff --git a/src/llnode_api.h b/src/llnode_api.h new file mode 100644 index 00000000..241851fa --- /dev/null +++ b/src/llnode_api.h @@ -0,0 +1,65 @@ +// C++ wrapper API for lldb and llnode APIs + +#ifndef SRC_LLNODE_API_H_ +#define SRC_LLNODE_API_H_ + +#include +#include +#include +#include + +namespace lldb { +class SBDebugger; +class SBTarget; +class SBProcess; + +} // namespace lldb + +namespace llnode { + +class LLScan; +class TypeRecord; + +namespace v8 { +class LLV8; +} + +class LLNodeApi { + public: + // TODO: a status class + + LLNodeApi(); + ~LLNodeApi(); + LLNodeApi(LLNodeApi&&); + LLNodeApi& operator=(LLNodeApi&&); + + bool Init(const char* filename, const char* executable); + std::string GetProcessInfo(); + uint32_t GetProcessID(); + std::string GetProcessState(); + uint32_t GetThreadCount(); + uint32_t GetFrameCount(size_t thread_index); + std::string GetFrame(size_t thread_index, size_t frame_index); + void ScanHeap(); + // Must be called after ScanHeap; + uint32_t GetTypeCount(); + std::string GetTypeName(size_t type_index); + uint32_t GetTypeInstanceCount(size_t type_index); + uint32_t GetTypeTotalSize(size_t type_index); + std::set* GetTypeInstances(size_t type_index); + std::string GetObject(uint64_t address); + + private: + bool loaded; + static bool debugger_initialized_; + std::unique_ptr debugger; + std::unique_ptr target; + std::unique_ptr process; + std::unique_ptr llv8; + std::unique_ptr llscan; + std::vector object_types; +}; + +} // namespace llnode + +#endif // SRC_LLNODE_API_H_ diff --git a/src/llnode_module.cc b/src/llnode_module.cc new file mode 100644 index 00000000..8da2f55a --- /dev/null +++ b/src/llnode_module.cc @@ -0,0 +1,310 @@ +// Javascript module API for llnode/lldb +#include +#include + +#include "src/llnode_api.h" +#include "src/llnode_module.h" + +namespace llnode { + +using ::v8::Array; +using ::v8::Context; +using ::v8::Exception; +using ::v8::Function; +using ::v8::FunctionCallback; +using ::v8::FunctionTemplate; +using ::v8::HandleScope; +using ::v8::Isolate; +using ::v8::Local; +using ::v8::Name; +using ::v8::Number; +using ::v8::Object; +using ::v8::Persistent; +using ::v8::PropertyAttribute; +using ::v8::PropertyCallbackInfo; +using ::v8::String; +using ::v8::Symbol; +using ::v8::Value; +using Nan::New; + +Persistent LLNode::tpl; +Persistent LLNode::constructor; + +template +bool HasInstance(Local context, Local value) { +#if NODE_MODULE_VERSION < 57 + Local tpl = Nan::New(T::tpl); + return tpl->HasInstance(value); +#else + Local ctor = Nan::New(T::constructor); + return value->InstanceOf(context, ctor).ToChecked(); +#endif +} + +NAN_MODULE_INIT(LLNode::Init) { + Isolate* isolate = Isolate::GetCurrent(); + Local local_tpl = Nan::New(New); + tpl.Reset(isolate, local_tpl); + Local llnode_class = Nan::New("LLNode").ToLocalChecked(); + + local_tpl->SetClassName(llnode_class); + local_tpl->InstanceTemplate()->SetInternalFieldCount(1); + + Nan::SetPrototypeMethod(local_tpl, "getProcessInfo", GetProcessInfo); + Nan::SetPrototypeMethod(local_tpl, "getProcessObject", GetProcessObject); + Nan::SetPrototypeMethod(local_tpl, "getHeapTypes", GetHeapTypes); + Nan::SetPrototypeMethod(local_tpl, "getObjectAtAddress", GetObjectAtAddress); + + Local local_ctor = Nan::GetFunction(local_tpl).ToLocalChecked(); + constructor.Reset(isolate, local_ctor); + + Nan::Set(target, Nan::New("fromCoredump").ToLocalChecked(), + Nan::New(FromCoreDump)->GetFunction()); + Nan::Set(target, llnode_class, local_ctor); +} + +LLNode::LLNode() : api(new llnode::LLNodeApi()) {} + +LLNode::~LLNode() {} + +NAN_METHOD(LLNode::New) {} + +NAN_METHOD(LLNode::FromCoreDump) { + if (info.Length() < 2) { + Nan::ThrowTypeError("Wrong number of args"); + return; + } + Nan::Utf8String filename(info[0]); + Nan::Utf8String executable(info[1]); + + LLNode* obj = new LLNode(); + obj->_heap_initialized = false; + bool ret = obj->api->Init(*filename, *executable); + if (!ret) { + Nan::ThrowTypeError("Failed to load coredump"); + return; + } + + Local ctor = Nan::New(constructor); + Local llnode_obj = Nan::NewInstance(ctor).ToLocalChecked(); + obj->Wrap(llnode_obj); + info.GetReturnValue().Set(llnode_obj); +} + +NAN_METHOD(LLNode::GetProcessInfo) { + LLNode* obj = Nan::ObjectWrap::Unwrap(info.Holder()); + std::string process_info = obj->api->GetProcessInfo(); + info.GetReturnValue().Set(Nan::New(process_info).ToLocalChecked()); +} + +NAN_METHOD(LLNode::GetProcessObject) { + LLNode* obj = Nan::ObjectWrap::Unwrap(info.Holder()); + uint32_t pid = obj->api->GetProcessID(); + std::string state = obj->api->GetProcessState(); + uint32_t thread_count = obj->api->GetThreadCount(); + + Local result = Nan::New(); + result->Set(Nan::New("pid").ToLocalChecked(), Nan::New(pid)); + result->Set(Nan::New("state").ToLocalChecked(), + Nan::New(state).ToLocalChecked()); + result->Set(Nan::New("threadCount").ToLocalChecked(), Nan::New(thread_count)); + + Local thread_list = Nan::New(); + for (size_t i = 0; i < thread_count; i++) { + Local thread = Nan::New(); + thread->Set(Nan::New("threadId").ToLocalChecked(), Nan::New(i)); + uint32_t frame_count = obj->api->GetFrameCount(i); + thread->Set(Nan::New("frameCount").ToLocalChecked(), Nan::New(frame_count)); + Local frame_list = Nan::New(); + for (size_t j = 0; j < frame_count; j++) { + Local frame = Nan::New(); + std::string frame_str = obj->api->GetFrame(i, j); + frame->Set(Nan::New("function").ToLocalChecked(), + Nan::New(frame_str).ToLocalChecked()); + frame_list->Set(j, frame); + } + + thread->Set(Nan::New("frames").ToLocalChecked(), frame_list); + thread_list->Set(i, thread); + } + result->Set(Nan::New("threads").ToLocalChecked(), thread_list); + + info.GetReturnValue().Set(result); +} + +NAN_METHOD(LLNode::GetHeapTypes) { + LLNode* obj = Nan::ObjectWrap::Unwrap(info.Holder()); + Local llnode_obj = info.This(); + + // Initialize the heap and the type iterators + if (!obj->_heap_initialized) { + obj->api->ScanHeap(); + obj->_heap_initialized = true; + } + + uint32_t type_count = obj->api->GetTypeCount(); + Local type_list = Nan::New(); + Local ctor = Nan::New(LLNodeHeapType::constructor); + for (size_t i = 0; i < type_count; i++) { + Local argv[2] = {llnode_obj, Nan::New(static_cast(i))}; + + Local type_obj = Nan::NewInstance(ctor, 2, argv).ToLocalChecked(); + type_list->Set(i, type_obj); + } + + info.GetReturnValue().Set(type_list); +} + +Local GetTypeInstanceObject(Isolate* isolate, LLNodeApi* api, + uint64_t addr) { + HandleScope scope(isolate); + + Local result = Nan::New(); + char buf[20]; + snprintf(buf, sizeof(buf), "0x%016" PRIx64, addr); + result->Set(Nan::New("address").ToLocalChecked(), + Nan::New(buf).ToLocalChecked()); + + std::string value = api->GetObject(addr); + result->Set(Nan::New("value").ToLocalChecked(), + Nan::New(value).ToLocalChecked()); + + return result; +} + +// TODO: create JS object to introspect core dump +// process/threads/frames +NAN_METHOD(LLNode::GetObjectAtAddress) { + LLNode* obj = Nan::ObjectWrap::Unwrap(info.Holder()); + Nan::Utf8String address_str(info[0]); + if ((*address_str)[0] != '0' || (*address_str)[1] != 'x' || + address_str.length() > 18) { + Nan::ThrowTypeError("Invalid address"); + return; + } + uint64_t addr = std::strtoull(*address_str, nullptr, 16); + Isolate* isolate = Isolate::GetCurrent(); + Local result = GetTypeInstanceObject(isolate, obj->api.get(), addr); + info.GetReturnValue().Set(result); +} + +Persistent LLNodeHeapType::tpl; +Persistent LLNodeHeapType::constructor; + +Persistent LLNodeHeapType::llnode_symbol; + +NAN_MODULE_INIT(LLNodeHeapType::Init) { + Isolate* isolate = Isolate::GetCurrent(); + + Local local_tpl = Nan::New(New); + tpl.Reset(isolate, local_tpl); + + Local type_class = Nan::New("LLNodeHeapType").ToLocalChecked(); + local_tpl->SetClassName(type_class); + local_tpl->InstanceTemplate()->SetInternalFieldCount(1); + + Local local_ctor = Nan::GetFunction(local_tpl).ToLocalChecked(); + constructor.Reset(isolate, local_ctor); + llnode_symbol.Reset( + isolate, + Symbol::New(isolate, Nan::New("llnode_symbol").ToLocalChecked())); + + Nan::Set(target, Nan::New("nextInstance").ToLocalChecked(), + Nan::New(NextInstance)->GetFunction()); + Nan::Set(target, type_class, local_ctor); +} + +LLNodeHeapType::~LLNodeHeapType() {} +LLNodeHeapType::LLNodeHeapType(LLNode* ptr, size_t index) { + llnode_ptr = ptr; + instances_initialized_ = false; + + type_name = llnode_ptr->api->GetTypeName(index); + type_index = index; + type_ins_count = llnode_ptr->api->GetTypeInstanceCount(index); + type_total_size = llnode_ptr->api->GetTypeTotalSize(index); +} + +NAN_METHOD(LLNodeHeapType::New) { + if (!info.IsConstructCall()) { + int argc = info.Length(); + Local argv[argc]; + for (int i = 0; i < argc; ++i) { + argv[i] = info[i]; + } + + Local ctor = Nan::New(constructor); + info.GetReturnValue().Set( + Nan::NewInstance(ctor, argc, argv).ToLocalChecked()); + return; + } + + Local context = Nan::GetCurrentContext(); + if (info.Length() < 2 || !HasInstance(context, info[0])) { + Nan::ThrowTypeError("Must pass an LLNode instance"); + return; + } + + Local llnode_obj = Nan::To(info[0]).ToLocalChecked(); + LLNode* llnode_ptr = Nan::ObjectWrap::Unwrap(llnode_obj); + uint32_t index = Nan::To(info[1]).FromJust(); + + LLNodeHeapType* obj = new LLNodeHeapType(llnode_ptr, index); + Local instance = info.This(); + + Local typeName = Nan::New("typeName").ToLocalChecked(); + Local instanceCount = Nan::New("instanceCount").ToLocalChecked(); + Local totalSize = Nan::New("totalSize").ToLocalChecked(); + + Local local_sym = Nan::New(llnode_symbol); + instance->Set(typeName, Nan::New(obj->type_name).ToLocalChecked()); + instance->Set(instanceCount, Nan::New(obj->type_ins_count)); + instance->Set(totalSize, Nan::New(obj->type_total_size)); + instance->Set(local_sym, llnode_obj); + + obj->Wrap(instance); + info.GetReturnValue().Set(instance); +} + +void LLNodeHeapType::InitInstances() { + std::set* instances_set = + llnode_ptr->api->GetTypeInstances(type_index); + current_instance_index = 0; + for (const uint64_t& addr : *instances_set) { + type_instances.push_back(addr); + } + type_ins_count = type_instances.size(); + instances_initialized_ = true; +} + +bool LLNodeHeapType::HasMoreInstances() { + return current_instance_index < type_ins_count; +} + +NAN_METHOD(LLNodeHeapType::NextInstance) { + Local context = Nan::GetCurrentContext(); + if (info.Length() < 1 || !HasInstance(context, info[0])) { + Nan::ThrowTypeError("Must pass an LLNodeHeapType instance"); + return; + } + + Local type_obj = Nan::To(info[0]).ToLocalChecked(); + LLNodeHeapType* obj = Nan::ObjectWrap::Unwrap(type_obj); + + if (!obj->instances_initialized_) { + obj->InitInstances(); + } + + if (!obj->HasMoreInstances()) { + info.GetReturnValue().Set(Nan::Undefined()); + return; + } + + uint64_t addr = obj->type_instances[obj->current_instance_index++]; + Isolate* isolate = Isolate::GetCurrent(); + Local result = + GetTypeInstanceObject(isolate, obj->llnode_ptr->api.get(), addr); + info.GetReturnValue().Set(result); +} + +} // namespace llnode diff --git a/src/llnode_module.h b/src/llnode_module.h new file mode 100644 index 00000000..1d5f6743 --- /dev/null +++ b/src/llnode_module.h @@ -0,0 +1,78 @@ +#ifndef SRC_LLNODE_MODULE_H +#define SRC_LLNODE_MODULE_H + +#include +#include + +namespace llnode { + +class LLNodeApi; +class LLNodeHeapType; + +class LLNode : public Nan::ObjectWrap { + friend class LLNodeHeapType; + + public: + static NAN_MODULE_INIT(Init); + + static ::v8::Persistent<::v8::Function> constructor; + static ::v8::Persistent<::v8::FunctionTemplate> tpl; + + protected: + std::unique_ptr api; + + private: + explicit LLNode(); + ~LLNode(); + + static NAN_METHOD(New); + static NAN_METHOD(FromCoreDump); + // static NAN_METHOD(FromPid); + // static NAN_METHOD(LoadDump); + static NAN_METHOD(GetProcessInfo); + static NAN_METHOD(GetProcessObject); + static NAN_METHOD(GetHeapTypes); + static NAN_METHOD(GetObjectAtAddress); + bool _heap_initialized; +}; + +class LLNodeHeapType : public Nan::ObjectWrap { + friend class LLNode; + + public: + static NAN_MODULE_INIT(Init); + explicit LLNodeHeapType(LLNode* llnode_ptr, size_t index); + ::v8::Local<::v8::Object> Instantiate(::v8::Isolate* isolate, + ::v8::Local<::v8::Context> context, + ::v8::Local<::v8::Object> llnode_obj); + + static NAN_METHOD(NextInstance); + + static ::v8::Persistent<::v8::Function> constructor; + static ::v8::Persistent<::v8::FunctionTemplate> tpl; + + private: + ~LLNodeHeapType(); + static NAN_METHOD(New); + + void InitInstances(); + bool HasMoreInstances(); + + // For getting objects + LLNode* llnode_ptr; + + static ::v8::Persistent<::v8::Symbol> llnode_symbol; + + std::vector type_instances; + bool instances_initialized_; + size_t current_instance_index; + + std::string type_name; + size_t type_index; + uint32_t type_ins_count; + uint32_t type_total_size; +}; + +} // namespace llnode + +#endif diff --git a/test/jsapi-test.js b/test/jsapi-test.js new file mode 100644 index 00000000..0a5d4055 --- /dev/null +++ b/test/jsapi-test.js @@ -0,0 +1,154 @@ +'use strict'; + +const fromCoredump = require('../').fromCoredump; + +const debug = process.env.TEST_LLNODE_DEBUG ? + console.log.bind(console) : () => { }; + +const common = require('./common'); +const tape = require('tape'); + +tape('llnode API', (t) => { + t.timeoutAfter(common.saveCoreTimeout); + + // Use prepared core and executable to test + if (process.env.LLNODE_CORE && process.env.LLNODE_NODE_EXE) { + test(process.env.LLNODE_NODE_EXE, process.env.LLNODE_CORE, t); + t.end(); + } else if (process.platform === 'linux') { + t.skip('No `process save-core` on linux'); + t.end(); + } else { + common.saveCore({ + scenario: 'inspect-scenario.js' + }, (err) => { + t.error(err); + t.ok(true, 'Saved core'); + + test(process.execPath, common.core, t); + }); + } +}); + +function test(executable, core, t) { + debug('============= Loading =============='); + // Equivalent to lldb executable -c core + debug(`Loading core dump: ${core}, executable: ${executable}`); + const llnode = fromCoredump(core, executable); + + verifySBProcess(llnode, t); + const typeMap = verifyBasicTypes(llnode, t); + const processType = verifyProcessType(typeMap, llnode, t); + verifyProcessInstances(processType, llnode, t); +} + +function verifySBProcess(llnode, t) { + const processInfo = llnode.getProcessInfo(); + debug('Process info', processInfo); + const procRe = new RegExp( + 'SBProcess: pid = (\\d+), state = (\\w+), ' + + 'threads = (\\d+), executable = .+'); + const procMatch = processInfo.match(procRe); + t.ok(procMatch, 'SBProcess info should be formatted correctly'); + + const procObj = llnode.getProcessObject(); + debug('Process object', procObj); + t.equal(procObj.pid, parseInt(procMatch[1]), 'SBProcess pid'); + t.equal(procObj.state, procMatch[2], 'SBProcess state'); + t.equal(procObj.threadCount, parseInt(procMatch[3]), + 'SBProcess thread count'); + t.ok(procObj.threadCount > 0, + 'SBProcess should have more than one thread'); + t.ok(Array.isArray(procObj.threads), + 'processObject.threads should be an array'); + t.equal(procObj.threads.length, + procObj.threadCount, + 'processObject.threads should contain all the threads'); + + let i = 0; + for (const thread of procObj.threads) { + debug(`Thread ${i}:`, thread); + t.equal(thread.threadId, i++, 'thread.threadId'); + t.ok(Array.isArray(thread.frames), + 'thread.frames should be an array'); + t.equal(thread.frameCount, thread.frames.length, + 'thread.frames should contain all the frames'); + + for (const frame of thread.frames) { + debug(` #`, frame); + t.ok(typeof frame.function === 'string', + 'frame.function should be a string'); + } + } +} + +function verifyBasicTypes(llnode, t) { + debug('============= Heap Types =============='); + const heapTypes = llnode.getHeapTypes(); + // debug('Heap types', heapTypes); + const basicTypes = [ + // basic JS types + '(Array)', '(String)', 'Object', '(Object)', '(ArrayBufferView)', + // Node types + 'process', 'NativeModule', 'console', 'TickObject' + ].sort(); + + const typeMap = new Map(); + for (const item of heapTypes) { + if (basicTypes.indexOf(item.typeName) !== -1) { + typeMap.set(item.typeName, item); + } + } + + const foundTypes = Array.from(typeMap.keys()).sort(); + t.deepEqual(foundTypes, basicTypes, + 'The heap should contain all the basic types'); + + return typeMap; +} + +function verifyProcessType(typeMap, llnode, t) { + const processType = typeMap.get('process'); + + t.equal(processType.typeName, 'process', + 'The typeName of process type should be "process"') + t.ok(processType.instanceCount > 0, + 'There should be more than one process instances'); + t.ok(processType.totalSize > 0, + 'The process objects should have a non-zero size'); + return processType; +} + +function verifyProcessInstances(processType, llnode, t) { + // TODO: should be implemented as an iterator + let foundProcess = false; + + const propRe = [ + /.pid=/, + /.platform=0x[0-9a-z]+:/, + /.arch=0x[0-9a-z]+:/, + /.version=0x[0-9a-z]+:/, + /.versions=0x[0-9a-z]+:/, + /.release=0x[0-9a-z]+:/, + /.execPath=0x[0-9a-z]+:/, + /.execArgv=0x[0-9a-z]+:/, + /.argv=0x[0-9a-z]+:/ + ]; + + const visited = new Map(); + + for (const instance of processType.instances) { + t.ok(!visited.get(instance.address), + 'should not duplicate instances'); + visited.set(instance.address, instance.value); + t.deepEqual( + instance, + llnode.getObjectAtAddress(instance.address), + 'instance should be the same as obtained from address') + + if (propRe.every((re) => re.test(instance.value))) { + foundProcess = true; + } + } + t.ok(foundProcess, 'should find the process object'); +}