1
1
mirror of https://github.com/tstack/lnav.git synced 2024-10-04 00:31:31 +03:00

[crashd] support uploading crash logs

This commit is contained in:
Tim Stack 2024-05-27 06:25:53 -07:00
parent fc79d703b1
commit d4cdbc1f49
14 changed files with 586 additions and 51 deletions

19
crashd/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
# syntax=docker/dockerfile:1
ARG PYTHON_VERSION=3.12.3
FROM python:${PYTHON_VERSION}-slim
LABEL fly_launch_runtime="flask"
WORKDIR /code
RUN apt-get update && apt-get install -y --no-install-recommends gcc build-essential
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
EXPOSE 8080
CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=8080"]

1
crashd/Procfile Normal file
View File

@ -0,0 +1 @@
web: gunicorn app:app

148
crashd/app.py Normal file
View File

@ -0,0 +1,148 @@
from werkzeug.middleware.proxy_fix import ProxyFix
from flask import Flask, request, send_file, after_this_request
import os
import glob
import uuid
from datetime import datetime
import zipfile
import spookyhash
app = Flask(__name__)
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=2, x_proto=2, x_host=2, x_prefix=2
)
ROOT_PAGE = """
<html>
<head>
<title>lnav crash upload site</title>
</head>
<body>
You can help improve <b>lnav</b> by uploading your crash logs by running:
<pre>
lnav -m crash upload
</pre>
</body>
</html>
"""
# Function to check free space on filesystem
def check_free_space():
statvfs = os.statvfs('/logs')
# Calculate free space in MB
free_space_mb = (statvfs.f_bsize * statvfs.f_bavail) / (1024 * 1024)
return free_space_mb
@app.route('/crash', methods=['POST'])
def crash_handler():
# Check free space on filesystem
free_space_mb = check_free_space()
if free_space_mb < 100:
return 'Insufficient free space on the filesystem!\n', 500
# Retrieve the secret value from the environment variable
lnav_secret_env = os.environ.get('LNAV_SECRET')
# Check if header 'lnav-secret' exists and has the correct value
if request.headers.get('lnav-secret') == lnav_secret_env:
# Check if content length is provided
if 'Content-Length' not in request.headers:
return 'Content length header is missing!', 400
# Get the content length
content_length = int(request.headers['Content-Length'])
# Check if content length is zero
if content_length == 0:
return 'Empty request body!', 400
# Check if content length exceeds 10MB
if content_length > 10 * 1024 * 1024: # 10MB limit
return 'Content size exceeds the limit of 10MB!', 413
# Get the content from the request body
content = request.data
nonce = request.headers.get('X-lnav-nonce', '')
crash_hash = request.headers.get('X-lnav-hash', '')
if not crash_hash.startswith("0000"):
return "Invalid proof of work hash", 401
sh = spookyhash.Hash128()
sh.update(nonce.encode('utf-8'))
sh.update(content)
verify_hash = sh.hexdigest()
if verify_hash != crash_hash:
return "Proof of work hash does not match", 401
# Generate a unique ID
unique_id = str(uuid.uuid4())
# Get the current time
current_time = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
# Construct the file name with current time and unique ID
file_name = f'crash_log_{current_time}_{unique_id}.txt'
full_path = os.path.join("/logs", file_name)
# Save the content to the file in the current directory
with open(full_path, 'wb') as f:
f.write(content)
return 'Data saved successfully!', 200
else:
return 'Unauthorized access!', 401
@app.route('/download_crashes', methods=['GET'])
def download_crashes():
# Retrieve the secret value for downloading from the environment variable
lnav_download_secret_env = os.environ.get('LNAV_DOWNLOAD_SECRET')
# Check if header 'lnav-secret' exists and has the correct value for downloading
if request.headers.get('lnav-secret') == lnav_download_secret_env:
# Get all the files in the current directory
crash_files = glob.glob("/logs/crash_log_*")
# Generate a unique ID for the zip file
zip_id = str(uuid.uuid4())
# Construct the zip file name
zip_file_name = f'crash_archive_{zip_id}.zip'
# Create a new zip file
with zipfile.ZipFile(zip_file_name, 'w') as zipf:
# Add each crash file to the zip file
for crash_file in crash_files:
zipf.write(crash_file)
# Delete the crash files
for crash_file in crash_files:
os.remove(crash_file)
@after_this_request
def remove_zip(response):
try:
os.remove(zip_file_name)
except Exception as e:
app.logger.error(f"Error removing zip file: {e}")
return response
# Send the zip file as a response
return send_file(zip_file_name, as_attachment=True, download_name=zip_file_name)
else:
return 'Unauthorized access!\n', 401
@app.route('/', methods=['GET'])
def root_page():
return ROOT_PAGE, 200
if __name__ == '__main__':
# Run the Flask app
app.run()

26
crashd/fly.toml Normal file
View File

@ -0,0 +1,26 @@
# fly.toml app configuration file generated for crashd on 2024-05-24T15:13:43-07:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = 'crashd'
primary_region = 'sea'
[build]
[mounts]
source = "crash_logs"
destination = "/logs"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[vm]]
memory = '1gb'
cpu_kind = 'shared'
cpus = 1

10
crashd/requirements.txt Normal file
View File

@ -0,0 +1,10 @@
blinker==1.8.2
click==8.1.7
Flask==3.0.3
gunicorn==22.0.0
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
packaging==24.0
spookyhash==2.1.0
Werkzeug==3.0.3

View File

@ -423,6 +423,7 @@ add_library(
collation-functions.cc
column_namer.cc
command_executor.cc
crashd.client.cc
curl_looper.cc
db_sub_source.cc
dump_internals.cc
@ -543,6 +544,7 @@ add_library(
byte_array.hh
command_executor.hh
column_namer.hh
crashd.client.hh
curl_looper.hh
doc_status_source.hh
dump_internals.hh

View File

@ -227,6 +227,7 @@ noinst_HEADERS = \
byte_array.hh \
column_namer.hh \
command_executor.hh \
crashd.client.hh \
curl_looper.hh \
data_scanner.hh \
data_scanner_re.re \
@ -426,6 +427,7 @@ libdiag_a_SOURCES = \
collation-functions.cc \
column_namer.cc \
command_executor.cc \
crashd.client.cc \
curl_looper.cc \
db_sub_source.cc \
document.sections.cc \

View File

@ -85,25 +85,14 @@
# error "SysV or X/Open-compatible Curses header file required"
#endif
#include "ansi_scrubber.hh"
#include "auto_mem.hh"
#include "enum_util.hh"
#include "lnav_log.hh"
#include "opt_util.hh"
static const size_t BUFFER_SIZE = 256 * 1024;
static const size_t MAX_LOG_LINE_SIZE = 2 * 1024;
static const char* CRASH_MSG
= "\n"
"\n"
"==== GURU MEDITATION ====\n"
"Unfortunately, lnav has crashed, sorry for the inconvenience.\n"
"\n"
"You can help improve lnav by sending the following file "
"to " PACKAGE_BUGREPORT
" :\n"
" %s\n"
"=========================\n";
static constexpr size_t BUFFER_SIZE = 256 * 1024;
static constexpr size_t MAX_LOG_LINE_SIZE = 2 * 1024;
std::optional<FILE*> lnav_log_file;
lnav_log_level_t lnav_log_level = lnav_log_level_t::DEBUG;
@ -546,7 +535,29 @@ sigabrt(int sig, siginfo_t* info, void* ctx)
tcsetattr(STDOUT_FILENO, TCSAFLUSH, termios);
dup2(STDOUT_FILENO, STDERR_FILENO);
};
fprintf(stderr, CRASH_MSG, crash_path);
fmt::print(R"(
{red_start}==== GURU MEDITATION ===={norm}
Unfortunately, lnav has crashed, sorry for the inconvenience.
You can help improve lnav by executing the following command
to upload the crash logs to https://crash.lnav.org:
{green_start}${norm} {bold_start}lnav -m crash upload{norm}
Or, you can send the following file to {PACKAGE_BUGREPORT}:
{crash_path}
{red_start}========================={norm}
)",
fmt::arg("red_start", ANSI_COLOR(1)),
fmt::arg("green_start", ANSI_COLOR(2)),
fmt::arg("bold_start", ANSI_BOLD_START),
fmt::arg("norm", ANSI_NORM),
fmt::arg("PACKAGE_BUGREPORT", PACKAGE_BUGREPORT),
fmt::arg("crash_path", crash_path));
#ifndef ATTACH_ON_SIGNAL
if (isatty(STDIN_FILENO)) {

132
src/crashd.client.cc Normal file
View File

@ -0,0 +1,132 @@
/**
* Copyright (c) 2024, Timothy Stack
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Timothy Stack nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "crashd.client.hh"
#include "base/fs_util.hh"
#include "curl_looper.hh"
#include "hasher.hh"
namespace lnav::crashd::client {
static constexpr auto USER_AGENT = "lnav/" PACKAGE_VERSION;
static constexpr auto SECRET = "2F40374C-25CE-4472-883F-CBBA4660A586";
static int
progress_tramp(
void* clientp, double dltotal, double dlnow, double ultotal, double ulnow)
{
progress_callback& cb = *static_cast<progress_callback*>(clientp);
switch (cb(dltotal, dlnow, ultotal, ulnow)) {
case progress_result_t::ok:
return 0;
default:
return 1;
}
}
Result<void, lnav::console::user_message>
upload(const std::filesystem::path& log_path, progress_callback cb)
{
const auto read_res = lnav::filesystem::read_file(log_path);
if (read_res.isErr()) {
return Err(lnav::console::user_message::error(
attr_line_t("unable to read crash log: ")
.append(lnav::roles::file(log_path)))
.with_reason(read_res.unwrapErr()));
}
const auto log_content = read_res.unwrap();
std::string hash_str;
auto nonce = 0;
{
while (true) {
auto ha = hasher();
ha.update(fmt::to_string(nonce));
ha.update(log_content);
hash_str = ha.to_string();
if (startswith(hash_str, "0000")) {
break;
}
nonce += 1;
}
}
curl_request cr("https://crash.lnav.org/crash");
curl_easy_setopt(cr, CURLOPT_VERBOSE, 0);
curl_easy_setopt(cr, CURLOPT_URL, cr.get_name().c_str());
curl_easy_setopt(cr, CURLOPT_POST, 1);
curl_easy_setopt(cr, CURLOPT_POSTFIELDS, log_content.c_str());
curl_easy_setopt(cr, CURLOPT_POSTFIELDSIZE, log_content.size());
curl_easy_setopt(cr, CURLOPT_USERAGENT, USER_AGENT);
curl_easy_setopt(cr, CURLOPT_XFERINFODATA, &cb);
curl_easy_setopt(cr, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(cr, CURLOPT_XFERINFOFUNCTION, progress_tramp);
const auto secret_header
= fmt::format(FMT_STRING("lnav-secret: {}"), SECRET);
const auto nonce_header
= fmt::format(FMT_STRING("X-lnav-nonce: {}"), nonce);
const auto hash_header
= fmt::format(FMT_STRING("X-lnav-hash: {}"), hash_str);
auto_mem<curl_slist> list(curl_slist_free_all);
list = curl_slist_append(list, "Content-Type: text/plain");
list = curl_slist_append(list, secret_header.c_str());
list = curl_slist_append(list, nonce_header.c_str());
list = curl_slist_append(list, hash_header.c_str());
curl_easy_setopt(cr, CURLOPT_HTTPHEADER, list.in());
const auto perform_res = cr.perform();
if (perform_res.isErr()) {
return Err(
lnav::console::user_message::error("unable to upload crash log")
.with_reason(curl_easy_strerror(perform_res.unwrapErr())));
}
const auto response = perform_res.unwrap();
if (cr.get_response_code() != 200) {
return Err(lnav::console::user_message::error(
attr_line_t("server rejected crash log: ")
.append(lnav::roles::file(log_path)))
.with_reason(response));
}
log_info("crashd response: %s", response.c_str());
return Ok();
}
} // namespace lnav::crashd::client

54
src/crashd.client.hh Normal file
View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2024, Timothy Stack
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Timothy Stack nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef lnav_crashd_client_hh
#define lnav_crashd_client_hh
#include <filesystem>
#include <functional>
#include "base/lnav.console.hh"
#include "base/result.h"
namespace lnav::crashd::client {
enum class progress_result_t {
ok,
abort,
};
using progress_callback = std::function<progress_result_t(
double dltotal, double dlnow, double ultotal, double ulnow)>;
Result<void, lnav::console::user_message> upload(
const std::filesystem::path& log_path, progress_callback callback);
} // namespace lnav::crashd::client
#endif

View File

@ -174,6 +174,27 @@ curl_request::string_cb(void* data, size_t size, size_t nmemb, void* userp)
return realsize;
}
curl_request::
curl_request(std::string name)
: cr_name(std::move(name)), cr_handle(curl_easy_cleanup)
{
this->cr_handle.reset(curl_easy_init());
curl_easy_setopt(this->cr_handle, CURLOPT_NOSIGNAL, 1);
curl_easy_setopt(
this->cr_handle, CURLOPT_ERRORBUFFER, this->cr_error_buffer);
curl_easy_setopt(this->cr_handle, CURLOPT_DEBUGFUNCTION, debug_cb);
curl_easy_setopt(this->cr_handle, CURLOPT_DEBUGDATA, this);
curl_easy_setopt(this->cr_handle, CURLOPT_VERBOSE, 1);
if (getenv("SSH_AUTH_SOCK") != nullptr) {
curl_easy_setopt(this->cr_handle,
CURLOPT_SSH_AUTH_TYPES,
# ifdef CURLSSH_AUTH_AGENT
CURLSSH_AUTH_AGENT |
# endif
CURLSSH_AUTH_PASSWORD);
}
}
long
curl_request::complete(CURLcode result)
{
@ -191,6 +212,22 @@ curl_request::complete(CURLcode result)
return -1;
}
Result<std::string, CURLcode>
curl_request::perform() const
{
std::string response;
curl_easy_setopt(this->get_handle(), CURLOPT_WRITEFUNCTION, string_cb);
curl_easy_setopt(this->get_handle(), CURLOPT_WRITEDATA, &response);
auto rc = curl_easy_perform(this->get_handle());
if (rc == CURLE_OK) {
return Ok(response);
}
return Err(rc);
}
curl_looper::
curl_looper()
: cl_curl_multi(curl_multi_cleanup)

View File

@ -74,25 +74,7 @@ public:
class curl_request {
public:
curl_request(std::string name)
: cr_name(std::move(name)), cr_handle(curl_easy_cleanup)
{
this->cr_handle.reset(curl_easy_init());
curl_easy_setopt(this->cr_handle, CURLOPT_NOSIGNAL, 1);
curl_easy_setopt(
this->cr_handle, CURLOPT_ERRORBUFFER, this->cr_error_buffer);
curl_easy_setopt(this->cr_handle, CURLOPT_DEBUGFUNCTION, debug_cb);
curl_easy_setopt(this->cr_handle, CURLOPT_DEBUGDATA, this);
curl_easy_setopt(this->cr_handle, CURLOPT_VERBOSE, 1);
if (getenv("SSH_AUTH_SOCK") != nullptr) {
curl_easy_setopt(this->cr_handle,
CURLOPT_SSH_AUTH_TYPES,
# ifdef CURLSSH_AUTH_AGENT
CURLSSH_AUTH_AGENT |
# endif
CURLSSH_AUTH_PASSWORD);
}
}
explicit curl_request(std::string name);
curl_request(const curl_request&) = delete;
curl_request(curl_request&&) = delete;
@ -114,20 +96,7 @@ public:
virtual long complete(CURLcode result);
Result<std::string, CURLcode> perform()
{
std::string response;
curl_easy_setopt(this->get_handle(), CURLOPT_WRITEFUNCTION, string_cb);
curl_easy_setopt(this->get_handle(), CURLOPT_WRITEDATA, &response);
auto rc = curl_easy_perform(this->get_handle());
if (rc == CURLE_OK) {
return Ok(response);
}
return Err(rc);
}
Result<std::string, CURLcode> perform() const;
long get_response_code() const
{

View File

@ -29,6 +29,8 @@
#include "lnav.management_cli.hh"
#include <glob.h>
#include "base/fs_util.hh"
#include "base/humanize.hh"
#include "base/humanize.time.hh"
@ -36,6 +38,7 @@
#include "base/paths.hh"
#include "base/result.h"
#include "base/string_util.hh"
#include "crashd.client.hh"
#include "file_options.hh"
#include "fmt/chrono.h"
#include "fmt/format.h"
@ -1109,11 +1112,100 @@ struct subcmd_regex101_t {
}
};
struct subcmd_crash_t {
using action_t = std::function<perform_result_t(const subcmd_crash_t&)>;
CLI::App* sc_app{nullptr};
action_t sc_action;
subcmd_crash_t& set_action(action_t act)
{
if (!this->sc_action) {
this->sc_action = std::move(act);
}
return *this;
}
static perform_result_t default_action(const subcmd_crash_t& sc)
{
auto um = console::user_message::error(
"expecting an operation related to crash logs");
um.with_help(
sc.sc_app->get_subcommands({})
| lnav::itertools::fold(
subcmd_reducer, attr_line_t{"the available operations are:"}));
return {um};
}
static perform_result_t upload_action(const subcmd_crash_t&)
{
static constexpr char SPINNER_CHARS[] = "-\\|/";
constexpr size_t SPINNER_SIZE = sizeof(SPINNER_CHARS) - 1;
static_root_mem<glob_t, globfree> gl;
const auto path = lnav::paths::dotlnav() / "crash" / "crash-*.log";
perform_result_t retval;
auto glob_rc = glob(path.c_str(), 0, nullptr, gl.inout());
if (glob_rc == GLOB_NOMATCH) {
auto um = console::user_message::info("no crash logs to upload");
return {um};
}
if (glob_rc != 0) {
auto um = console::user_message::error("unable to find crash logs");
return {um};
}
for (size_t lpc = 0; lpc < gl->gl_pathc; lpc++) {
auto crash_file = std::filesystem::path(gl->gl_pathv[lpc]);
int spinner_index = 0;
log_info("uploading crash log: %s", crash_file.c_str());
printf("~");
fflush(stdout);
auto upload_res = crashd::client::upload(
crash_file,
[&spinner_index](double dltotal,
double dlnow,
double ultotal,
double ulnow) {
printf("\b%c", SPINNER_CHARS[spinner_index % SPINNER_SIZE]);
spinner_index += 1;
fflush(stdout);
return crashd::client::progress_result_t::ok;
});
if (spinner_index > 0) {
printf("\b");
}
printf(".");
fflush(stdout);
if (upload_res.isErr()) {
retval.push_back(upload_res.unwrapErr());
} else {
std::error_code ec;
std::filesystem::remove(crash_file, ec);
}
}
printf("\n");
auto um = console::user_message::ok(
attr_line_t("uploaded ")
.append(lnav::roles::number(fmt::to_string(gl->gl_pathc)))
.append(" crash logs, thank you!"));
retval.push_back(um);
return retval;
}
};
using operations_v = mapbox::util::variant<no_subcmd_t,
subcmd_config_t,
subcmd_format_t,
subcmd_piper_t,
subcmd_regex101_t>;
subcmd_regex101_t,
subcmd_crash_t>;
class operations {
public:
@ -1135,6 +1227,7 @@ describe_cli(CLI::App& app, int argc, char* argv[])
subcmd_format_t format_args;
subcmd_piper_t piper_args;
subcmd_regex101_t regex101_args;
subcmd_crash_t crash_args;
{
auto* subcmd_config
@ -1336,6 +1429,22 @@ describe_cli(CLI::App& app, int argc, char* argv[])
}
}
{
auto* subcmd_crash
= app.add_subcommand("crash", "manage crash logs")->callback([&]() {
crash_args.set_action(subcmd_crash_t::default_action);
retval->o_ops = crash_args;
});
crash_args.sc_app = subcmd_crash;
{
subcmd_crash->add_subcommand("upload", "upload crash logs")
->callback([&]() {
crash_args.set_action(subcmd_crash_t::upload_action);
});
}
}
app.parse(argc, argv);
return retval;
@ -1358,7 +1467,8 @@ perform(std::shared_ptr<operations> opts)
[](const subcmd_config_t& sc) { return sc.sc_action(sc); },
[](const subcmd_format_t& sf) { return sf.sf_action(sf); },
[](const subcmd_piper_t& sp) { return sp.sp_action(sp); },
[](const subcmd_regex101_t& sr) { return sr.sr_action(sr); });
[](const subcmd_regex101_t& sr) { return sr.sr_action(sr); },
[](const subcmd_crash_t& sc) { return sc.sc_action(sc); });
}
} // namespace management

View File

@ -7042,6 +7042,18 @@ static std::unordered_map<char const*, std::vector<char const*>> aliases = {
{"write-table-to", {"write-cols-to"}},
};
static Result<std::string, lnav::console::user_message>
com_crash(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
{
if (args.empty()) {
} else if (!ec.ec_dry_run) {
int* nums = nullptr;
return ec.make_error(FMT_STRING("oops... {}"), nums[0]);
}
return Ok(std::string());
}
void
init_lnav_commands(readline_context::command_map_t& cmd_map)
{
@ -7064,10 +7076,12 @@ init_lnav_commands(readline_context::command_map_t& cmd_map)
}
if (getenv("lnav_test") != nullptr) {
static readline_context::command_t shexec(com_shexec),
poll_now(com_poll_now), test_comment(com_test_comment);
poll_now(com_poll_now), test_comment(com_test_comment),
crasher(com_crash);
cmd_map["shexec"] = &shexec;
cmd_map["poll-now"] = &poll_now;
cmd_map["test-comment"] = &test_comment;
cmd_map["crash"] = &crasher;
}
}