mirror of
https://github.com/tstack/lnav.git
synced 2024-08-17 00:40:30 +03:00
[docs] some more doc text
This commit is contained in:
parent
0f238f7972
commit
f5c72736cb
@ -71,6 +71,14 @@ As files are being indexed, if a matching format is found, the file is
|
||||
it is added to the [logfile_sub_source](src/logfile_sub_source.hh), which
|
||||
collates all log messages together into a single index.
|
||||
|
||||
### Timestamp Parsing
|
||||
|
||||
Since all log messages need to have a timestamp, timestamp parsing needs to be
|
||||
very efficient. The standard `strptime()` function is quite expensive, so lnav
|
||||
includes an optimized custom parser and code-generator in the
|
||||
[ptimec](src/ptimec.hh) component. The code-generator is used at compile-time
|
||||
to generate parsers for several [common formats](src/time_formats.am).
|
||||
|
||||
## Log Formats
|
||||
|
||||
[log_format](src/log_format.hh) instances are used to parse lines from files
|
||||
@ -83,9 +91,32 @@ implemented in the [log_format_impls.cc](src/log_format_impls.cc) file.
|
||||
|
||||
## User Interface
|
||||
|
||||
[![lnav TUI](docs/lnav-tui.png)](https://whimsical.com/lnav-tui-MQjXc7Vx23BxQTHrnuNp5F)
|
||||
|
||||
The lnav text-user-interface is built on top of the basic drawing functionality
|
||||
provided by [ncurses](https://invisible-island.net/ncurses/announce.html).
|
||||
The lnav text-user-interface is built on top of
|
||||
[ncurses](https://invisible-island.net/ncurses/announce.html).
|
||||
However, the higher-level functionality of panels, widgets, and such is not
|
||||
used.
|
||||
used. Instead, the following custom components are built on top of the ncurses
|
||||
primitives:
|
||||
|
||||
* [view_curses](src/view_curses.hh) - Provides the basics for text roles, which
|
||||
allows for themes to color and style text. The `mvwattrline()` function does
|
||||
all the heavy lifting of drawing ["attributed" lines](src/attr_line.hh),
|
||||
which are strings that have attributes associated with a given range of
|
||||
characters.
|
||||
* [listview_curses](src/listview_curses.hh) - Displays a list of items that are
|
||||
provided by a source.
|
||||
* [textview_curses](src/textview_curses.hh) - Builds on the list view by adding
|
||||
support for searching, filtering, bookmarks, etc... The main panel that
|
||||
displays the logs/plaintext/help is a textview.
|
||||
* [statusview_curses](src/state-extension-functions.cc) - Draws the status bars
|
||||
at the top and bottom of the TUI.
|
||||
* [vt52_curses](src/vt52_curses.hh) - Adapts vt52 escape codes to the ncurses
|
||||
API.
|
||||
* [readline_curses](src/readline_curses.hh) - Provides access to the readline
|
||||
library. The readline code is executed in a child process since readline
|
||||
does not get along with ncurses. The child process and readline is set to
|
||||
use a vt52 terminal and the vt52_curses view is uses to translate those
|
||||
escape codes to ncurses.
|
||||
|
||||
The following diagram shows the underlying components that make up the TUI:
|
||||
|
||||
[![lnav TUI](docs/lnav-tui.png)](https://whimsical.com/lnav-tui-MQjXc7Vx23BxQTHrnuNp5F)
|
||||
|
@ -47,11 +47,21 @@ own formats or if you need to modify existing ones. Format directories can
|
||||
also contain '.sql' and '.lnav' script files that can be used automate log file
|
||||
analysis.
|
||||
|
||||
The contents of the format configuration should be a JSON object with a field
|
||||
for each format defined by the file. Each field name should be the symbolic
|
||||
name of the format. This value will also be used as the SQL table name for
|
||||
the log. The value for each field should be another object with the following
|
||||
fields:
|
||||
An **lnav** format file must contain a single JSON object, preferably with a
|
||||
:code:`$schema` property that refers to the
|
||||
`config-v1.schema <https://lnav.org/schemas/config-v1.schema.json>`_,
|
||||
like so:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"$schema": "https://lnav.org/schemas/config-v1.schema.json"
|
||||
}
|
||||
|
||||
Each format to be defined in the file should a separate field in the top-level
|
||||
object. The field name should be the symbolic name of the format. This value
|
||||
will also be used as the SQL table name for the log. The value for each field
|
||||
should be another object with the following fields:
|
||||
|
||||
:title: The short and human-readable name for the format.
|
||||
:description: A longer description of the format.
|
||||
@ -253,6 +263,7 @@ Example format:
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"$schema": "https://lnav.org/schemas/format-v1.schema.json",
|
||||
"example_log" : {
|
||||
"title" : "Example Log Format",
|
||||
"description" : "Log format used in the documentation example.",
|
||||
@ -293,6 +304,7 @@ with the following contents:
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"$schema": "https://lnav.org/schemas/format-v1.schema.json",
|
||||
"example_log" : {
|
||||
"regex" : {
|
||||
"custom1" : {
|
||||
|
@ -61,58 +61,7 @@ builtin-sh-scripts.h builtin-sh-scripts.cc: bin2c$(BUILD_EXEEXT) $(BUILTIN_SHSCR
|
||||
%-json.cc %-json.h: $(srcdir)/%.json bin2c$(BUILD_EXEEXT)
|
||||
$(BIN2C_V)./bin2c$(BUILD_EXEEXT) $(*)-json $<
|
||||
|
||||
TIME_FORMATS = \
|
||||
"@%@" \
|
||||
"%Y-%m-%d %H:%M:%S" \
|
||||
"%Y-%m-%d %H:%M:%S%z" \
|
||||
"%Y-%m-%d %H:%M:%S %z" \
|
||||
"%Y-%m-%d %H:%M" \
|
||||
"%Y-%m-%dT%H:%M:%S.%f%z" \
|
||||
"%y-%m-%dT%H:%M:%S.%f%z" \
|
||||
"%Y-%m-%dT%H:%M:%SZ" \
|
||||
"%Y-%m-%dT%H:%M:%S%z" \
|
||||
"%Y-%m-%dT%H:%M:%S" \
|
||||
"%Y-%m-%dT%H:%M:%S%z" \
|
||||
"%Y/%m/%d %H:%M:%S" \
|
||||
"%Y/%m/%d %H:%M:%S %z" \
|
||||
"%Y/%m/%d %H:%M:%S%z" \
|
||||
"%Y/%m/%d %H:%M" \
|
||||
"%Y %b %d %a %H:%M:%S.%L" \
|
||||
"%Y %b %d %H:%M:%S.%L" \
|
||||
"%Y %b %d %H:%M:%S" \
|
||||
"%a %b %d %H:%M:%S %Y" \
|
||||
"%a %b %d %H:%M:%S.%f %Y" \
|
||||
"%a %b %d %H:%M:%S %Z %Y" \
|
||||
"%a %b %d %H:%M:%S " \
|
||||
"%a %b %d %H:%M:%S.%L " \
|
||||
"%d/%b/%Y:%H:%M:%S +0000" \
|
||||
"%d/%b/%Y:%H:%M:%S %z" \
|
||||
"%d-%b-%Y %H:%M:%S %z" \
|
||||
"%d-%b-%Y %H:%M:%S %Z" \
|
||||
"%d %b %Y %H:%M:%S" \
|
||||
"%d %b %Y %H:%M:%S.%L" \
|
||||
"%d %b %Y %H:%M:%S,%L" \
|
||||
"%b %d %H:%M:%S" \
|
||||
"%b %d %k:%M:%S" \
|
||||
"%b %d %l:%M:%S" \
|
||||
"%b %e, %Y %l:%M:%S %p" \
|
||||
"%m/%d/%y %H:%M:%S" \
|
||||
"%m/%d/%Y %I:%M:%S:%L %p %Z" \
|
||||
"%m/%d/%Y %I:%M:%S %p %Z" \
|
||||
"%m/%d/%Y %l:%M:%S %p %Z" \
|
||||
"%m/%e/%Y %I:%M:%S %p" \
|
||||
"%m/%e/%Y %l:%M:%S %p" \
|
||||
"%d/%b/%y %H:%M:%S" \
|
||||
"%m%d %H:%M:%S" \
|
||||
"%H:%M:%S" \
|
||||
"%M:%S" \
|
||||
"%m/%d %H:%M:%S" \
|
||||
"%Y-%m-%d" \
|
||||
"%Y-%m" \
|
||||
"%Y/%m/%d" \
|
||||
"%Y/%m" \
|
||||
"%s.%f" \
|
||||
$()
|
||||
include time_formats.am
|
||||
|
||||
time_fmts.cc: ptimec$(BUILD_EXEEXT)
|
||||
$(PTIME_V)./ptimec$(BUILD_EXEEXT) $(TIME_FORMATS) > $@
|
||||
|
@ -33,6 +33,16 @@
|
||||
#include <deque>
|
||||
#include <future>
|
||||
|
||||
namespace lnav {
|
||||
namespace futures {
|
||||
|
||||
/**
|
||||
* Create a future that is ready to immediately return a result.
|
||||
*
|
||||
* @tparam T The result type of the future.
|
||||
* @param t The value the future should return.
|
||||
* @return The new future.
|
||||
*/
|
||||
template<class T>
|
||||
std::future<std::decay_t<T>> make_ready_future( T&& t ) {
|
||||
std::promise<std::decay_t<T>> pr;
|
||||
@ -41,9 +51,18 @@ std::future<std::decay_t<T>> make_ready_future( T&& t ) {
|
||||
return r;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
/**
|
||||
* A queue used to limit the number of futures that are running concurrently.
|
||||
*
|
||||
* @tparam T The result of the futures.
|
||||
* @tparam MAX_QUEUE_SIZE The maximum number of futures that can be in flight.
|
||||
*/
|
||||
template<typename T, int MAX_QUEUE_SIZE = 8>
|
||||
class future_queue {
|
||||
public:
|
||||
/**
|
||||
* @param processor The function to execute with the result of a future.
|
||||
*/
|
||||
explicit future_queue(std::function<void(const T&)> processor)
|
||||
: fq_processor(processor) {};
|
||||
|
||||
@ -51,11 +70,24 @@ public:
|
||||
this->pop_to();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a future to the queue. If the size of the queue is greater than the
|
||||
* MAX_QUEUE_SIZE, this call will block waiting for the first queued
|
||||
* future to return a result.
|
||||
*
|
||||
* @param f The future to add to the queue.
|
||||
*/
|
||||
void push_back(std::future<T>&& f) {
|
||||
this->fq_deque.emplace_back(std::move(f));
|
||||
this->pop_to(8);
|
||||
this->pop_to(MAX_QUEUE_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the next future from the queue, waits for the result, and then
|
||||
* repeats until the queue reaches the given size.
|
||||
*
|
||||
* @param size The new desired size of the queue.
|
||||
*/
|
||||
void pop_to(size_t size = 0) {
|
||||
while (this->fq_deque.size() > size) {
|
||||
this->fq_processor(this->fq_deque.front().get());
|
||||
@ -67,4 +99,7 @@ public:
|
||||
std::deque<std::future<T>> fq_deque;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -816,7 +816,7 @@ future<string> pipe_callback(exec_context &ec, const string &cmdline, auto_fd &f
|
||||
HELP_MSG_1(X, "to close the file"));
|
||||
}
|
||||
|
||||
return make_ready_future(std::string());
|
||||
return lnav::futures::make_ready_future(std::string());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,7 +160,7 @@ file_collection::watch_logfile(const std::string &filename,
|
||||
int rc;
|
||||
|
||||
if (this->fc_closed_files.count(filename)) {
|
||||
return make_ready_future(retval);
|
||||
return lnav::futures::make_ready_future(retval);
|
||||
}
|
||||
|
||||
if (loo.loo_fd != -1) {
|
||||
@ -177,14 +177,14 @@ file_collection::watch_logfile(const std::string &filename,
|
||||
this->fc_file_names.end()) {
|
||||
retval.fc_file_names.emplace(wilddir, logfile_open_options());
|
||||
}
|
||||
return make_ready_future(retval);
|
||||
return lnav::futures::make_ready_future(retval);
|
||||
}
|
||||
if (!S_ISREG(st.st_mode)) {
|
||||
if (required) {
|
||||
rc = -1;
|
||||
errno = EINVAL;
|
||||
} else {
|
||||
return make_ready_future(retval);
|
||||
return lnav::futures::make_ready_future(retval);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -192,7 +192,7 @@ file_collection::watch_logfile(const std::string &filename,
|
||||
if (required) {
|
||||
retval.fc_name_to_errors[filename] = strerror(errno);
|
||||
}
|
||||
return make_ready_future(retval);
|
||||
return lnav::futures::make_ready_future(retval);
|
||||
}
|
||||
|
||||
auto stat_iter = find_if(this->fc_new_stats.begin(),
|
||||
@ -204,7 +204,7 @@ file_collection::watch_logfile(const std::string &filename,
|
||||
if (stat_iter != this->fc_new_stats.end()) {
|
||||
// this file is probably a link that we have already scanned in this
|
||||
// pass.
|
||||
return make_ready_future(retval);
|
||||
return lnav::futures::make_ready_future(retval);
|
||||
}
|
||||
|
||||
this->fc_new_stats.emplace_back(st);
|
||||
@ -214,7 +214,7 @@ file_collection::watch_logfile(const std::string &filename,
|
||||
|
||||
if (file_iter == this->fc_files.end()) {
|
||||
if (this->fc_other_files.find(filename) != this->fc_other_files.end()) {
|
||||
return make_ready_future(retval);
|
||||
return lnav::futures::make_ready_future(retval);
|
||||
}
|
||||
|
||||
auto func = [filename, loo, prog = this->fc_progress, errs = this->fc_name_to_errors]() mutable {
|
||||
@ -329,7 +329,7 @@ file_collection::watch_logfile(const std::string &filename,
|
||||
}
|
||||
}
|
||||
|
||||
return make_ready_future(retval);
|
||||
return lnav::futures::make_ready_future(retval);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -338,7 +338,7 @@ file_collection::watch_logfile(const std::string &filename,
|
||||
* @param path The glob pattern to expand.
|
||||
* @param required Passed to watch_logfile.
|
||||
*/
|
||||
void file_collection::expand_filename(future_queue<file_collection> &fq,
|
||||
void file_collection::expand_filename(lnav::futures::future_queue<file_collection> &fq,
|
||||
const std::string &path,
|
||||
logfile_open_options &loo,
|
||||
bool required)
|
||||
@ -404,7 +404,7 @@ void file_collection::expand_filename(future_queue<file_collection> &fq,
|
||||
file_collection file_collection::rescan_files(bool required)
|
||||
{
|
||||
file_collection retval;
|
||||
future_queue<file_collection> fq([&retval](auto &fc) {
|
||||
lnav::futures::future_queue<file_collection> fq([&retval](auto &fc) {
|
||||
retval.merge(fc);
|
||||
});
|
||||
|
||||
|
@ -83,8 +83,10 @@ struct file_collection {
|
||||
file_collection rescan_files(bool required = false);
|
||||
|
||||
void
|
||||
expand_filename(future_queue<file_collection> &fq, const std::string &path,
|
||||
logfile_open_options &loo, bool required);
|
||||
expand_filename(lnav::futures::future_queue<file_collection> &fq,
|
||||
const std::string &path,
|
||||
logfile_open_options &loo,
|
||||
bool required);
|
||||
|
||||
std::future<file_collection>
|
||||
watch_logfile(const std::string &filename, logfile_open_options &loo,
|
||||
|
@ -26,7 +26,11 @@
|
||||
"cache-ttl": {
|
||||
"title": "/tuning/archive-manager/cache-ttl",
|
||||
"description": "The time-to-live for unpacked archives, expressed as a duration (e.g. '3d' for three days)",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"3d",
|
||||
"12h"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@ -357,15 +361,18 @@
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"((?:x[0-9a-f]{2})+)": {
|
||||
"description": "The hexadecimal encoding of key codes to map to a command.",
|
||||
"description": "Map of key codes to commands to execute. The field names are the keys to be mapped using as a hexadecimal representation of the UTF-8 encoding. Each byte of the UTF-8 should start with an 'x' followed by the hexadecimal representation of the byte.",
|
||||
"title": "/ui/keymap-defs/<keymap_name>/<key_seq>",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"title": "/ui/keymap-defs/<keymap_name>/<key_seq>/command",
|
||||
"description": "The command to execute for the given key sequence.",
|
||||
"description": "The command to execute for the given key sequence. Use a script to execute more complicated operations.",
|
||||
"type": "string",
|
||||
"pattern": "[:|;].*"
|
||||
"pattern": "[:|;].*",
|
||||
"examples": [
|
||||
":goto next hour"
|
||||
]
|
||||
},
|
||||
"alt-msg": {
|
||||
"title": "/ui/keymap-defs/<keymap_name>/<key_seq>/alt-msg",
|
||||
|
@ -403,8 +403,11 @@ static void config_error_reporter(const yajlpp_parse_context &ypc,
|
||||
static struct json_path_container key_command_handlers = {
|
||||
yajlpp::property_handler("command")
|
||||
.with_synopsis("<command>")
|
||||
.with_description("The command to execute for the given key sequence.")
|
||||
.with_description(
|
||||
"The command to execute for the given key sequence. Use a script "
|
||||
"to execute more complicated operations.")
|
||||
.with_pattern("[:|;].*")
|
||||
.with_example(":goto next hour")
|
||||
.FOR_FIELD(key_command, kc_cmd),
|
||||
yajlpp::property_handler("alt-msg")
|
||||
.with_synopsis("<msg>")
|
||||
@ -414,7 +417,12 @@ static struct json_path_container key_command_handlers = {
|
||||
|
||||
static struct json_path_container keymap_def_handlers = {
|
||||
yajlpp::pattern_property_handler("(?<key_seq>(?:x[0-9a-f]{2})+)")
|
||||
.with_description("The hexadecimal encoding of key codes to map to a command.")
|
||||
.with_synopsis("<utf8-key-code-in-hex>")
|
||||
.with_description(
|
||||
"Map of key codes to commands to execute. The field names are "
|
||||
"the keys to be mapped using as a hexadecimal representation of "
|
||||
"the UTF-8 encoding. Each byte of the UTF-8 should start with "
|
||||
"an 'x' followed by the hexadecimal representation of the byte.")
|
||||
.with_obj_provider<key_command, key_map>([](const yajlpp_provider_context &ypc, key_map *km) {
|
||||
key_command &retval = km->km_seq_to_cmd[ypc.ypc_extractor.get_substr("key_seq")];
|
||||
|
||||
@ -863,6 +871,7 @@ static struct json_path_container ui_handlers = {
|
||||
|
||||
static struct json_path_container archive_handlers = {
|
||||
yajlpp::property_handler("min-free-space")
|
||||
.with_synopsis("<bytes>")
|
||||
.with_description(
|
||||
"The minimum free space, in bytes, to maintain when unpacking "
|
||||
"archives")
|
||||
@ -870,9 +879,12 @@ static struct json_path_container archive_handlers = {
|
||||
.for_field(&_lnav_config::lc_archive_manager,
|
||||
&archive_manager::config::amc_min_free_space),
|
||||
yajlpp::property_handler("cache-ttl")
|
||||
.with_synopsis("<duration>")
|
||||
.with_description(
|
||||
"The time-to-live for unpacked archives, expressed as a duration "
|
||||
"(e.g. '3d' for three days)")
|
||||
.with_example("3d")
|
||||
.with_example("12h")
|
||||
.for_field(&_lnav_config::lc_archive_manager,
|
||||
&archive_manager::config::amc_cache_ttl),
|
||||
};
|
||||
|
@ -770,7 +770,7 @@ public:
|
||||
int start,
|
||||
int end);
|
||||
|
||||
bool is_searching() { return this->tc_searching > 0; };
|
||||
bool is_searching() const { return this->tc_searching > 0; };
|
||||
|
||||
void set_follow_search_for(int64_t ms_to_deadline,
|
||||
std::function<bool()> func) {
|
||||
|
53
src/time_formats.am
Normal file
53
src/time_formats.am
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
TIME_FORMATS = \
|
||||
"@%@" \
|
||||
"%Y-%m-%d %H:%M:%S" \
|
||||
"%Y-%m-%d %H:%M:%S%z" \
|
||||
"%Y-%m-%d %H:%M:%S %z" \
|
||||
"%Y-%m-%d %H:%M" \
|
||||
"%Y-%m-%dT%H:%M:%S.%f%z" \
|
||||
"%y-%m-%dT%H:%M:%S.%f%z" \
|
||||
"%Y-%m-%dT%H:%M:%SZ" \
|
||||
"%Y-%m-%dT%H:%M:%S%z" \
|
||||
"%Y-%m-%dT%H:%M:%S" \
|
||||
"%Y-%m-%dT%H:%M:%S%z" \
|
||||
"%Y/%m/%d %H:%M:%S" \
|
||||
"%Y/%m/%d %H:%M:%S %z" \
|
||||
"%Y/%m/%d %H:%M:%S%z" \
|
||||
"%Y/%m/%d %H:%M" \
|
||||
"%Y %b %d %a %H:%M:%S.%L" \
|
||||
"%Y %b %d %H:%M:%S.%L" \
|
||||
"%Y %b %d %H:%M:%S" \
|
||||
"%a %b %d %H:%M:%S %Y" \
|
||||
"%a %b %d %H:%M:%S.%f %Y" \
|
||||
"%a %b %d %H:%M:%S %Z %Y" \
|
||||
"%a %b %d %H:%M:%S " \
|
||||
"%a %b %d %H:%M:%S.%L " \
|
||||
"%d/%b/%Y:%H:%M:%S +0000" \
|
||||
"%d/%b/%Y:%H:%M:%S %z" \
|
||||
"%d-%b-%Y %H:%M:%S %z" \
|
||||
"%d-%b-%Y %H:%M:%S %Z" \
|
||||
"%d %b %Y %H:%M:%S" \
|
||||
"%d %b %Y %H:%M:%S.%L" \
|
||||
"%d %b %Y %H:%M:%S,%L" \
|
||||
"%b %d %H:%M:%S" \
|
||||
"%b %d %k:%M:%S" \
|
||||
"%b %d %l:%M:%S" \
|
||||
"%b %e, %Y %l:%M:%S %p" \
|
||||
"%m/%d/%y %H:%M:%S" \
|
||||
"%m/%d/%Y %I:%M:%S:%L %p %Z" \
|
||||
"%m/%d/%Y %I:%M:%S %p %Z" \
|
||||
"%m/%d/%Y %l:%M:%S %p %Z" \
|
||||
"%m/%e/%Y %I:%M:%S %p" \
|
||||
"%m/%e/%Y %l:%M:%S %p" \
|
||||
"%d/%b/%y %H:%M:%S" \
|
||||
"%m%d %H:%M:%S" \
|
||||
"%H:%M:%S" \
|
||||
"%M:%S" \
|
||||
"%m/%d %H:%M:%S" \
|
||||
"%Y-%m-%d" \
|
||||
"%Y-%m" \
|
||||
"%Y/%m/%d" \
|
||||
"%Y/%m" \
|
||||
"%s.%f" \
|
||||
$()
|
@ -39,6 +39,9 @@
|
||||
|
||||
#include "ghc/filesystem.hpp"
|
||||
|
||||
/**
|
||||
* A source of a path for the unique_path_generator.
|
||||
*/
|
||||
class unique_path_source {
|
||||
public:
|
||||
virtual ~unique_path_source() = default;
|
||||
@ -65,6 +68,10 @@ private:
|
||||
std::string ups_unique_path;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a collection of filesystem paths, this class will generate a shortened
|
||||
* and unique path for each of the given paths.
|
||||
*/
|
||||
class unique_path_generator {
|
||||
public:
|
||||
void add_source(const std::shared_ptr<unique_path_source>& path_source);
|
||||
|
Loading…
Reference in New Issue
Block a user