├── .npmignore ├── .gitignore ├── AUTHORS.md ├── test ├── test_create_core.js └── test_collect_core.js ├── .eslintrc.js ├── binding.gyp ├── package.json ├── LICENSE.md ├── src ├── gencore.h ├── mac.cc ├── linux.cc └── gencore.cc ├── CONTRIBUTING.md ├── README.md └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !src/** 3 | !binding.gyp 4 | !index.js 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project files 2 | .project 3 | node_modules 4 | build 5 | gencore.node 6 | core_* 7 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Authors ordered by first contribution 2 | 3 | - Howard Hellyer (hhellyer@uk.ibm.com) 4 | - Richard Chamberlain (richard_chamberlain@uk.ibm.com) 5 | -------------------------------------------------------------------------------- /test/test_create_core.js: -------------------------------------------------------------------------------- 1 | const gencore = require('../'); 2 | const fs = require('fs'); 3 | const tap = require('tap'); 4 | 5 | tap.comment('Creating core and collecting libraries.'); 6 | gencore.createCore(checkCore); 7 | 8 | function checkCore(error, filename) { 9 | tap.ok(error === null, 'Error object should be null'); 10 | tap.ok(fs.existsSync(filename), `Found core file ${filename}.`); 11 | } 12 | 13 | // TODO - Test with ulimit of 0? 14 | // Need to set a hard ulimit, might need a wrapper child process so we don't break this shell. 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "indent": [ 9 | "error", 10 | 2 11 | ], 12 | "linebreak-style": [ 13 | "error", 14 | "unix" 15 | ], 16 | "quotes": [ 17 | "error", 18 | "single" 19 | ], 20 | "semi": [ 21 | "error", 22 | "always" 23 | ] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "gencore", 5 | "include_dirs": [ ' 23 | #include 24 | #endif 25 | 26 | namespace gencore { 27 | 28 | NAN_METHOD(FindLibraries); 29 | NAN_METHOD(CheckChild); 30 | NAN_METHOD(ForkCore); 31 | 32 | /* Shared functions (at least on Unix-y platforms.) */ 33 | 34 | // Setting the ulimits is void as even if we fail we are 35 | // still going to try to create a core dump. 36 | void raiseUlimits(); 37 | 38 | // The code to run in the child process. 39 | // void, since this function will never return. 40 | void childFunction(const char* working_dir); 41 | 42 | } // namespace gencore 43 | #endif // SRC_GENCORE_H_ 44 | -------------------------------------------------------------------------------- /test/test_collect_core.js: -------------------------------------------------------------------------------- 1 | const gencore = require('../'); 2 | const fs = require('fs'); 3 | const tap = require('tap'); 4 | const zlib = require('zlib'); 5 | const tar = require('tar'); 6 | const path = require('path'); 7 | 8 | let core_count = 0; 9 | let callback_count = 0; 10 | 11 | tap.comment('Creating core and collecting libraries.'); 12 | gencore.collectCore(checkTarGz); 13 | 14 | function checkTarGz(error, filename) { 15 | callback_count++; 16 | tap.ok(error === null, 'Error argument should be null'); 17 | tap.ok(callback_count == 1, 'Callback should only be called once on success.'); 18 | tap.ok(fs.existsSync(filename), `Collected core and libraries file ${filename} exists.`); 19 | 20 | tap.match(filename, /core_[0-9]{8}.[0-9]{6}\.[0-9]+\.[0-9]{3}\.tar\.gz/, 'Filename pattern should be core_YYYYMMDD.HHMMSS...tar.gz'); 21 | 22 | //Get the list of files in the tar.gz so we can check them in subsequent tests. 23 | const read = fs.createReadStream(filename); 24 | const gunzip = zlib.createGunzip(); 25 | const parse = tar.Parse(); 26 | 27 | // Setup the plumbing for checking the results. 28 | parse.on('entry', checkEntry); 29 | parse.on('end', checkCoreExists); 30 | 31 | // Read the tar.gz file and the callbacks above should do the checks. 32 | read.pipe(gunzip).pipe(parse); 33 | } 34 | 35 | function checkEntry(entry) { 36 | // Trim off the top level directory containing our timestamp. 37 | let name = entry.path.split(path.sep).slice(1).join(path.sep); 38 | let size = entry.size; 39 | let type = entry.type; 40 | 41 | // Check there's a file in the root that has a core-ish name. 42 | // TODO - How do I know how many files there are and when I'm done? 43 | // TODO - This will break on Mac with /cores but that's ok I want to check it works. 44 | if( name.startsWith('core')) { 45 | core_count++; 46 | } 47 | 48 | // Check no file in the core starts with / so we can't overwrite system files on 49 | // extraction. (Unless root extracts in /!) 50 | tap.notOk( name.startsWith('/'), 'Check for relative paths in tar: ' + name); 51 | 52 | // Check any file in the tar has a non-zero size. 53 | if( type == 'File') { 54 | tap.notOk(size == 0, 'File size > 0 bytes: ' + name); 55 | } else if (type == 'Directory') { 56 | tap.notOk( size != 0, 'Directory size == 0 bytes: ' + name); 57 | } else { 58 | tap.fail('Only files and directories in the tar file: ' + name); 59 | } 60 | } 61 | 62 | function checkCoreExists() { 63 | tap.ok(core_count === 1, 'Check we have only one core file, found ' + core_count); 64 | } 65 | -------------------------------------------------------------------------------- /src/mac.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2017 IBM Corp. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | #include "gencore.h" 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | namespace gencore { 28 | 29 | using v8::Array; 30 | using v8::Isolate; 31 | using v8::Local; 32 | using v8::String; 33 | 34 | void childFunction(const char *directory_path) { 35 | // If child: 36 | // Set ulimits, core_dumpfilter, raise(SIGSEGV) 37 | int rc = chdir(directory_path); 38 | if (rc != 0) { 39 | // Error - In child, awkward to handle. Exit without creationg a core? 40 | } 41 | raiseUlimits(); 42 | 43 | // Tell Mac OS to create core dumps in the current directory. 44 | // DISABLED - sysctl setting is system wide and can only be changed 45 | // if running as root. 46 | // const char* corefile = "core.%P"; 47 | // std::cout << "Setting kern.corefile to: " << corefile << "\n"; 48 | // int result = sysctlbyname("kern.corefile", NULL, NULL, (void*)corefile, 49 | // strlen(corefile)); 50 | // if( result != 0 ) { 51 | // std::cout << "Failed to set kern.corefile, result " << result << "\n"; 52 | // } 53 | 54 | // Crash with a signal that should create a core dump. 55 | raise(SIGSEGV); 56 | } 57 | 58 | NAN_METHOD(FindLibraries) { 59 | Isolate *isolate = Isolate::GetCurrent(); 60 | Local library_names = Array::New(isolate); 61 | 62 | int index = 0; 63 | const char *name = _dyld_get_image_name(index); 64 | while (name != nullptr) { 65 | // std::cout << "Found library: " << name << "\n"; 66 | Local libName = String::NewFromUtf8(isolate, name); 67 | library_names->Set(index, libName); 68 | index++; 69 | name = _dyld_get_image_name(index); 70 | } 71 | 72 | info.GetReturnValue().Set(library_names); 73 | } 74 | } // namespace gencore 75 | -------------------------------------------------------------------------------- /src/linux.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2017 IBM Corp. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | #include "gencore.h" 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | namespace gencore { 27 | 28 | using v8::Array; 29 | using v8::Isolate; 30 | using v8::Local; 31 | using v8::String; 32 | 33 | void childFunction(const char* directory_path) { 34 | // If child: 35 | // Set ulimits, core_dumpfilter, raise(SIGSEGV) 36 | int rc = chdir(directory_path); 37 | if (rc != 0) { 38 | // Error - In child, awkward to handle. Exit without creationg a core? 39 | } 40 | raiseUlimits(); 41 | 42 | std::fstream coredump_filter; 43 | coredump_filter.open("/proc/self/coredump_filter", std::ios::out); 44 | if (coredump_filter.is_open()) { 45 | coredump_filter << "0xFF"; 46 | coredump_filter.close(); 47 | } 48 | 49 | // Crash with a signal that should create a core dump. 50 | raise(SIGSEGV); 51 | } 52 | 53 | typedef struct library_record { 54 | char *library_name; 55 | library_record *next; 56 | } library_record_t; 57 | 58 | static int LibraryRecordCallback(struct dl_phdr_info *info, size_t size, 59 | void *data) { 60 | library_record_t **records_ptr = reinterpret_cast(data); 61 | library_record_t *new_record = 62 | reinterpret_cast(malloc(sizeof(library_record_t))); 63 | if (new_record == nullptr) { 64 | return 1; // Abort the iteration. 65 | } 66 | if (info->dlpi_name != nullptr && *info->dlpi_name != '\0') { 67 | size_t name_len = strlen(info->dlpi_name) + 1; 68 | new_record->library_name = reinterpret_cast(malloc(name_len)); 69 | if (new_record->library_name == nullptr) { 70 | return 1; 71 | } 72 | snprintf(new_record->library_name, name_len, "%s", info->dlpi_name); 73 | } else { 74 | new_record->library_name = nullptr; 75 | } 76 | new_record->next = *records_ptr; 77 | *records_ptr = new_record; 78 | return 0; 79 | } 80 | 81 | NAN_METHOD(FindLibraries) { 82 | Isolate *isolate = Isolate::GetCurrent(); 83 | Local library_names = Array::New(isolate); 84 | uint32_t index = 0; 85 | library_record_t *record_ptr = nullptr; 86 | dl_iterate_phdr(LibraryRecordCallback, &record_ptr); 87 | bool have_exe = false; 88 | while (record_ptr != nullptr) { 89 | library_record_t *next = record_ptr->next; 90 | if (record_ptr->library_name != nullptr) { 91 | Local libName = 92 | String::NewFromUtf8(isolate, record_ptr->library_name); 93 | library_names->Set(index, libName); 94 | } else if (!have_exe) { 95 | char buffer[PATH_MAX + 1]; 96 | memset(buffer, 0, sizeof(buffer)); 97 | ssize_t len = readlink("/proc/self/exe", buffer, sizeof(buffer)); 98 | if (len > 0) { 99 | Local exeName = String::NewFromUtf8(isolate, buffer); 100 | library_names->Set(index, exeName); 101 | have_exe = true; 102 | } 103 | } 104 | free(record_ptr->library_name); 105 | free(record_ptr); 106 | record_ptr = next; 107 | index++; 108 | } 109 | 110 | info.GetReturnValue().Set(library_names); 111 | } 112 | } // namespace gencore 113 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to gencore 2 | 3 | We welcome contributions, but request you follow these guidelines. 4 | 5 | - [Raising bug reports and feature requests](#raising-bug-reports-and-feature-requests) 6 | - [Pull requests](#pull-requests) 7 | - [Coding standards](#coding-standards) 8 | - [IBM Contributor License Agreement](#ibm-contributor-license-agreement) 9 | 10 | 11 | ## Raising bug reports and feature requests 12 | 13 | Please raise any bug reports and feature requests on the project's GitHub [issue tracker](https://github.com/runtimetools/gencore/issues). 14 | Be sure to search the list of open and closed issues to see if your problem or request has already been raised. 15 | 16 | A good bug report is one that make it easy for us to understand what you were trying to do and what went wrong. Provide as much context as possible so we can try to recreate the issue. 17 | 18 | ## Pull requests 19 | 20 | In order for us to accept pull requests from a new contributor, the contributor must indicate that they accept and agree to be bound by the terms of the IBM Contributor License Agreement below. Please do this by adding your name to the [AUTHORS file](https://github.com/runtimetools/gencore/blob/master/AUTHORS.md) 21 | in your first pull request. 22 | 23 | If you want to raise a pull-request for a bug fix or a new feature, we recommend that you raise a [GitHub issue](https://github.com/runtimetools/gencore/issues) to discuss it first. 24 | 25 | ### Coding standards 26 | 27 | Please ensure you follow the coding standards used through-out the existing code base. All source code files include the Apache v2.0 license header. 28 | 29 | ### IBM Contributor License Agreement 30 | Version 1.0.1 - January 25th, 2017 31 | 32 | In order for You (as defined below) to make intellectual property Contributions (as defined below) now or in the future to IBM GitHub repositories, You must agree to this Contributor License Agreement ("CLA"). Please read this CLA carefully before accepting its terms. By accepting the CLA, You are agreeing to be bound by its terms. If You submit a Pull Request against an IBM repository on GitHub You must include in the Pull Request a statement of Your acceptance of this CLA. 33 | As used in this CLA: (i) "You" (or "Your") shall mean the entity that is making this Agreement with IBM; (ii)"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is submitted by You to IBM for inclusion in, or documentation of, any of the IBM GitHub repositories; (iii) "Submit" (or "Submitted") means any form of communication sent to IBM (e.g. the content You post in a GitHub Issue or submit as part of a GitHub Pull Request). 34 | 35 | This agreement applies to all Contributions You Submit. 36 | 37 | - You will only Submit Contributions where You have authored 100% of the content. 38 | 39 | - You will only Submit Contributions to which You have the necessary rights. This means that if You are employed You have received the necessary permissions from Your employer to make the Contributions. 40 | 41 | - Whatever content You Contribute will be provided under the Apache v2.0 license. You can read a copy of the Apache v2.0 License at http://www.apache.org/licenses/LICENSE-2.0. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 42 | 43 | - You understand and agree that the Project and Your contributions are public, and that a record of the contribution (including all personal information You submit with it, including Your sign-off) is maintained indefinitely and may be redistributed consistent with the license(s) involved. 44 | 45 | - You understand that the decision to include Your Contribution is entirely that of the Project and this agreement does not guarantee that the Contribution will be included in the Project. 46 | 47 | - You are not expected to provide support for Your Contribution. However you may provide support for free, for a fee or not at all. You provide Your Contribution on an "AS IS" BASIS as stated in the License. 48 | 49 | You will promptly notify the Project if You become aware of any facts or circumstances that would make these commitments inaccurate in any way. To do so, please an issue on the project's GitHub [issue tracker](https://github.com/runtimetools/gencore/issues). 50 | If You think the Project could make use of content which You did not author, please talk to a committer on the Project. If they like Your idea, they will know the process to get it included. 51 | -------------------------------------------------------------------------------- /src/gencore.cc: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2017 IBM Corp. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | #include "gencore.h" 18 | #include 19 | #include 20 | #include 21 | #include 22 | #ifndef _WIN32 23 | #include 24 | #include 25 | #include 26 | #include 27 | #endif 28 | 29 | namespace gencore { 30 | 31 | using v8::Integer; 32 | using v8::Isolate; 33 | using v8::Local; 34 | using v8::Number; 35 | using v8::Object; 36 | using v8::String; 37 | 38 | // Functions that are used by more than one platform. 39 | #ifndef _WIN32 40 | // This function raises the ulimit to it's maximium value. 41 | static void raiseUlimit(int resource) { 42 | struct rlimit rlim; 43 | rlim.rlim_cur = 0; 44 | rlim.rlim_max = RLIM_INFINITY; 45 | int rc = getrlimit(resource, &rlim); 46 | // Guard against RLIM_INFINITY < 0 47 | // (RLIM_INFINITY is positive on my system but that might not always be true.) 48 | if (rc == 0 && 49 | (rlim.rlim_cur < rlim.rlim_max || rlim.rlim_max == RLIM_INFINITY)) { 50 | rlim.rlim_cur = rlim.rlim_max; 51 | rc = setrlimit(resource, &rlim); 52 | } 53 | } 54 | 55 | // Raise the ulimits for core size (obviously) and file size 56 | // which might also block core dumps. 57 | void raiseUlimits() { 58 | raiseUlimit(RLIMIT_CORE); 59 | raiseUlimit(RLIMIT_FSIZE); 60 | } 61 | #endif 62 | 63 | #ifdef _WIN32 64 | // Dummy functions for Windows build 65 | NAN_METHOD(ForkCore) {} 66 | NAN_METHOD(CheckChild) {} 67 | NAN_METHOD(FindLibraries) {} 68 | #else 69 | // This function forks to create a core file and 70 | // returns the directory we *expect* the core 71 | // to be created in. 72 | // (A number of things may intervene to dash our 73 | // expectations. We check later on!) 74 | NAN_METHOD(ForkCore) { 75 | // We only expect to get passed a simple temp dir name. 76 | char directory_path[32]; 77 | 78 | if (info[0]->IsString()) { 79 | // Filename parameter supplied 80 | Nan::Utf8String directory_parameter(info[0]); 81 | if (directory_parameter.length() < 82 | static_cast(sizeof(directory_path))) { 83 | snprintf(directory_path, sizeof(directory_path), "%s", 84 | *directory_parameter); 85 | } else { 86 | Nan::ThrowError("ForkCore: working directory path too long."); 87 | } 88 | } else { 89 | Nan::ThrowError("ForkCore: no working directory specified."); 90 | } 91 | 92 | // Fork 93 | pid_t child_pid = fork(); 94 | if (child_pid == 0) { 95 | childFunction(directory_path); 96 | } else if (child_pid == -1) { 97 | Nan::ThrowError("ForkCore: unable to create child process."); 98 | } else { 99 | // If parent: 100 | // Return the child pid so this process can wait on it. 101 | Isolate *isolate = info.GetIsolate(); 102 | Local result = Object::New(isolate); 103 | Local child_pid_name = String::NewFromUtf8(isolate, "child_pid"); 104 | result->Set(child_pid_name, Number::New(isolate, child_pid)); 105 | info.GetReturnValue().Set(result); 106 | } 107 | } 108 | 109 | // This just returns true if the pid has exitted, 110 | // false if it still running. 111 | NAN_METHOD(CheckChild) { 112 | int wstatus = 0; 113 | 114 | pid_t child_pid = 0; 115 | 116 | if (info[0]->IsNumber()) { 117 | child_pid = Integer::Cast(*info[0])->Value(); 118 | } 119 | // std::cout << "Checking for pid: " << child_pid << "\n"; 120 | 121 | pid_t wait_pid = waitpid(child_pid, &wstatus, WNOHANG); 122 | // wait_pid will be 0 if the child has not exited. 123 | if (wait_pid == child_pid) { 124 | info.GetReturnValue().Set(true); 125 | } else if (wait_pid == -1) { 126 | Nan::ThrowError("CheckChild: child pid not found."); 127 | } else { 128 | info.GetReturnValue().Set(false); 129 | } 130 | } 131 | #endif 132 | 133 | /******************************************************************************* 134 | * Native module initializer function, called when the module is require'd 135 | * 136 | ******************************************************************************/ 137 | void Initialize(v8::Local exports) { 138 | exports->Set(Nan::New("findLibraries").ToLocalChecked(), 139 | Nan::New(FindLibraries)->GetFunction()); 140 | exports->Set(Nan::New("checkChild").ToLocalChecked(), 141 | Nan::New(CheckChild)->GetFunction()); 142 | exports->Set(Nan::New("forkCore").ToLocalChecked(), 143 | Nan::New(ForkCore)->GetFunction()); 144 | } 145 | 146 | NODE_MODULE(gencore, Initialize) 147 | } // namespace gencore 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gencore 2 | 3 | Creates a core dump for the current process without terminating the process or attaching a debugger. 4 | 5 | # Background 6 | 7 | This module is intended for use creating non-destructive core dumps of running processes in environments where they cannot be created via gcore or may be deleted as soon as they are written. 8 | 9 | The usual technique for creating a core dump without terminating a process is to run gcore against the process. gcore invokes gdb and attaches to the process using ptrace. 10 | 11 | gcore is a better interface for creating core dumps if ptrace is available however in many configurations its use is restricted. You can check the availability of ptrace by looking at the value of /proc/sys/kernel/yama/ptrace_scope. For more details see: https://www.kernel.org/doc/Documentation/security/Yama.txt 12 | gcore will also cause your process to pause while it creates the core dump, the pause time will be proportional to the size of your process and how long it takes to write the core file to disk. This module offers more asynchronous behaviour and may be more suitable in cases where this causing your Node.js process to pause is unacceptable. 13 | 14 | # How it works: 15 | 16 | To get around the restrictions on attaching a debugger this module lets the kernel to create a core dump in the standard way for a process crashing via SIGSEGV. To avoid destroying the running process this module forks a child process and that child process raises SIGSEGV. 17 | 18 | Steps (Linux): 19 | - [Main process] Create a temporary working directory for the process to crash in. This is necessary as the name of the core dump created by the kernel may simply be "core". This is indistinguishable from other core dumps that may have been created. 20 | - [Main Process] Call fork() creating the child process and use waitpid() to wait for it to terminate. 21 | - [Child Process] Change to the temporary directory. 22 | - [Child Process] Raise the core dump and file size ulimits to their maximum. 23 | - [Child Process] Write 0xFF to /proc/self/coredump_filter to ensure all the processes memory is included in the dump. 24 | - [Child Process] Call raise(SIGSEGV) to terminate the process and create a core dump. 25 | - [Main Process] Resume as soon as the child process exists. 26 | - [Main Process] List the files in the temporary directory and set the file name found as the return value from createCore() 27 | 28 | The resulting core file can be analysed using standard debugging tools such as lldb or gdb. For Node.js debugging using the llnode plugin ( [npm installer](https://www.npmjs.com/package/llnode) / [github repository](https://github.com/nodejs/llnode) ) will allow you to access JavaScript objects and stacks. 29 | 30 | # API 31 | 32 | Note: This API is asynchronous, after the point at which the state of the process has been saved. (When fork() returns in the parent process.) It is synchronous before that, which isn't long, to prevent the process state changing between the point a core dump and is requested and the point it is created. After that point this API waits asynchronously for the child process to finish and perform any other operations asynchronously. 33 | 34 | - `gencore.createCore(callback)` 35 | 36 | Creates a core dump. The callback signature is (err, filename) where err is set if an error occurred and filename is the core dump that was created. 37 | 38 | - `gencore.collectCore(callback)` 39 | 40 | Creates a core dump and collects that and all the libraries loaded by the process into a tar.gz file. The tar.gz is named "core_" followed by a timestamp, the pid of the Node.js process and a sequence number to ensure multiple files are unique. The callback signature is (err, filename) where err is set if an error occured and filename is the file containing the core and libraries. 41 | All the files in the tar file are under a top level directory with the same name as the tar.gz file but without the .tar.gz extension. The libraries are stored with their paths intact but relative to the top level directory of the tar file. The core dump will be stored under the top level directory of the tar.gz file. 42 | This function is intended to support analysis taking place on a different system to the one that generated the core dump. For example using lldb and llnode on a Mac to analyse a core from your production system. 43 | 44 | *Note:* Core files are large (approximately your processes memory size) so you should ensure the files created by these APIs are deleted when you have finished with them. Repeatedly calling this API will without deleting the files it creates consume a large amount of disk space. 45 | 46 | # Limitations (possible bugs/enhancements): 47 | - Because fork() only copies the calling thread into the process only the calling threads stack will be available from the core dump. This is not a major restriction on Node.js as that is the only thread that will be running JavaScript code. The Node.js callback based design makes thread stacks a slightly less useful than they are on heavily threaded languages like Java. 48 | - On Linux the setting of /proc/sys/kernel/core_pattern allows the file name for the core dump to be anything. It can even divert the core dump data to another process which may not even write the core file to disk. This module makes no attempt to guess the name and simply relies on picking up whatever file is created in the crashing directory. 49 | 50 | 51 | # Other platforms: 52 | 53 | Non Linux platform support is provided to allow applications to be developed and tested without code changes. 54 | Linux and Mac support is implemented - Mac uses a simpler version of fork and abort as there are fewer configuration issues to work around. 55 | Support for each platform should be implemented by adding a .cc file that handles the platform specific code. 56 | 57 | AIX support would be implemented via the gencore API: https://www.ibm.com/support/knowledgecenter/ssw_aix_72/com.ibm.aix.basetrf1/gencore.htm 58 | 59 | Windows support would be implemented via MiniDumpWriteDump: 60 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms680360(v=vs.85).aspx 61 | 62 | Note: The lack of an API for requesting a core dump from the kernel on Linux is what necessitates the existence of this module. If there were an API this module could be rewritten to be far simpler as could gcore. 63 | 64 | 65 | ## License 66 | 67 | [Licensed under the Apache 2.0 License.](LICENSE.md) 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2017 IBM Corp. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ******************************************************************************/ 16 | 17 | // Main module entry point for gencore 18 | 'use strict'; 19 | 20 | const gencore = require('./gencore'); 21 | const fs = require('fs'); 22 | const path = require('path'); 23 | const fstream = require('fstream'); 24 | const exec = require('child_process').exec; 25 | 26 | let seq = 0; 27 | 28 | exports.collectCore = collectCore; 29 | exports.createCore = createCore; 30 | 31 | /* Fork and create the core dump then callback is called 32 | * with the path of the core file created so it can be 33 | * opened on the current machine with a standard debugger 34 | * such as gdb or lldb. 35 | */ 36 | function createCore(callback) { 37 | if (process.platform == 'win32') { 38 | callback(new Error('Function not supported on Windows.')); 39 | return; 40 | } 41 | 42 | // Create a directory for the child process to crash in. 43 | // Use the timestamp to create a (hopefully) unique namespace 44 | // that allows the core to be related back to which process 45 | // crashed and when. 46 | const timestamp = generateTimestamp(); 47 | 48 | // Run synchronously until fork() returns. 49 | const work_dir = `core_${timestamp}`; 50 | fs.mkdirSync(work_dir); 51 | 52 | let result = null; 53 | try { 54 | result = gencore.forkCore(work_dir); 55 | } catch (err) { 56 | setImmediate(callback, err); 57 | return; 58 | } 59 | result.work_dir = work_dir; 60 | setImmediate(waitForCore, result, callback); 61 | } 62 | 63 | function waitForCore(result, callback) { 64 | try { 65 | if(!gencore.checkChild(result.child_pid)) { 66 | // Check again once the file has had a chance to be written. 67 | setTimeout(waitForCore, 10, result, callback); 68 | return; 69 | } 70 | } catch (err) { 71 | setImmediate(callback, err); 72 | return; 73 | } 74 | 75 | const work_dir = result.work_dir; 76 | const pid = result.child_pid; 77 | 78 | let core_name = findCore(work_dir, pid); 79 | if( core_name !== undefined ) { 80 | callback(null, core_name); 81 | } else { 82 | callback(new Error('No core file created.')); 83 | } 84 | } 85 | 86 | /* Fork and create the core dump then collect the libraries 87 | * that were in use at the same time and package the core 88 | * and the libraries in a tar.gz file for analysis on another 89 | * box. 90 | * The uncompressed core is deleted. 91 | * callback is called with the name of the tar.gz file created. 92 | */ 93 | 94 | function collectCore(callback) { 95 | if (process.platform == 'win32') { 96 | callback(new Error('Function not supported on Windows.')); 97 | return; 98 | } 99 | // Up until we fork the child process this funciton needs to 100 | // work synchronously to minimise how much the state of the 101 | // process changes between requesting a core and the core 102 | // being created. 103 | 104 | // Create a directory for the child process to crash in. 105 | // Use the timestamp to create a (hopefully) unique namespace 106 | // that allows the core to be related back to which process 107 | // crashed and when. 108 | const timestamp = generateTimestamp(); 109 | 110 | // Run synchronously until fork() returns. 111 | const work_dir = `core_${timestamp}`; 112 | fs.mkdirSync(work_dir); 113 | 114 | // Gather the library list before we allow async work 115 | // that might change the list to run. 116 | const libraries = gencore.findLibraries(); 117 | 118 | let result = null; 119 | try { 120 | result = gencore.forkCore(work_dir); 121 | } catch (err) { 122 | setImmediate(callback, err); 123 | return; 124 | } 125 | 126 | // Now we can let other things run asyncrhonously! 127 | result.libraries = libraries; 128 | result.work_dir = work_dir; 129 | setImmediate(waitForCoreAndCollect, result, callback); 130 | } 131 | 132 | function waitForCoreAndCollect(result, callback) { 133 | try { 134 | if(!gencore.checkChild(result.child_pid)) { 135 | // Check again once the file has had a chance to be written. 136 | setTimeout(waitForCoreAndCollect, 10, result, callback); 137 | // Return so we don't need to indent the rest of 138 | // this function in an else. 139 | return; 140 | } 141 | } catch (err) { 142 | setImmediate(callback, err); 143 | return; 144 | } 145 | 146 | // Zip up the core file and libraries. 147 | // The core file should have been created or 148 | // copied to the work_dir. 149 | const work_dir = result.work_dir; 150 | const pid = result.child_pid; 151 | let file_count = result.libraries.length; 152 | 153 | // We will only return the last error if multiple files failed to copy. 154 | let copy_err = null; 155 | 156 | // Declare triggerZip here to give shared access 157 | // to lib_count. 158 | function triggerZip(error) { 159 | // No need for locking to update libCount, 160 | // only happens on the main node loop. 161 | file_count--; 162 | if( error ) { 163 | copy_err = error; 164 | } 165 | if( file_count == 0 ) { 166 | if( copy_err == null ) { 167 | tarGzDir(work_dir, result, callback); 168 | } else { 169 | callback(copy_err); 170 | } 171 | } 172 | } 173 | 174 | let core_name = findCore(work_dir, pid); 175 | if( core_name === undefined ) { 176 | callback(new Error('Unable to locate core file')); 177 | return; 178 | } else if (!path.dirname(core_name).endsWith(work_dir)) { 179 | // Mac OS X puts cores in /cores/core. by default so 180 | // we have an extra file to copy into our tar.gz. 181 | file_count++; 182 | copyFile(core_name, `${work_dir}/core.${result.child_pid}`, 183 | triggerZip); 184 | } 185 | 186 | for( let library of result.libraries ) { 187 | let dest; 188 | // Make all the paths relative to make it less likely to overwrite 189 | // system libraries when the tar is extracted. 190 | if( library.startsWith('/') ) { 191 | dest = work_dir + '/' + library.substr(1); 192 | } else { 193 | dest = work_dir + '/'; 194 | } 195 | 196 | // console.log(library + " -> " + dest); 197 | let library_writer = fstream.Writer(dest); 198 | let library_reader = fstream.Reader({ path: library, follow: true }); 199 | // Now copy the data, don't let failed copies 200 | // stop us creating the zip. 201 | library_reader.pipe(library_writer) 202 | .on('close', (e) => triggerZip(e)) 203 | .on('error', (e) => triggerZip(e)); 204 | } 205 | } 206 | 207 | function copyFile(source, dest, closeCb) { 208 | const read = fs.createReadStream(source); 209 | const write = fs.createWriteStream(dest); 210 | 211 | function error_func(e) { 212 | read.close(); 213 | write.close(); 214 | closeCb(e); 215 | } 216 | 217 | read.on('error', error_func); 218 | write.on('error', error_func); 219 | 220 | read.pipe(write).on('close', closeCb); 221 | } 222 | 223 | function tarGzDir(work_dir, result, callback) { 224 | let tar_file = `${work_dir}.tar.gz`; 225 | 226 | // Use ls to obtain a list of files in work dir so the 227 | // resulting paths don't start with "./" 228 | exec(`tar -czf ${tar_file} ${work_dir}`, 229 | (error, stdout, stderr) => { 230 | exec(`rm -r ${work_dir}`); 231 | callback(error, tar_file); 232 | } 233 | ); 234 | } 235 | 236 | function findCore(work_dir, pid) { 237 | let possible_names = []; 238 | if( process.platform == 'darwin') { 239 | // Mac OS X puts cores in /cores/core. by default so 240 | // we have an extra file to copy into our zip. 241 | // TODO - To be totally correct we should use: 242 | // sysctlbyname("kern.corefile", in C to find the exact 243 | // location. 244 | possible_names.push(path.resolve(`${work_dir}/core.${pid}`)); 245 | possible_names.push(path.resolve(`/cores/core.${pid}`)); 246 | } else if (process.platform == 'linux') { 247 | // Check the likely locations for a linux core dump. 248 | possible_names.push(path.resolve(`${work_dir}/core.${pid}`)); 249 | possible_names.push(path.resolve(`${work_dir}/core`)); 250 | } 251 | for(let name of possible_names) { 252 | if( fs.existsSync(name) ) { 253 | return name; 254 | } 255 | } 256 | return undefined; 257 | } 258 | 259 | function generateTimestamp() { 260 | 261 | const now = new Date(); 262 | function pad(n, len) { 263 | if( len === undefined ) { 264 | len = 2; 265 | } 266 | let str = `${n}`; 267 | while(str.length < len) { 268 | str = '0' + str; 269 | } 270 | return str; 271 | } 272 | 273 | // Create a time stamp that include the process id and a sequence number 274 | // to make the core identifiable and unique. 275 | const timestamp = `${pad(now.getFullYear())}${pad(now.getMonth()+1)}` + 276 | `${pad(now.getDate())}.${pad(now.getHours())}${pad(now.getMinutes())}` + 277 | `${pad(now.getSeconds())}.${process.pid}.${pad(++seq,3)}`; 278 | 279 | return timestamp; 280 | } 281 | --------------------------------------------------------------------------------