Skip to content

Commit

Permalink
postmortem host and lldb plugin prototypes
Browse files Browse the repository at this point in the history
mmarchini committed Feb 12, 2019
1 parent 15f7ff8 commit 800922f
Showing 12 changed files with 761 additions and 7 deletions.
3 changes: 3 additions & 0 deletions common.gypi
Original file line number Diff line number Diff line change
@@ -56,6 +56,9 @@
# Enable disassembler for `--print-code` v8 options
'v8_enable_disassembler': 1,

# Enable disassembler for `--print-code` v8 options
'v8_object_print': 1,

# Don't bake anything extra into the snapshot.
'v8_use_external_startup_data': 0,

26 changes: 26 additions & 0 deletions deps/v8/gypfiles/v8.gyp
Original file line number Diff line number Diff line change
@@ -2720,6 +2720,32 @@
"../src/torque/torque.cc",
],
}, # torque
{
'target_name': 'v8_postmortem_debugger',
'type': 'static_library',
'include_dirs': [
'..',
'../include/',
# This is for `gen/torque-generated`
'<(SHARED_INTERMEDIATE_DIR)',
],
'conditions': [
['v8_enable_i18n_support==1', {
'dependencies': [
'<(icu_gyp_path):icui18n',
'<(icu_gyp_path):icuuc',
],
}],
],
'sources': [
"../include/v8-postmortem-debugger.h",
"../src/debug/v8-postmortem-debugger.cc",
],
'dependencies': [
'v8_init',
'v8_nosnapshot',
],
},
{
'target_name': 'postmortem-metadata',
'type': 'none',
8 changes: 6 additions & 2 deletions deps/v8/include/v8-postmortem-debugger.h
Original file line number Diff line number Diff line change
@@ -14,6 +14,8 @@
#define V8_INCLUDE_V8_POSTMORTEM_DEBUGGER_H_

#include <stdint.h>
#include <fstream>
#include <iostream>

#include "v8config.h" // NOLINT(build/include)

@@ -47,14 +49,16 @@ typedef StaticAccessResult (*StaticAccessFunction)(const char* name,
// Prints details about an object. The object should be a tagged pointer.
V8_EXPORT void V8PostmortemPrintObject(void* object, RegisterAccessFunction r,
ThreadLocalAccessFunction t,
StaticAccessFunction s);
StaticAccessFunction s,
std::ostream& output=std::cout);

// Prints the current JS call stack.
V8_EXPORT void V8PostmortemPrintStackTrace(uintptr_t stack_pointer,
uintptr_t program_counter,
RegisterAccessFunction r,
ThreadLocalAccessFunction t,
StaticAccessFunction s);
StaticAccessFunction s,
FILE* output=stdout);
}

#endif
14 changes: 9 additions & 5 deletions deps/v8/src/debug/v8-postmortem-debugger.cc
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stddef.h>

#include "include/v8-postmortem-debugger.h"
#include "src/assembler-arch.h"
#include "src/elements.h"
@@ -107,18 +109,20 @@ extern "C" {

V8_EXPORT void V8PostmortemPrintObject(void* object, RegisterAccessFunction r,
ThreadLocalAccessFunction t,
StaticAccessFunction s) {
StaticAccessFunction s,
std::ostream& output) {
v8::internal::PostmortemDebuggerStatics statics(t, s);
if (!statics.SetStatics()) return;
v8::internal::Object(reinterpret_cast<v8::internal::Address>(object))
->Print();
->Print(output);
}

V8_EXPORT void V8PostmortemPrintStackTrace(uintptr_t stack_pointer,
uintptr_t program_counter,
RegisterAccessFunction r,
ThreadLocalAccessFunction t,
StaticAccessFunction s) {
StaticAccessFunction s,
FILE* output) {
v8::internal::PostmortemDebuggerStatics statics(t, s);
if (!statics.SetStatics()) return;

@@ -145,10 +149,10 @@ V8_EXPORT void V8PostmortemPrintStackTrace(uintptr_t stack_pointer,
state.fp = r(v8::internal::JavaScriptFrame::fp_register().code());
state.pc_address = &program_counter;

isolate->PrintStack(stdout, v8::internal::Isolate::kPrintStackVerbose,
isolate->PrintStack(output, v8::internal::Isolate::kPrintStackVerbose,
&state);
} else {
isolate->PrintStack(stdout);
isolate->PrintStack(output);
}
}

86 changes: 86 additions & 0 deletions lldb_v8/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/python

import lldb
import argparse
from functools import wraps

from .helpers import validate_hex
from .postmortem_host import PostmortemHost


def v8_load(debugger, args, result, internal_dict):
debugger.SetAsync(True)

postmortem_host = PostmortemHost(debugger)
if postmortem_host.is_valid:
# TODO support multiple targets
internal_dict["v8_postmortem_host"] = postmortem_host

return True


def v8_postmortem_host(func):
@wraps(func)
def v8_postmortem_host_wrapper(debugger, args, result, internal_dict):
if not "v8_postmortem_host" in internal_dict:
if not v8_load(debugger, args, result, internal_dict):
return False
postmortem_host = internal_dict["v8_postmortem_host"]
return func(debugger, args, result, internal_dict, postmortem_host)
return v8_postmortem_host_wrapper


@v8_postmortem_host
def v8_stack(debugger, args, result, internal_dict, postmortem_host):
core_target = debugger.GetSelectedTarget()
core_process = core_target.process
thread = core_process.selected_thread
print thread.frame[0]
frame = thread.frame[0]

stack_pointer = frame.FindRegister("rsp").unsigned
program_counter = frame.FindRegister("rip").unsigned
postmortem_host.send("s %d %d" % (stack_pointer, program_counter))

stack = postmortem_host.listen()
if stack:
print stack
return True
return False


@v8_postmortem_host
def v8_print(debugger, args, result, internal_dict, postmortem_host):
postmortem_host.send("p %x" % args.address)

obj = postmortem_host.listen()
if obj:
print obj
return True
return False



def v8_handler(debugger, command, result, internal_dict):
v8_parser = argparse.ArgumentParser(prog='v8', description='v8-related commands')
subparsers = v8_parser.add_subparsers()

load_parser = subparsers.add_parser('load')
load_parser.set_defaults(func=v8_load)

stack_parser = subparsers.add_parser('stack')
stack_parser.set_defaults(func=v8_stack)

print_parser = subparsers.add_parser('print')
print_parser.add_argument("address", type=validate_hex)
print_parser.set_defaults(func=v8_print)

args = v8_parser.parse_args(command.split(" "))
args.func(debugger, args, result, internal_dict)

return True


# And the initialization code to add your commands
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand('command script add -f lldb_v8.v8_handler v8')
30 changes: 30 additions & 0 deletions lldb_v8/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import re
import argparse


# TODO(mmarchini) use Python logger?
def logger(*args):
args = map(str, args)
print "[lldb_v8] ", " ".join(args)


def int_to_buffer(value):
value = hex(value)[2:]

# Fill zero in the left in hex has odd length
value = value.rjust((len(value)/2 + 1) * 2, '0')

# Split hex value into bytes (two hex digits)
value_bytes = list(map(''.join, zip(*[iter(value)]*2)))

# Convert bytes into chars and return

return "".join([chr(int(v, 16)) for v in value_bytes])


HEX_RE = re.compile(r"^(?:0x){0,1}[0-9a-f]+")
def validate_hex(val):
match = HEX_RE.match(val)
if not match:
raise argparse.ArgumentTypeError
return int(val, 16)
287 changes: 287 additions & 0 deletions lldb_v8/postmortem_host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import os
from time import sleep
import tempfile
from os import path
from functools import wraps
from collections import OrderedDict

import lldb

from .helpers import int_to_buffer, logger
from .tls import TLSAccessor

def ipc_call(ipc_method, ipc_args):
def ipc_call_decorator(func):
@wraps(func)
def ipc_call_wrapper(*args):
return func(args[0], *[ipc_args[i](args[i + 1]) for i in range(len(ipc_args))])
ipc_call_wrapper._ipc_method = ipc_method
return ipc_call_wrapper
return ipc_call_decorator


class MemoryContent(object):

def __init__(self):
self.content = bytes()
self.addr = 0
self.len = 0


class PostmortemHost(object):
def __init__(self, debugger):
self.debugger = debugger
self.is_valid = False
self.create_ipc_files()
if self.create_host_process():
self.is_valid = self.load_core_memory_on_host_process()

@property
def ipc_methods(self):
ipc_methods = {}
for name in dir(self):
if name == "ipc_methods":
continue
attr = getattr(self, name, None)
if hasattr(attr, "_ipc_method"):
ipc_methods[attr._ipc_method] = attr
return ipc_methods

def create_host_process(self):
# TODO add some checks
self.core_target = self.debugger.GetSelectedTarget()
self.core_process = self.core_target.process

self.host_target = self.debugger.CreateTarget(self.core_target.executable.fullpath)

error = lldb.SBError()
launch_parameters = OrderedDict([
("listener", self.debugger.GetListener()),
("argv",
["--experimental-postmortem-host", self.stdin.name, self.stdout.name]),
("envp", None),
("stdin_path", "/dev/null"),
("stdout_path", "/dev/null"),
("stderr_path", "/tmp/stderr"),
("working_directory", None),
("launch_flags", 0),
("stop_at_entry", False),
("error", error),
])
self.host_process = self.host_target.Launch(*(launch_parameters.values()))

if error.Fail() or not self.host_process.IsValid():
logger(error)
return False

retries = 5
while not self.host_process.is_running:
if retries == 0:
logger("Host process is not running")
return False
retries -= 1
sleep(1)

return True

def should_load_memory(self, memory_info):
# TODO (mmarchini) also copy executable sections
if not (memory_info.IsReadable() or memory_info.IsExecutable()):
return False

start = memory_info.GetRegionBase()
end = memory_info.GetRegionEnd()

return True

def get_memory_content(self, memory_info):
error = lldb.SBError()

memory_content = MemoryContent()

memory_content.addr = memory_info.GetRegionBase()
memory_content.len = memory_info.GetRegionEnd() - memory_content.addr

process = self.core_process
read_addr = memory_content.addr

process_stopped = False
if memory_info.IsExecutable():
for section in sum([m.sections for m in self.core_target.modules], []):
sec_start = section.GetLoadAddress(self.core_target)
sec_end = sec_start + section.size

if (sec_start <= read_addr and read_addr <= sec_end):
self.host_process.Stop()
process_stopped = True
offset = read_addr - sec_start
read_addr = section.GetLoadAddress(self.host_target) + offset
process = self.host_process
break


memory_content.content = bytes(process.ReadMemory(read_addr, memory_content.len, error))
if process_stopped:
self.host_process.Continue()
retries = 5
while not self.host_process.is_running:
if retries == 0:
logger("Host process is not running")
raise Exception
retries -= 1
sleep(1)

if error.Fail():
logger(error)
return None
if len(memory_content.content) != memory_content.len:
logger("Wrong read size")
return None

return memory_content

def load_core_memory_on_host_process(self):
error = lldb.SBError()
memory_ranges = self.core_process.GetMemoryRegions()
memory_info = lldb.SBMemoryRegionInfo()

regions = 0
failures = 0

for i in range(memory_ranges.GetSize()):
if not memory_ranges.GetMemoryRegionAtIndex(i, memory_info):
logger("Range unavailable: ", i)

# TODO (mmarchini) also copy executable sections
if not self.should_load_memory(memory_info):
continue

regions += 1

memory_content = self.get_memory_content(memory_info)
if not memory_content:
continue

filename = path.join(self._tmpdir, "%x" % memory_content.addr)

# TODO store file path in context
with open(filename, "wb+") as f:
f.write(memory_content.content)
f.flush()
os.chmod(filename, 0744)

message = "%x %d %s" % (memory_content.addr, memory_content.len, filename)
self.send(message)

ret = int(self.receive())
if ret != memory_content.len:
failures += 1
logger("Couldn't write region [0x%x, 0x%x)" % (memory_content.addr, memory_content.addr + memory_content.len))
logger("Wrong size written: %s != %s" % (memory_content.len, ret))
os.remove(filename)

self.send("done")

self.debugger.SetSelectedTarget(self.core_target)

print "%d Memory Regions loaded (%d Failed)" % (regions, failures)
return True

def create_ipc_files(self):
self._tmpdir = tempfile.mkdtemp()

self.stdin = open(path.join(self._tmpdir, "stdin"), "wb+")

with open(path.join(self._tmpdir, "stdout"), "w+") as f:
self.stdout = open(f.name, "r")
self.current_line = 0
self._stdout_buffer = []

def send(self, msg):
self.stdin.write(msg)
self.stdin.write("\n")
self.stdin.flush()

def receive(self):
if self._stdout_buffer:
return self._stdout_buffer.pop(0)

while True:
if not self.host_process.is_running:
return None

self.stdout.seek(self.current_line)
lines = self.stdout.readlines()
if not (lines and lines[-1].endswith("\n")):
continue

self.current_line = self.stdout.tell()
self._stdout_buffer = [line.rstrip('\n') for line in lines]
return self.receive()

@ipc_call("GetRegister", [str])
def get_register(register, frame):
top_frame = self.core_process.selected_thread.frame[0]
reg_value = top_frame.FindRegister(register).unsigned
return "0x%x" % reg_value

@ipc_call("GetStaticData", [int, str])
def get_static_data(self, byte_count, name):
value = "00" * byte_count
for m in self.core_target.module_iter():
symbol = m.FindSymbol(name)
if symbol:
if symbol.end_addr.offset - symbol.addr.offset != byte_count:
logger("We're in a hard pickle, yes we are")
continue
error = lldb.SBError()
maybe_value = self.core_target.ReadMemory(symbol.addr, byte_count, error)
if error.Fail():
logger("Oopsie doopsie!")
continue
value = "".join([("%x" % ord(c)).zfill(2) for c in maybe_value])
break
return value

@ipc_call("GetTlsData", [int])
def get_tls_data(self, key):
return "%x" % TLSAccessor(self.debugger).getspecific(key)

def listen(self):
reading_buffer = False
buf = []
while True:
request = self.receive()

if not self.host_process.is_running:
return False

if not request:
continue

if request == "end":
break

if request == "-- start buffer --":
reading_buffer = True
continue

if request == "-- end buffer --":
return "\n".join(buf)

if reading_buffer:
buf.append(request)
continue

if request.startswith("return"):
return request.split(" ", 1)[1]

ipc_method = self.ipc_methods.get(request.split(" ")[0], None)
if ipc_method:
self.send(ipc_method(*request.split(" ")[1:]))

return True

# TODO cleanup everything (files, target, process, etc.)
def __delete__(self):
pass
59 changes: 59 additions & 0 deletions lldb_v8/tls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import re
import lldb

TLS_DATA_RE = re.compile("data = (0x[0-9a-fA-F]+)")

class TLSAccessor(object):
def __init__(self, debugger):
self.debugger = debugger


def getspecific(self, key):
"""
__pthread_getspecific (glibc 2.27):
void *
__pthread_getspecific (pthread_key_t key)
{
struct __pthread *self;
if (key < 0 || key >= __pthread_key_count
|| __pthread_key_destructors[key] == PTHREAD_KEY_INVALID)
return NULL;
self = _pthread_self ();
if (key >= self->thread_specifics_size)
return 0;
return self->thread_specifics[key];
}
"""

interpreter = self.debugger.GetCommandInterpreter()
# https://stackoverflow.com/a/10859835/2956796
command_line = "p ((struct pthread*)0x%x)->specific[%d/32][%d%%32]" % (self._pthread_self, key, key)
result = lldb.SBCommandReturnObject()
result_status = interpreter.HandleCommand(command_line, result)

if not result.Succeeded():
print "Couldn't read TLS data for key %d" % key
return 0
search_result = TLS_DATA_RE.search(result.GetOutput())
if not search_result:
print "Couldn't parse TLS data for key %d" % key
return 0

return int(search_result.group(1), 16)

@property
def _pthread_self(self):
"""
__thread struct __pthread *___pthread_self;
"""
# 0x7ffff6e7a03f <+15>: movq %fs:0x10, %rdx

target = self.debugger.GetSelectedTarget()
process = target.process
top_frame = process.selected_thread.frame[0]

return top_frame.FindRegister("fs_base").unsigned
5 changes: 5 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@
'node_enable_v8_vtunejit%': 'false',
'node_core_target_name%': 'node',
'node_lib_target_name%': 'node_lib',
'symbol_level': 2,
'node_intermediate_lib_type%': 'static_library',
'library_files': [
'lib/internal/per_context.js',
@@ -252,6 +253,9 @@
'src',
'deps/v8/include',
],
'dependencies': [
'deps/v8/gypfiles/v8.gyp:v8_postmortem_debugger',
],

# - "C4244: conversion from 'type1' to 'type2', possible loss of data"
# Ususaly safe. Disable for `dep`, enable for `src`
@@ -372,6 +376,7 @@
'src/js_stream.cc',
'src/module_wrap.cc',
'src/node.cc',
'src/node_postmortem_host.cc',
'src/node_api.cc',
'src/node_binding.cc',
'src/node_buffer.cc',
6 changes: 6 additions & 0 deletions src/node_main.cc
Original file line number Diff line number Diff line change
@@ -20,7 +20,9 @@
// USE OR OTHER DEALINGS IN THE SOFTWARE.

#include "node.h"
#include "node_postmortem_host.h"
#include <stdio.h>
#include <getopt.h>

#ifdef _WIN32
#include <windows.h>
@@ -94,6 +96,10 @@ extern bool linux_at_secure;
} // namespace node

int main(int argc, char* argv[]) {
if (std::string(argv[1]) == "--experimental-postmortem-host") {
return PostmortemHost(argc, argv);
}

#if defined(__POSIX__) && defined(NODE_SHARED_MODE)
// In node::PlatformInit(), we squash all signal handlers for non-shared lib
// build. In order to run test cases against shared lib build, we also need
242 changes: 242 additions & 0 deletions src/node_postmortem_host.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
#include <string>
#include <sstream>
#include <iostream>
#include <fstream> // std::fstream
#include <stdlib.h> /* rand */
#include <unistd.h>
#include <cstring>
#include <cstdio>

#include <fcntl.h>
#include <sys/mman.h>

#include <v8-postmortem-debugger.h>

#define LOGGER(M) std::cerr << "[postmortem host] " << M << std::endl

static std::ifstream input;
static std::ofstream output;

// TODO (mmarchini): V8 should provide a way to determine the register name
#define GENERAL_REGISTERS(V) \
V(rax) \
V(rcx) \
V(rdx) \
V(rbx) \
V(rsp) \
V(rbp) \
V(rsi) \
V(rdi) \
V(r8) \
V(r9) \
V(r10) \
V(r11) \
V(r12) \
V(r13) \
V(r14) \
V(r15)

enum RegisterCode {
#define REGISTER_CODE(R) kRegCode_##R,
GENERAL_REGISTERS(REGISTER_CODE)
#undef REGISTER_CODE
kRegAfterLast
};

void LoadMemoryAddresses() {
std::string buffer;

while (true) {
getline(input, buffer);
if (input.eof()) {
input.clear();
input.sync();
if (buffer.empty()) continue;
}

std::cerr << buffer << std::endl;
if (buffer == "done") break;

char fromBuffer[buffer.length() + 1] = { 0 };
buffer.copy(fromBuffer, buffer.length());
unsigned long long addr = std::stoul(strtok(fromBuffer, " "), nullptr, 16);
size_t len = std::stoul(strtok(nullptr, " "));
char* filePath = strtok(nullptr, " ");

// TODO (mmarchini) change mmap with something cross-platform
std::cerr << filePath << std::endl;
int fd = open(filePath, O_RDWR);

if (fd == -1) {
std::cerr << "error while opening file '" << filePath << "': " << strerror(errno) << std::endl;
output << "-1" << std::endl;
continue;
}

void* result = mmap(reinterpret_cast<void*>(addr), static_cast<size_t>(len),
PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_SHARED, fd, 0);

if (result == MAP_FAILED) {
std::cerr << "map failed " << strerror(errno) << std::endl;
output << "-1" << std::endl;
} else if (result != reinterpret_cast<void*>(addr)) {
std::cerr << "allocated in incorrect address 0x" << std::hex << result << std::dec << std::endl;
output << "-1" << std::endl;
} else {
output << len << std::endl;
}

// TODO(mmarchini): Check if we can close the fd when a file is mapped to memory
close(fd);
}
}


uintptr_t GetRegister(int index) {
switch (index) {
#define REGISTER_CODE(R) \
case kRegCode_##R: \
output << "GetRegister " << #R << std::endl; \
break;
GENERAL_REGISTERS(REGISTER_CODE)
#undef REGISTER_CODE
default:
std::cerr << "Couldn't determine register for index " << index << std::endl;
return 0;
}

std::string buffer;
while (true) {
getline(input, buffer);
if (input.eof()) {
input.clear();
input.sync();
if (buffer.empty()) continue;
}
return std::stoul(buffer, nullptr, 16);
}
}


void* GetTlsData(int32_t key) {
output << "GetTlsData " << key << std::endl;
// LOGGER("Loading tls data");

std::string buffer;
while (true) {
getline(input, buffer);
if (input.eof()) {
input.clear();
input.sync();
if (buffer.empty()) continue;
}

// LOGGER("here ya go");
// LOGGER(buffer);

unsigned long long value = std::stoul(buffer, nullptr, 16);

// LOGGER("result: " << std::hex << value << std::dec);
return reinterpret_cast<void*>(value);
}
}

StaticAccessResult GetStaticData(const char* name, uint8_t* destination,
size_t byte_count) {
output << "GetStaticData " << byte_count << " " << name << std::endl;

std::string buffer;
while (true) {
getline(input, buffer);
if (input.eof()) {
input.clear();
input.sync();
if (buffer.empty()) continue;
}

if (buffer.length() != byte_count * 2) {
return StaticAccessResult::kGenericError;
}

const char* c_buffer = buffer.c_str();

for (unsigned int i = 0; i < byte_count; i++) {
char a[3] = { c_buffer[(i * 2)], c_buffer[(i * 2) + 1], '\0' };
destination[i] = static_cast<uint8_t>(std::stoul(a, nullptr, 16));
}

return StaticAccessResult::kOk;
}
return StaticAccessResult::kOk;
}


int PostmortemHost(int argc, char* argv[]) {
input = std::ifstream(argv[2]);
output = std::ofstream(argv[3]);

std::cerr << argv[2] << std::endl;
std::cerr << argv[3] << std::endl;

std::string buffer;

LoadMemoryAddresses();

while (true) {
getline(input, buffer);
if (input.eof()) {
input.clear();
input.sync();
if (buffer.empty()) continue;
}

switch (buffer.c_str()[0]) {
case 's': {
std::FILE* tmp = std::tmpfile();
char fromBuffer[buffer.length() + 1] = { 0 };
buffer.copy(fromBuffer, buffer.length());
strtok(fromBuffer, " ");
uintptr_t stack_pointer = std::stoul(strtok(nullptr, " "));
uintptr_t program_counter = std::stoul(strtok(nullptr, " "));
V8PostmortemPrintStackTrace(stack_pointer, program_counter,
&GetRegister, &GetTlsData, &GetStaticData,
tmp);
output << "-- start buffer --" << std::endl;
{
size_t len = 0;
ssize_t read;
char* line = nullptr;
std::fseek(tmp, 0, SEEK_SET);

while ((read = getline(&line, &len, tmp)) != -1) {
output << line << std::endl;
}
}
output << "-- end buffer --" << std::endl;
break;
}
case 'p': {
std::ostringstream tmp;
char fromBuffer[buffer.length() + 1] = { 0 };
buffer.copy(fromBuffer, buffer.length());
strtok(fromBuffer, " ");
uintptr_t obj = std::stoul(strtok(nullptr, " "), nullptr, 16);

V8PostmortemPrintObject(reinterpret_cast<void*>(obj), &GetRegister,
&GetTlsData, &GetStaticData, tmp);

output << "-- start buffer --" << std::endl;
{
output << tmp.str() << std::endl;
}
output << "-- end buffer --" << std::endl;
break;
}
default:
std::cerr << "Invalid option '" << buffer.c_str()[0] << "'" << std::endl;
break;
}
}

return 0;
}
2 changes: 2 additions & 0 deletions src/node_postmortem_host.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

int PostmortemHost(int argc, char* argv[]);

0 comments on commit 800922f

Please sign in to comment.