• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tstack / lnav / 25348852825-3024

04 May 2026 11:18PM UTC coverage: 69.963% (+0.7%) from 69.226%
25348852825-3024

push

github

tstack
[ui] horizontal scroll should work on columns

Related to #1685

7 of 141 new or added lines in 5 files covered. (4.96%)

7760 existing lines in 84 files now uncovered.

57014 of 81492 relevant lines covered (69.96%)

622491.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.57
/src/session_data.cc
1
/**
2
 * Copyright (c) 2013, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 *
29
 * @file session_data.cc
30
 */
31

32
#include <algorithm>
33
#include <utility>
34

35
#include "session_data.hh"
36

37
#include <glob.h>
38
#include <stdio.h>
39
#include <sys/types.h>
40
#include <yajl/api/yajl_tree.h>
41

42
#include "base/fs_util.hh"
43
#include "base/isc.hh"
44
#include "base/itertools.hh"
45
#include "base/opt_util.hh"
46
#include "base/paths.hh"
47
#include "bookmarks.json.hh"
48
#include "bound_tags.hh"
49
#include "command_executor.hh"
50
#include "config.h"
51
#include "hasher.hh"
52
#include "lnav.events.hh"
53
#include "lnav.hh"
54
#include "lnav.indexing.hh"
55
#include "log_format_ext.hh"
56
#include "logfile.hh"
57
#include "service_tags.hh"
58
#include "sql_util.hh"
59
#include "sqlitepp.client.hh"
60
#include "tailer/tailer.looper.hh"
61
#include "timeline_source.hh"
62
#include "vtab_module.hh"
63
#include "yajlpp/yajlpp.hh"
64
#include "yajlpp/yajlpp_def.hh"
65

66
session_data_t session_data;
67
recent_refs_t recent_refs;
68

69
static const char* const LOG_METADATA_NAME = "log_metadata.db";
70

71
static const char* const META_TABLE_DEF = R"(
72
CREATE TABLE IF NOT EXISTS bookmarks (
73
    log_time datetime,
74
    log_format varchar(64),
75
    log_hash varchar(128),
76
    session_time integer,
77
    part_name text,
78
    access_time datetime DEFAULT CURRENT_TIMESTAMP,
79
    comment text DEFAULT '',
80
    tags text DEFAULT '',
81
    annotations text DEFAULT NULL,
82
    log_opid text DEFAULT NULL,
83
    sticky integer DEFAULT 0,
84

85
    PRIMARY KEY (log_time, log_format, log_hash, session_time)
86
);
87

88
CREATE TABLE IF NOT EXISTS time_offset (
89
    log_time datetime,
90
    log_format varchar(64),
91
    log_hash varchar(128),
92
    session_time integer,
93
    offset_sec integer,
94
    offset_usec integer,
95
    access_time datetime DEFAULT CURRENT_TIMESTAMP,
96

97
    PRIMARY KEY (log_time, log_format, log_hash, session_time)
98
);
99

100
CREATE TABLE IF NOT EXISTS recent_netlocs (
101
    netloc text,
102

103
    access_time datetime DEFAULT CURRENT_TIMESTAMP,
104

105
    PRIMARY KEY (netloc)
106
);
107

108
CREATE TABLE IF NOT EXISTS regex101_entries (
109
    format_name text NOT NULL,
110
    regex_name text NOT NULL,
111
    permalink text NOT NULL,
112
    delete_code text NOT NULL,
113

114
    PRIMARY KEY (format_name, regex_name),
115

116
    CHECK(
117
       format_name  <> '' AND
118
       regex_name   <> '' AND
119
       permalink    <> '')
120
);
121

122
CREATE TABLE IF NOT EXISTS text_bookmarks2 (
123
    file_path     text NOT NULL,
124
    line_number   integer NOT NULL,
125
    line_hash     text NOT NULL,
126
    mark_type     text NOT NULL DEFAULT 'user',
127
    session_time  integer NOT NULL,
128
    anchor        text DEFAULT NULL,
129
    anchor_offset integer DEFAULT NULL CHECK (anchor_offset >= 0),
130

131
    PRIMARY KEY (file_path, line_number, mark_type, session_time),
132

133
    CHECK(line_number >= 0),
134
    CHECK(mark_type IN ('user', 'sticky'))
135
);
136

137
CREATE TABLE IF NOT EXISTS timeline_bookmarks (
138
    row_type     text NOT NULL,
139
    row_name     text NOT NULL,
140
    mark_type    text NOT NULL DEFAULT 'sticky',
141
    session_time integer NOT NULL,
142

143
    PRIMARY KEY (row_type, row_name, mark_type, session_time),
144

145
    CHECK(mark_type IN ('user', 'sticky'))
146
);
147
)";
148

149
static const char* const BOOKMARK_LRU_STMT
150
    = "DELETE FROM bookmarks WHERE access_time <= "
151
      "  (SELECT DISTINCT access_time FROM bookmarks "
152
      "   ORDER BY access_time DESC LIMIT 1 OFFSET 50000)";
153

154
static const char* const NETLOC_LRU_STMT
155
    = "DELETE FROM recent_netlocs WHERE access_time <= "
156
      "  (SELECT DISTINCT access_time FROM bookmarks "
157
      "   ORDER BY access_time DESC LIMIT 1 OFFSET 10)";
158

159
static const char* const TEXT_BOOKMARK_LRU_STMT
160
    = "DELETE FROM text_bookmarks2 WHERE session_time <= "
161
      "  (SELECT DISTINCT session_time FROM text_bookmarks2 "
162
      "   ORDER BY session_time DESC LIMIT 1 OFFSET 50000)";
163

164
static const char* const TIMELINE_BOOKMARK_LRU_STMT
165
    = "DELETE FROM timeline_bookmarks WHERE session_time <= "
166
      "  (SELECT DISTINCT session_time FROM timeline_bookmarks "
167
      "   ORDER BY session_time DESC LIMIT 1 OFFSET 50000)";
168

169
static const char* const UPGRADE_STMTS[] = {
170
    R"(ALTER TABLE bookmarks ADD COLUMN comment text DEFAULT '';)",
171
    R"(ALTER TABLE bookmarks ADD COLUMN tags text DEFAULT '';)",
172
    R"(ALTER TABLE bookmarks ADD COLUMN annotations text DEFAULT NULL;)",
173
    R"(ALTER TABLE bookmarks ADD COLUMN log_opid text DEFAULT NULL;)",
174
    R"(ALTER TABLE bookmarks ADD COLUMN sticky integer DEFAULT 0;)",
175
    R"(DROP TABLE IF EXISTS text_bookmarks;)",
176
    R"(ALTER TABLE text_bookmarks2 ADD COLUMN anchor text DEFAULT NULL;)",
177
    R"(ALTER TABLE text_bookmarks2
178
         ADD COLUMN anchor_offset integer
179
             DEFAULT NULL CHECK (anchor_offset >= 0);)",
180
};
181

182
static constexpr size_t MAX_SESSIONS = 8;
183
static constexpr size_t MAX_SESSION_FILE_COUNT = 256;
184

185
struct session_line {
186
    session_line(struct timeval tv,
20✔
187
                 intern_string_t format_name,
188
                 std::string line_hash)
189
        : sl_time(tv), sl_format_name(format_name),
20✔
190
          sl_line_hash(std::move(line_hash))
20✔
191
    {
192
    }
20✔
193

194
    struct timeval sl_time;
195
    intern_string_t sl_format_name;
196
    std::string sl_line_hash;
197
};
198

199
static std::vector<session_line> marked_session_lines;
200
static std::vector<session_line> offset_session_lines;
201

202
static std::optional<std::string>
203
get_text_line_hash(logfile* lf, int line_number)
26✔
204
{
205
    auto res = lf->find_line(line_number) | lnav::itertools::to_result([&] {
52✔
206
                   return fmt::format(
UNCOV
207
                       FMT_STRING("could not find line {} in file {}"),
×
208
                       line_number,
UNCOV
209
                       lf->get_filename_as_string());
×
210
               })
211
        | lnav::itertools::map(&logfile::get_msg_range, lf)
78✔
212
        | lnav::itertools::map(&logfile::read_range, lf)
78✔
213
        | lnav::itertools::map(hash_string_for<shared_buffer_ref>);
52✔
214

215
    if (res.isErr()) {
26✔
UNCOV
216
        log_error("could not read line %d from file %s -- %s",
×
217
                  line_number,
218
                  lf->get_filename_as_string().c_str(),
219
                  res.unwrapErr().c_str());
UNCOV
220
        return std::nullopt;
×
221
    }
222

223
    return res.unwrap();
26✔
224
}
26✔
225

226
static bool
227
bind_line(sqlite3* db,
41✔
228
          sqlite3_stmt* stmt,
229
          content_line_t cl,
230
          time_t session_time)
231
{
232
    auto& lss = lnav_data.ld_log_source;
41✔
233
    auto lf = lss.find(cl);
41✔
234

235
    if (lf == nullptr) {
41✔
UNCOV
236
        return false;
×
237
    }
238

239
    sqlite3_clear_bindings(stmt);
41✔
240

241
    auto line_iter = lf->begin() + cl;
41✔
242
    auto fr = lf->get_file_range(line_iter, false);
41✔
243
    auto read_result = lf->read_range(fr);
41✔
244

245
    if (read_result.isErr()) {
41✔
UNCOV
246
        return false;
×
247
    }
248

249
    auto line_hash = read_result
250
                         .map([cl](auto sbr) {
82✔
251
                             return hasher()
82✔
252
                                 .update(sbr.get_data(), sbr.length())
41✔
253
                                 .update(cl)
41✔
254
                                 .to_string();
82✔
255
                         })
256
                         .unwrap();
41✔
257

258
    return bind_values(stmt,
82✔
259
                       lf->original_line_time(line_iter),
260
                       lf->get_format()->get_name(),
82✔
261
                       line_hash,
262
                       session_time)
263
        == SQLITE_OK;
41✔
264
}
41✔
265

266
struct session_file_info {
267
    session_file_info(int timestamp, std::string id, std::string path)
69✔
268
        : sfi_timestamp(timestamp), sfi_id(std::move(id)),
69✔
269
          sfi_path(std::move(path))
69✔
270
    {
271
    }
69✔
272

273
    bool operator<(const session_file_info& other) const
92✔
274
    {
275
        if (this->sfi_timestamp < other.sfi_timestamp) {
92✔
UNCOV
276
            return true;
×
277
        }
278
        if (this->sfi_path < other.sfi_path) {
92✔
UNCOV
279
            return true;
×
280
        }
281
        return false;
92✔
282
    }
283

284
    int sfi_timestamp;
285
    std::string sfi_id;
286
    std::string sfi_path;
287
};
288

289
static void
290
cleanup_session_data()
40✔
291
{
292
    static_root_mem<glob_t, globfree> session_file_list;
40✔
293
    std::list<session_file_info> session_info_list;
40✔
294
    std::map<std::string, int> session_count;
40✔
295
    auto session_file_pattern = lnav::paths::dotlnav() / "*-*.ts*.json";
40✔
296

297
    if (glob(
40✔
298
            session_file_pattern.c_str(), 0, nullptr, session_file_list.inout())
299
        == 0)
40✔
300
    {
301
        for (size_t lpc = 0; lpc < session_file_list->gl_pathc; lpc++) {
106✔
302
            const char* path = session_file_list->gl_pathv[lpc];
69✔
303
            char hash_id[64];
304
            int timestamp;
305
            const char* base;
306

307
            base = strrchr(path, '/');
69✔
308
            if (base == nullptr) {
69✔
UNCOV
309
                continue;
×
310
            }
311
            base += 1;
69✔
312
            if (sscanf(base, "file-%63[^.].ts%d.json", hash_id, &timestamp)
69✔
313
                == 2)
69✔
314
            {
315
                session_count[hash_id] += 1;
×
UNCOV
316
                session_info_list.emplace_back(timestamp, hash_id, path);
×
317
            }
318
            if (sscanf(base,
69✔
319
                       "view-info-%63[^.].ts%d.ppid%*d.json",
320
                       hash_id,
321
                       &timestamp)
322
                == 2)
69✔
323
            {
324
                session_count[hash_id] += 1;
69✔
325
                session_info_list.emplace_back(timestamp, hash_id, path);
69✔
326
            }
327
        }
328
    }
329

330
    session_info_list.sort();
40✔
331

332
    size_t session_loops = 0;
40✔
333

334
    while (session_info_list.size() > MAX_SESSION_FILE_COUNT) {
40✔
335
        const session_file_info& front = session_info_list.front();
×
336

UNCOV
337
        session_loops += 1;
×
UNCOV
338
        if (session_loops < MAX_SESSION_FILE_COUNT
×
UNCOV
339
            && session_count[front.sfi_id] == 1)
×
340
        {
UNCOV
341
            session_info_list.splice(session_info_list.end(),
×
342
                                     session_info_list,
UNCOV
343
                                     session_info_list.begin());
×
344
        } else {
UNCOV
345
            if (remove(front.sfi_path.c_str()) != 0) {
×
UNCOV
346
                log_error("Unable to remove session file: %s -- %s",
×
347
                          front.sfi_path.c_str(),
348
                          strerror(errno));
349
            }
UNCOV
350
            session_count[front.sfi_id] -= 1;
×
UNCOV
351
            session_info_list.pop_front();
×
352
        }
353
    }
354

355
    session_info_list.sort();
40✔
356

357
    while (session_info_list.size() > MAX_SESSION_FILE_COUNT) {
40✔
UNCOV
358
        const session_file_info& front = session_info_list.front();
×
359

UNCOV
360
        if (remove(front.sfi_path.c_str()) != 0) {
×
UNCOV
361
            log_error("Unable to remove session file: %s -- %s",
×
362
                      front.sfi_path.c_str(),
363
                      strerror(errno));
364
        }
365
        session_count[front.sfi_id] -= 1;
×
UNCOV
366
        session_info_list.pop_front();
×
367
    }
368
}
40✔
369

370
void
371
init_session()
776✔
372
{
373
    lnav_data.ld_session_time = time(nullptr);
776✔
374
    lnav_data.ld_session_id.clear();
776✔
375
    session_data.sd_view_states[LNV_LOG].vs_top = -1;
776✔
376
}
776✔
377

378
static std::optional<std::string>
379
compute_session_id()
82✔
380
{
381
    bool has_files = false;
82✔
382
    hasher h;
82✔
383

384
    for (auto& ld_file_name : lnav_data.ld_active_files.fc_file_names) {
171✔
385
        if (!ld_file_name.second.loo_include_in_session) {
89✔
UNCOV
386
            continue;
×
387
        }
388
        has_files = true;
89✔
389
        h.update(ld_file_name.first);
89✔
390
    }
391
    for (const auto& lf : lnav_data.ld_active_files.fc_files) {
171✔
392
        if (lf->is_valid_filename()) {
89✔
393
            continue;
88✔
394
        }
395
        if (!lf->get_open_options().loo_include_in_session) {
1✔
UNCOV
396
            continue;
×
397
        }
398

399
        has_files = true;
1✔
400
        h.update(lf->get_content_id());
1✔
401
    }
402
    if (!has_files) {
82✔
403
        return std::nullopt;
3✔
404
    }
405

406
    return h.to_string();
79✔
407
}
408

409
std::optional<session_pair_t>
410
scan_sessions()
40✔
411
{
412
    static_root_mem<glob_t, globfree> view_info_list;
40✔
413

414
    cleanup_session_data();
40✔
415

416
    const auto session_id = compute_session_id();
40✔
417
    if (!session_id) {
40✔
418
        return std::nullopt;
1✔
419
    }
420
    auto& session_file_names = lnav_data.ld_session_id[session_id.value()];
39✔
421
    session_file_names.clear();
39✔
422

423
    auto view_info_pattern_base
424
        = fmt::format(FMT_STRING("view-info-{}.*.json"), session_id.value());
117✔
425
    auto view_info_pattern = lnav::paths::dotlnav() / view_info_pattern_base;
39✔
426
    if (glob(view_info_pattern.c_str(), 0, nullptr, view_info_list.inout())
39✔
427
        == 0)
39✔
428
    {
429
        for (size_t lpc = 0; lpc < view_info_list->gl_pathc; lpc++) {
88✔
430
            const char* path = view_info_list->gl_pathv[lpc];
51✔
431
            int timestamp, ppid, rc;
432
            const char* base;
433

434
            base = strrchr(path, '/');
51✔
435
            if (base == nullptr) {
51✔
UNCOV
436
                continue;
×
437
            }
438
            base += 1;
51✔
439
            if ((rc = sscanf(base,
51✔
440
                             "view-info-%*[^.].ts%d.ppid%d.json",
441
                             &timestamp,
442
                             &ppid))
443
                == 2)
51✔
444
            {
445
                ppid_time_pair_t ptp;
51✔
446

447
                ptp.first = (ppid == getppid()) ? 1 : 0;
51✔
448
                ptp.second = timestamp;
51✔
449
                session_file_names.emplace_back(ptp, path);
51✔
450
            }
451
        }
452
    }
453

454
    session_file_names.sort();
39✔
455

456
    while (session_file_names.size() > MAX_SESSIONS) {
39✔
UNCOV
457
        const std::string& name = session_file_names.front().second;
×
458

UNCOV
459
        if (remove(name.c_str()) != 0) {
×
UNCOV
460
            log_error("Unable to remove session: %s -- %s",
×
461
                      name.c_str(),
462
                      strerror(errno));
463
        }
UNCOV
464
        session_file_names.pop_front();
×
465
    }
466

467
    if (session_file_names.empty()) {
39✔
468
        return std::nullopt;
2✔
469
    }
470

471
    return std::make_optional(session_file_names.back());
37✔
472
}
40✔
473

474
static void load_text_bookmarks(sqlite3* db);
475
static void load_timeline_bookmarks(sqlite3* db);
476

477
void
478
load_time_bookmarks()
40✔
479
{
480
    static const char* const BOOKMARK_STMT = R"(
481
       SELECT
482
         log_time,
483
         log_format,
484
         log_hash,
485
         session_time,
486
         part_name,
487
         access_time,
488
         comment,
489
         tags,
490
         annotations,
491
         log_opid,
492
         sticky,
493
         session_time=? AS same_session
494
       FROM bookmarks WHERE
495
         log_time BETWEEN ? AND ? AND
496
         log_format = ?
497
       ORDER BY same_session DESC, session_time DESC
498
)";
499

500
    static const char* const TIME_OFFSET_STMT = R"(
501
       SELECT
502
         *,
503
         session_time=? AS same_session
504
       FROM time_offset
505
       WHERE
506
         log_hash = ? AND
507
         log_time BETWEEN ? AND ? AND
508
         log_format = ?
509
       ORDER BY
510
         same_session DESC,
511
         session_time DESC
512
)";
513

514
    static auto op = lnav_operation{__FUNCTION__};
40✔
515

516
    auto op_guard = lnav_opid_guard::internal(op);
40✔
517
    auto& lss = lnav_data.ld_log_source;
40✔
518
    auto_sqlite3 db;
40✔
519
    auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
40✔
520
    auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
40✔
521
    bool reload_needed = false;
40✔
522
    bool meta_loaded = false;
40✔
523
    auto_mem<char, sqlite3_free> errmsg;
40✔
524

525
    log_info("loading bookmark db: %s", db_path.c_str());
40✔
526

527
    if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
40✔
528
        return;
×
529
    }
530

531
    for (const char* upgrade_stmt : UPGRADE_STMTS) {
360✔
532
        auto rc = sqlite3_exec(
320✔
533
            db.in(), upgrade_stmt, nullptr, nullptr, errmsg.out());
534
        if (rc != SQLITE_OK) {
320✔
535
            auto exterr = sqlite3_extended_errcode(db.in());
280✔
536
            log_error("unable to upgrade bookmark table -- (%d/%d) %s",
280✔
537
                      rc,
538
                      exterr,
539
                      errmsg.in());
540
        }
541
    }
542

543
    {
544
        auto netloc_prep_res
545
            = prepare_stmt(db.in(), "SELECT netloc FROM recent_netlocs");
40✔
546
        if (netloc_prep_res.isErr()) {
40✔
547
            log_error("unable to get netlocs: %s",
2✔
548
                      netloc_prep_res.unwrapErr().c_str());
549
            return;
2✔
550
        }
551

552
        auto netloc_stmt = netloc_prep_res.unwrap();
38✔
553
        bool done = false;
38✔
554

555
        while (!done) {
76✔
556
            done = netloc_stmt.fetch_row<std::string>().match(
76✔
557
                [](const std::string& netloc) {
×
UNCOV
558
                    recent_refs.rr_netlocs.insert(netloc);
×
UNCOV
559
                    return false;
×
560
                },
UNCOV
561
                [](const prepared_stmt::fetch_error& fe) {
×
UNCOV
562
                    log_error("failed to fetch netloc row: %s",
×
563
                              fe.fe_msg.c_str());
UNCOV
564
                    return true;
×
565
                },
566
                [](prepared_stmt::end_of_rows) { return true; });
76✔
567
        }
568
    }
40✔
569

570
    log_info("BEGIN select bookmarks");
38✔
571
    if (sqlite3_prepare_v2(db.in(), BOOKMARK_STMT, -1, stmt.out(), nullptr)
38✔
572
        != SQLITE_OK)
38✔
573
    {
UNCOV
574
        log_error("could not prepare bookmark select statement -- %s",
×
575
                  sqlite3_errmsg(db));
576
        return;
×
577
    }
578

579
    for (auto file_iter = lnav_data.ld_log_source.begin();
38✔
580
         file_iter != lnav_data.ld_log_source.end();
73✔
581
         ++file_iter)
35✔
582
    {
583
        auto lf = (*file_iter)->get_file();
35✔
584
        if (lf == nullptr) {
35✔
UNCOV
585
            continue;
×
586
        }
587
        if (lf->size() == 0) {
35✔
UNCOV
588
            continue;
×
589
        }
590
        const auto* format = lf->get_format_ptr();
35✔
591
        content_line_t base_content_line;
35✔
592

593
        base_content_line = lss.get_file_base_content_line(file_iter);
35✔
594

595
        auto low_line_iter = lf->begin();
35✔
596
        auto high_line_iter = lf->end();
35✔
597

598
        --high_line_iter;
35✔
599

600
        if (bind_values(stmt.in(),
35✔
601
                        lnav_data.ld_session_load_time,
602
                        lf->original_line_time(low_line_iter),
603
                        lf->original_line_time(high_line_iter),
604
                        lf->get_format()->get_name())
70✔
605
            != SQLITE_OK)
35✔
606
        {
UNCOV
607
            return;
×
608
        }
609

610
        date_time_scanner dts;
35✔
611
        bool done = false;
35✔
612
        int64_t last_mark_time = -1;
35✔
613
        size_t marked_count = 0;
35✔
614
        size_t sticky_count = 0;
35✔
615

616
        while (!done) {
138✔
617
            int rc = sqlite3_step(stmt.in());
103✔
618

619
            switch (rc) {
103✔
620
                case SQLITE_OK:
35✔
621
                case SQLITE_DONE:
622
                    done = true;
35✔
623
                    break;
35✔
624

625
                case SQLITE_ROW: {
68✔
626
                    const char* log_time
627
                        = (const char*) sqlite3_column_text(stmt.in(), 0);
68✔
628
                    const char* log_hash
629
                        = (const char*) sqlite3_column_text(stmt.in(), 2);
68✔
630
                    int64_t mark_time = sqlite3_column_int64(stmt.in(), 3);
68✔
631
                    const char* part_name
632
                        = (const char*) sqlite3_column_text(stmt.in(), 4);
68✔
633
                    const char* comment
634
                        = (const char*) sqlite3_column_text(stmt.in(), 6);
68✔
635
                    const char* tags
636
                        = (const char*) sqlite3_column_text(stmt.in(), 7);
68✔
637
                    const auto annotations = sqlite3_column_text(stmt.in(), 8);
68✔
638
                    const auto log_opid = sqlite3_column_text(stmt.in(), 9);
68✔
639
                    int sticky = sqlite3_column_int(stmt.in(), 10);
68✔
640
                    timeval log_tv;
641
                    exttm log_tm;
68✔
642

643
                    if (last_mark_time == -1) {
68✔
644
                        last_mark_time = mark_time;
34✔
645
                    } else if (last_mark_time != mark_time) {
34✔
UNCOV
646
                        done = true;
×
647
                        continue;
36✔
648
                    }
649

650
                    if (part_name == nullptr && !sticky) {
68✔
651
                        continue;
36✔
652
                    }
653

654
                    if (dts.scan(log_time,
32✔
655
                                 strlen(log_time),
656
                                 nullptr,
657
                                 &log_tm,
658
                                 log_tv)
659
                        == nullptr)
32✔
660
                    {
UNCOV
661
                        log_warning("bad log time: %s", log_time);
×
UNCOV
662
                        continue;
×
663
                    }
664

665
                    auto line_iter = format->lf_time_ordered
32✔
666
                        ? std::lower_bound(lf->begin(), lf->end(), log_tv)
32✔
667
                        : lf->begin();
13✔
668
                    while (line_iter != lf->end()) {
135✔
669
                        const auto line_tv = line_iter->get_timeval();
129✔
670

671
                        // NB: only milliseconds were stored in the DB, but the
672
                        // internal rep stores micros now.
673
                        if (line_tv.tv_sec != log_tv.tv_sec
129✔
674
                            || line_tv.tv_usec / 1000 != log_tv.tv_usec / 1000)
64✔
675
                        {
676
                            if (format->lf_time_ordered) {
83✔
677
                                break;
26✔
678
                            }
679
                            ++line_iter;
83✔
680
                            continue;
103✔
681
                        }
682

683
                        auto cl = content_line_t(
684
                            std::distance(lf->begin(), line_iter));
92✔
685
                        auto fr = lf->get_file_range(line_iter, false);
46✔
686
                        auto read_result = lf->read_range(fr);
46✔
687

688
                        if (read_result.isErr()) {
46✔
UNCOV
689
                            break;
×
690
                        }
691

692
                        auto sbr = read_result.unwrap();
46✔
693

694
                        auto line_hash
695
                            = hasher()
46✔
696
                                  .update(sbr.get_data(), sbr.length())
46✔
697
                                  .update(cl)
46✔
698
                                  .to_string();
46✔
699

700
                        if (line_hash != log_hash) {
46✔
701
                            // Using the formatted line for JSON-lines logs was
702
                            // a mistake in earlier versions. To carry forward
703
                            // older bookmarks, we need to replicate the bad
704
                            // behavior.
705
                            auto hack_read_res
706
                                = lf->read_line(line_iter, {false, true});
20✔
707
                            if (hack_read_res.isErr()) {
20✔
UNCOV
708
                                break;
×
709
                            }
710
                            auto hack_sbr = hack_read_res.unwrap();
20✔
711
                            auto hack_hash = hasher()
20✔
712
                                                 .update(hack_sbr.get_data(),
20✔
713
                                                         hack_sbr.length())
714
                                                 .update(cl)
20✔
715
                                                 .to_string();
20✔
716
                            if (hack_hash == log_hash) {
20✔
UNCOV
717
                                log_trace("needed hack to match line: %s:%d",
×
718
                                          lf->get_filename_as_string().c_str(),
719
                                          (int) cl);
720
                            } else {
721
                                ++line_iter;
20✔
722
                                continue;
20✔
723
                            }
724
                        }
60✔
725
                        auto& bm_meta = lf->get_bookmark_metadata();
26✔
726
                        auto line_number = static_cast<uint32_t>(
727
                            std::distance(lf->begin(), line_iter));
26✔
728
                        content_line_t line_cl
729
                            = content_line_t(base_content_line + line_number);
26✔
730
                        bool meta = false;
26✔
731

732
                        if (part_name != nullptr && part_name[0] != '\0') {
26✔
733
                            lss.set_user_mark(&textview_curses::BM_PARTITION,
7✔
734
                                              line_cl);
735
                            bm_meta[line_number].bm_name = part_name;
7✔
736
                            if (!meta) {
7✔
737
                                log_debug("  loaded partition bookmark");
7✔
738
                            }
739
                            meta = true;
7✔
740
                        }
741
                        if (comment != nullptr && comment[0] != '\0') {
26✔
742
                            lss.set_user_mark(&textview_curses::BM_META,
7✔
743
                                              line_cl);
744
                            bm_meta[line_number].bm_comment = comment;
7✔
745
                            if (!meta) {
7✔
746
                                log_debug("  loaded message comment");
7✔
747
                            }
748
                            meta = true;
7✔
749
                        }
750
                        if (tags != nullptr && tags[0] != '\0') {
26✔
751
                            auto_mem<yajl_val_s> tag_list(yajl_tree_free);
10✔
752
                            char error_buffer[1024];
753

754
                            tag_list = yajl_tree_parse(
755
                                tags, error_buffer, sizeof(error_buffer));
10✔
756
                            if (!YAJL_IS_ARRAY(tag_list.in())) {
10✔
UNCOV
757
                                log_error("invalid tags column: %s", tags);
×
758
                            } else {
759
                                lss.set_user_mark(&textview_curses::BM_META,
10✔
760
                                                  line_cl);
761
                                for (size_t lpc = 0;
20✔
762
                                     lpc < tag_list.in()->u.array.len;
20✔
763
                                     lpc++)
764
                                {
765
                                    yajl_val elem
766
                                        = tag_list.in()->u.array.values[lpc];
10✔
767

768
                                    if (!YAJL_IS_STRING(elem)) {
10✔
UNCOV
769
                                        continue;
×
770
                                    }
771
                                    bookmark_metadata::KNOWN_TAGS.insert(
10✔
772
                                        elem->u.string);
10✔
773
                                    bm_meta[line_number].add_tag(
30✔
774
                                        elem->u.string);
10✔
775
                                }
776
                            }
777
                            if (!meta) {
10✔
778
                                log_debug("  loaded tags");
3✔
779
                            }
780
                            meta = true;
10✔
781
                        }
10✔
782
                        if (annotations != nullptr && annotations[0] != '\0') {
26✔
783
                            static const intern_string_t SRC
784
                                = intern_string::lookup("annotations");
3✔
785

786
                            const auto anno_sf
787
                                = string_fragment::from_c_str(annotations);
1✔
788
                            auto parse_res
789
                                = logmsg_annotations_handlers.parser_for(SRC)
2✔
790
                                      .of(anno_sf);
1✔
791
                            if (!meta) {
1✔
792
                                log_debug("  loaded message annotation");
1✔
793
                            }
794
                            if (parse_res.isErr()) {
1✔
UNCOV
795
                                log_error(
×
796
                                    "unable to parse annotations JSON -- "
797
                                    "%s",
798
                                    parse_res.unwrapErr()[0]
799
                                        .to_attr_line()
800
                                        .get_string()
801
                                        .c_str());
802
                            } else if (bm_meta.find(line_number)
1✔
803
                                       == bm_meta.end())
2✔
804
                            {
805
                                lss.set_user_mark(&textview_curses::BM_META,
1✔
806
                                                  line_cl);
807
                                bm_meta[line_number].bm_annotations
1✔
808
                                    = parse_res.unwrap();
2✔
809
                                meta = true;
1✔
810
                            } else {
UNCOV
811
                                meta = true;
×
812
                            }
813
                        }
1✔
814
                        if (log_opid != nullptr && log_opid[0] != '\0') {
26✔
815
                            auto opid_sf
816
                                = string_fragment::from_c_str(log_opid);
×
817
                            lf->set_logline_opid(line_number, opid_sf);
×
UNCOV
818
                            if (!meta) {
×
UNCOV
819
                                log_debug("  loaded user opid");
×
820
                            }
UNCOV
821
                            meta = true;
×
822
                        }
823
                        if (meta) {
26✔
824
                            meta_loaded = true;
18✔
825
                        } else if (part_name != nullptr) {
8✔
826
                            marked_session_lines.emplace_back(
4✔
827
                                lf->original_line_time(line_iter),
4✔
828
                                format->get_name(),
4✔
829
                                line_hash);
830
                            lss.set_user_mark(&textview_curses::BM_USER,
4✔
831
                                              line_cl);
832
                            marked_count += 1;
4✔
833
                        }
834
                        if (sticky) {
26✔
835
                            lss.set_user_mark(&textview_curses::BM_STICKY,
4✔
836
                                              line_cl);
837
                            sticky_count += 1;
4✔
838
                        }
839
                        reload_needed = true;
26✔
840
                        break;
26✔
841
                    }
138✔
842
                    break;
32✔
843
                }
844

845
                default: {
×
846
                    const char* errmsg;
847

UNCOV
848
                    errmsg = sqlite3_errmsg(lnav_data.ld_db);
×
UNCOV
849
                    log_error(
×
850
                        "bookmark select error: code %d -- %s", rc, errmsg);
UNCOV
851
                    done = true;
×
UNCOV
852
                } break;
×
853
            }
854
        }
855

856
        if (marked_count > 0 || sticky_count > 0) {
35✔
857
            log_info("  loaded %zu marked and %zu sticky lines from %s",
8✔
858
                     marked_count,
859
                     sticky_count,
860
                     lf->get_filename_as_string().c_str());
861
        }
862
        sqlite3_reset(stmt.in());
35✔
863
    }
35✔
864
    log_info("END select bookmarks");
38✔
865

866
    log_info("BEGIN select time_offset");
38✔
867
    if (sqlite3_prepare_v2(db.in(), TIME_OFFSET_STMT, -1, stmt.out(), nullptr)
38✔
868
        != SQLITE_OK)
38✔
869
    {
UNCOV
870
        log_error("could not prepare time_offset select statement -- %s",
×
871
                  sqlite3_errmsg(db));
UNCOV
872
        return;
×
873
    }
874

875
    for (auto file_iter = lnav_data.ld_log_source.begin();
38✔
876
         file_iter != lnav_data.ld_log_source.end();
73✔
877
         ++file_iter)
35✔
878
    {
879
        auto lf = (*file_iter)->get_file();
35✔
880
        content_line_t base_content_line;
35✔
881

882
        if (lf == nullptr) {
35✔
UNCOV
883
            continue;
×
884
        }
885
        if (lf->size() == 0) {
35✔
UNCOV
886
            continue;
×
887
        }
888

889
        lss.find(lf->get_filename_as_string().c_str(), base_content_line);
35✔
890

891
        auto low_line_iter = lf->begin();
35✔
892
        auto high_line_iter = lf->end();
35✔
893

894
        --high_line_iter;
35✔
895

896
        if (bind_values(stmt.in(),
70✔
897
                        lnav_data.ld_session_load_time,
898
                        lf->get_content_id(),
35✔
899
                        lf->original_line_time(low_line_iter),
900
                        lf->original_line_time(high_line_iter),
901
                        lf->get_format()->get_name())
70✔
902
            != SQLITE_OK)
35✔
903
        {
UNCOV
904
            return;
×
905
        }
906

907
        date_time_scanner dts;
35✔
908
        auto done = false;
35✔
909
        int64_t last_mark_time = -1;
35✔
910

911
        while (!done) {
104✔
912
            const auto rc = sqlite3_step(stmt.in());
69✔
913

914
            switch (rc) {
69✔
915
                case SQLITE_OK:
35✔
916
                case SQLITE_DONE:
917
                    done = true;
35✔
918
                    break;
35✔
919

920
                case SQLITE_ROW: {
34✔
921
                    const auto* log_time
922
                        = (const char*) sqlite3_column_text(stmt.in(), 0);
34✔
923
                    int64_t mark_time = sqlite3_column_int64(stmt.in(), 3);
34✔
924
                    timeval log_tv;
925
                    exttm log_tm;
34✔
926

927
                    if (last_mark_time == -1) {
34✔
928
                        last_mark_time = mark_time;
34✔
UNCOV
929
                    } else if (last_mark_time != mark_time) {
×
UNCOV
930
                        done = true;
×
931
                        continue;
31✔
932
                    }
933

934
                    if (sqlite3_column_type(stmt.in(), 4) == SQLITE_NULL) {
34✔
935
                        continue;
31✔
936
                    }
937

938
                    if (!dts.scan(log_time,
3✔
939
                                  strlen(log_time),
940
                                  nullptr,
941
                                  &log_tm,
942
                                  log_tv))
943
                    {
UNCOV
944
                        continue;
×
945
                    }
946

947
                    auto line_iter
948
                        = lower_bound(lf->begin(), lf->end(), log_tv);
3✔
949
                    while (line_iter != lf->end()) {
6✔
950
                        auto line_tv = line_iter->get_timeval();
6✔
951

952
                        if ((line_tv.tv_sec != log_tv.tv_sec)
6✔
953
                            || (line_tv.tv_usec / 1000
3✔
954
                                != log_tv.tv_usec / 1000))
3✔
955
                        {
956
                            break;
957
                        }
958

959
                        int file_line = std::distance(lf->begin(), line_iter);
3✔
960
                        timeval offset;
961

962
                        offset_session_lines.emplace_back(
3✔
963
                            lf->original_line_time(line_iter),
3✔
964
                            lf->get_format_ptr()->get_name(),
3✔
965
                            lf->get_content_id());
3✔
966
                        offset.tv_sec = sqlite3_column_int64(stmt.in(), 4);
3✔
967
                        offset.tv_usec = sqlite3_column_int64(stmt.in(), 5);
3✔
968
                        lf->adjust_content_time(file_line, offset);
3✔
969

970
                        reload_needed = true;
3✔
971

972
                        ++line_iter;
3✔
973
                    }
974
                    break;
3✔
975
                }
976

UNCOV
977
                default: {
×
UNCOV
978
                    const auto* errmsg = sqlite3_errmsg(lnav_data.ld_db);
×
UNCOV
979
                    log_error(
×
980
                        "bookmark select error: code %d -- %s", rc, errmsg);
UNCOV
981
                    done = true;
×
UNCOV
982
                    break;
×
983
                }
984
            }
985
        }
986

987
        sqlite3_reset(stmt.in());
35✔
988
    }
35✔
989
    log_info("END select time_offset");
38✔
990

991
    if (reload_needed) {
38✔
992
        if (meta_loaded) {
21✔
993
            lss.set_line_meta_changed();
17✔
994
            lss.text_filters_changed();
17✔
995
        }
996
        lnav_data.ld_views[LNV_LOG].reload_data();
21✔
997
    }
998

999
    load_text_bookmarks(db.in());
38✔
1000
    load_timeline_bookmarks(db.in());
38✔
1001
}
48✔
1002

1003
static int
1004
read_files(yajlpp_parse_context* ypc,
38✔
1005
           const unsigned char* str,
1006
           size_t len,
1007
           yajl_string_props_t*)
1008
{
1009
    return 1;
38✔
1010
}
1011

1012
const json_path_handler_base::enum_value_t LEVEL_ENUM[] = {
1013
    {level_names[LEVEL_TRACE], LEVEL_TRACE},
1014
    {level_names[LEVEL_DEBUG5], LEVEL_DEBUG5},
1015
    {level_names[LEVEL_DEBUG4], LEVEL_DEBUG4},
1016
    {level_names[LEVEL_DEBUG3], LEVEL_DEBUG3},
1017
    {level_names[LEVEL_DEBUG2], LEVEL_DEBUG2},
1018
    {level_names[LEVEL_DEBUG], LEVEL_DEBUG},
1019
    {level_names[LEVEL_INFO], LEVEL_INFO},
1020
    {level_names[LEVEL_STATS], LEVEL_STATS},
1021
    {level_names[LEVEL_NOTICE], LEVEL_NOTICE},
1022
    {level_names[LEVEL_WARNING], LEVEL_WARNING},
1023
    {level_names[LEVEL_ERROR], LEVEL_ERROR},
1024
    {level_names[LEVEL_CRITICAL], LEVEL_CRITICAL},
1025
    {level_names[LEVEL_FATAL], LEVEL_FATAL},
1026

1027
    json_path_handler_base::ENUM_TERMINATOR,
1028
};
1029

1030
static const json_path_container view_def_handlers = {
1031
    json_path_handler("top_line").for_field(&view_state::vs_top),
1032
    json_path_handler("focused_line").for_field(&view_state::vs_selection),
1033
    json_path_handler("anchor").for_field(&view_state::vs_anchor),
1034
    json_path_handler("anchor_offset").for_field(&view_state::vs_anchor_offset),
1035
    json_path_handler("search").for_field(&view_state::vs_search),
1036
    json_path_handler("word_wrap").for_field(&view_state::vs_word_wrap),
1037
    json_path_handler("filtering").for_field(&view_state::vs_filtering),
1038
    json_path_handler("min_level")
1039
        .with_enum_values(LEVEL_ENUM)
1040
        .for_field(&view_state::vs_min_log_level),
1041
    json_path_handler("commands#").for_field(&view_state::vs_commands),
1042
};
1043

1044
static const json_path_container view_handlers = {
1045
    yajlpp::pattern_property_handler("(?<view_name>[\\w\\-]+)")
1046
        .with_obj_provider<view_state, session_data_t>(
1047
            +[](const yajlpp_provider_context& ypc, session_data_t* root) {
2,703✔
1048
                auto view_index_opt
1049
                    = view_from_string(ypc.get_substr("view_name").c_str());
2,703✔
1050
                if (view_index_opt) {
2,703✔
1051
                    return &root->sd_view_states[view_index_opt.value()];
2,703✔
1052
                }
1053

UNCOV
1054
                log_error("unknown view name: %s",
×
1055
                          ypc.get_substr("view_name").c_str());
1056
                static view_state dummy;
UNCOV
1057
                return &dummy;
×
1058
            })
1059
        .with_children(view_def_handlers),
1060
};
1061

1062
static const json_path_container file_state_handlers = {
1063
    yajlpp::property_handler("visible")
1064
        .with_description("Indicates whether the file is visible or not")
1065
        .for_field(&file_state::fs_is_visible),
1066
};
1067

1068
static const json_path_container file_states_handlers = {
1069
    yajlpp::pattern_property_handler(R"((?<filename>[^/]+))")
1070
        .with_description("Map of file names to file state objects")
1071
        .with_obj_provider<file_state, session_data_t>(
1072
            [](const auto& ypc, session_data_t* root) {
114✔
1073
                auto fn = ypc.get_substr("filename");
114✔
1074
                return &root->sd_file_states[fn];
228✔
1075
            })
114✔
1076
        .with_children(file_state_handlers),
1077
};
1078

1079
static const typed_json_path_container<session_data_t> view_info_handlers = {
1080
    yajlpp::property_handler("save-time")
1081
        .for_field(&session_data_t::sd_save_time),
1082
    yajlpp::property_handler("time-offset")
1083
        .for_field(&session_data_t::sd_time_offset),
1084
    json_path_handler("files#", read_files),
1085
    yajlpp::property_handler("file-states").with_children(file_states_handlers),
1086
    yajlpp::property_handler("views").with_children(view_handlers),
1087
};
1088

1089
void
1090
load_session()
40✔
1091
{
1092
    static auto op = lnav_operation{"load_session"};
40✔
1093

1094
    auto op_guard = lnav_opid_guard::internal(op);
40✔
1095
    log_info("BEGIN load_session");
40✔
1096
    scan_sessions() | [](const auto pair) {
40✔
1097
        lnav_data.ld_session_load_time = pair.first.second;
37✔
1098
        const auto& view_info_path = pair.second;
37✔
1099
        auto view_info_src = intern_string::lookup(view_info_path.string());
37✔
1100

1101
        auto open_res = lnav::filesystem::open_file(view_info_path, O_RDONLY);
37✔
1102
        if (open_res.isErr()) {
37✔
UNCOV
1103
            log_error("cannot open session file: %s -- %s",
×
1104
                      view_info_path.c_str(),
1105
                      open_res.unwrapErr().c_str());
UNCOV
1106
            return;
×
1107
        }
1108

1109
        auto fd = open_res.unwrap();
37✔
1110
        unsigned char buffer[1024];
1111
        ssize_t rc;
1112

1113
        log_info("loading session file: %s", view_info_path.c_str());
37✔
1114
        auto parser = view_info_handlers.parser_for(view_info_src);
37✔
1115
        while ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
117✔
1116
            auto buf_frag = string_fragment::from_bytes(buffer, rc);
40✔
1117
            auto parse_res = parser.consume(buf_frag);
40✔
1118
            if (parse_res.isErr()) {
40✔
UNCOV
1119
                log_error("failed to load session: %s -- %s",
×
1120
                          view_info_path.c_str(),
1121
                          parse_res.unwrapErr()[0]
1122
                              .to_attr_line()
1123
                              .get_string()
1124
                              .c_str());
UNCOV
1125
                return;
×
1126
            }
1127
        }
1128

1129
        auto complete_res = parser.complete();
37✔
1130
        if (complete_res.isErr()) {
37✔
UNCOV
1131
            log_error("failed to load session: %s -- %s",
×
1132
                      view_info_path.c_str(),
1133
                      complete_res.unwrapErr()[0]
1134
                          .to_attr_line()
1135
                          .get_string()
1136
                          .c_str());
UNCOV
1137
            return;
×
1138
        }
1139
        session_data = complete_res.unwrap();
37✔
1140

1141
        bool log_changes = false;
37✔
1142

1143
        for (auto& lf : lnav_data.ld_active_files.fc_files) {
77✔
1144
            auto iter = session_data.sd_file_states.find(lf->get_filename());
40✔
1145

1146
            if (iter == session_data.sd_file_states.end()) {
40✔
1147
                continue;
2✔
1148
            }
1149

1150
            log_debug("found state for file: %s %d (%s)",
38✔
1151
                      lf->get_content_id().c_str(),
1152
                      iter->second.fs_is_visible,
1153
                      lf->get_filename_as_string().c_str());
1154
            lnav_data.ld_log_source.find_data(lf) |
38✔
1155
                [iter, &log_changes](auto ld) {
64✔
1156
                    if (ld->ld_visible != iter->second.fs_is_visible) {
32✔
1157
                        ld->get_file_ptr()->set_indexing(
1✔
1158
                            iter->second.fs_is_visible);
1✔
1159
                        ld->set_visibility(iter->second.fs_is_visible);
1✔
1160
                        log_changes = true;
1✔
1161
                    }
1162
                };
1163
        }
1164

1165
        if (log_changes) {
37✔
1166
            lnav_data.ld_log_source.text_filters_changed();
1✔
1167
        }
1168
    };
37✔
1169

1170
    lnav::events::publish(lnav_data.ld_db.in(),
40✔
1171
                          lnav::events::session::loaded{});
1172

1173
    log_info("END load_session");
40✔
1174
}
40✔
1175

1176
static void
1177
yajl_writer(void* context, const char* str, size_t len)
15,443✔
1178
{
1179
    FILE* file = (FILE*) context;
15,443✔
1180

1181
    fwrite(str, len, 1, file);
15,443✔
1182
}
15,443✔
1183

1184
static void
1185
save_user_bookmarks(sqlite3* db,
42✔
1186
                    sqlite3_stmt* stmt,
1187
                    bookmark_vector<content_line_t>& user_marks,
1188
                    bookmark_vector<content_line_t>& sticky_marks)
1189
{
1190
    auto& lss = lnav_data.ld_log_source;
42✔
1191

1192
    // Collect all lines that are either user-marked or sticky
1193
    tlx::btree_set<content_line_t> all_lines;
42✔
1194
    for (const auto& cl : user_marks.bv_tree) {
45✔
1195
        all_lines.insert(cl);
3✔
1196
    }
1197
    for (const auto& cl : sticky_marks.bv_tree) {
45✔
1198
        all_lines.insert(cl);
3✔
1199
    }
1200

1201
    for (const auto& cl_const : all_lines) {
48✔
1202
        auto cl = cl_const;
6✔
1203
        auto lf = lss.find(cl);
6✔
1204
        if (lf == nullptr) {
6✔
1205
            continue;
×
1206
        }
1207

1208
        sqlite3_clear_bindings(stmt);
6✔
1209

1210
        const auto line_iter = lf->begin() + cl;
6✔
1211
        auto fr = lf->get_file_range(line_iter, false);
6✔
1212
        auto read_result = lf->read_range(fr);
6✔
1213

1214
        if (read_result.isErr()) {
6✔
UNCOV
1215
            continue;
×
1216
        }
1217

1218
        auto line_hash = read_result
1219
                             .map([cl](auto sbr) {
12✔
1220
                                 return hasher()
12✔
1221
                                     .update(sbr.get_data(), sbr.length())
6✔
1222
                                     .update(cl)
6✔
1223
                                     .to_string();
12✔
1224
                             })
1225
                             .unwrap();
6✔
1226

1227
        if (bind_values(stmt,
18✔
1228
                        lf->original_line_time(line_iter),
1229
                        lf->get_format()->get_name(),
12✔
1230
                        line_hash,
1231
                        lnav_data.ld_session_time)
1232
            != SQLITE_OK)
6✔
1233
        {
UNCOV
1234
            continue;
×
1235
        }
1236

1237
        auto is_user = user_marks.bv_tree.find(cl) != user_marks.bv_tree.end();
6✔
1238
        auto is_sticky
1239
            = sticky_marks.bv_tree.find(cl) != sticky_marks.bv_tree.end();
6✔
1240

1241
        // Use part_name "" for user marks, null for sticky-only
1242
        if (is_user) {
6✔
1243
            if (sqlite3_bind_text(stmt, 5, "", 0, SQLITE_TRANSIENT)
3✔
1244
                != SQLITE_OK)
3✔
1245
            {
UNCOV
1246
                log_error("could not bind part name -- %s", sqlite3_errmsg(db));
×
UNCOV
1247
                return;
×
1248
            }
1249
        } else {
1250
            sqlite3_bind_null(stmt, 5);
3✔
1251
        }
1252

1253
        if (sqlite3_bind_int(stmt, 10, is_sticky ? 1 : 0) != SQLITE_OK) {
6✔
UNCOV
1254
            log_error("could not bind sticky -- %s", sqlite3_errmsg(db));
×
UNCOV
1255
            return;
×
1256
        }
1257

1258
        if (sqlite3_step(stmt) != SQLITE_DONE) {
6✔
UNCOV
1259
            log_error("could not execute bookmark insert statement -- %s",
×
1260
                      sqlite3_errmsg(db));
1261
            return;
×
1262
        }
1263

1264
        marked_session_lines.emplace_back(lf->original_line_time(line_iter),
6✔
1265
                                          lf->get_format_ptr()->get_name(),
6✔
1266
                                          line_hash);
1267

1268
        sqlite3_reset(stmt);
6✔
1269
    }
6✔
1270
}
42✔
1271

1272
static void
1273
save_meta_bookmarks(sqlite3* db, sqlite3_stmt* stmt, logfile* lf)
41✔
1274
{
1275
    for (const auto& bm_pair : lf->get_bookmark_metadata()) {
65✔
1276
        auto cl = content_line_t(bm_pair.first);
24✔
1277
        sqlite3_clear_bindings(stmt);
24✔
1278

1279
        auto line_iter = lf->begin() + cl;
24✔
1280
        auto fr = lf->get_file_range(line_iter, false);
24✔
1281
        auto read_result = lf->read_range(fr);
24✔
1282

1283
        if (read_result.isErr()) {
24✔
UNCOV
1284
            continue;
×
1285
        }
1286

1287
        auto line_hash = read_result
1288
                             .map([cl](auto sbr) {
48✔
1289
                                 return hasher()
48✔
1290
                                     .update(sbr.get_data(), sbr.length())
24✔
1291
                                     .update(cl)
24✔
1292
                                     .to_string();
48✔
1293
                             })
1294
                             .unwrap();
24✔
1295

1296
        if (bind_values(stmt,
72✔
1297
                        lf->original_line_time(line_iter),
1298
                        lf->get_format()->get_name(),
48✔
1299
                        line_hash,
1300
                        lnav_data.ld_session_time)
1301
            != SQLITE_OK)
24✔
1302
        {
UNCOV
1303
            continue;
×
1304
        }
1305

1306
        const auto& line_meta = bm_pair.second;
24✔
1307
        if (line_meta.empty(bookmark_metadata::categories::session)) {
24✔
1308
            continue;
17✔
1309
        }
1310

1311
        if (line_meta.bm_name_source == bookmark_metadata::meta_source::user) {
7✔
1312
            if (sqlite3_bind_text(stmt,
7✔
1313
                                  5,
1314
                                  line_meta.bm_name.c_str(),
1315
                                  line_meta.bm_name.length(),
7✔
1316
                                  SQLITE_TRANSIENT)
1317
                != SQLITE_OK)
7✔
1318
            {
UNCOV
1319
                log_error("could not bind part name -- %s", sqlite3_errmsg(db));
×
UNCOV
1320
                return;
×
1321
            }
1322
        }
1323

1324
        if (sqlite3_bind_text(stmt,
7✔
1325
                              6,
1326
                              line_meta.bm_comment.c_str(),
1327
                              line_meta.bm_comment.length(),
7✔
1328
                              SQLITE_TRANSIENT)
1329
            != SQLITE_OK)
7✔
1330
        {
UNCOV
1331
            log_error("could not bind comment -- %s", sqlite3_errmsg(db));
×
UNCOV
1332
            return;
×
1333
        }
1334

1335
        std::string tags;
7✔
1336

1337
        {
1338
            yajlpp_gen gen;
7✔
1339

1340
            yajl_gen_config(gen, yajl_gen_beautify, false);
7✔
1341

1342
            {
1343
                yajlpp_array arr(gen);
7✔
1344

1345
                for (const auto& entry : line_meta.bm_tags) {
11✔
1346
                    if (entry.te_source == bookmark_metadata::meta_source::user)
4✔
1347
                    {
1348
                        arr.gen(entry.te_tag);
4✔
1349
                    }
1350
                }
1351
            }
7✔
1352

1353
            tags = gen.to_string_fragment().to_string();
7✔
1354

1355
            if (tags == "[]") {
7✔
1356
                tags.clear();
3✔
1357
            }
1358
        }
7✔
1359

1360
        if (sqlite3_bind_text(
7✔
1361
                stmt, 7, tags.c_str(), tags.length(), SQLITE_TRANSIENT)
7✔
1362
            != SQLITE_OK)
7✔
1363
        {
UNCOV
1364
            log_error("could not bind tags -- %s", sqlite3_errmsg(db));
×
UNCOV
1365
            return;
×
1366
        }
1367

1368
        if (!line_meta.bm_annotations.la_pairs.empty()) {
7✔
1369
            auto anno_str = logmsg_annotations_handlers.to_string(
1370
                line_meta.bm_annotations);
1✔
1371

1372
            if (sqlite3_bind_text(stmt,
1✔
1373
                                  8,
1374
                                  anno_str.c_str(),
1375
                                  anno_str.length(),
1✔
1376
                                  SQLITE_TRANSIENT)
1377
                != SQLITE_OK)
1✔
1378
            {
UNCOV
1379
                log_error("could not bind annotations -- %s",
×
1380
                          sqlite3_errmsg(db));
UNCOV
1381
                return;
×
1382
            }
1383
        } else {
1✔
1384
            sqlite3_bind_null(stmt, 8);
6✔
1385
        }
1386

1387
        if (line_meta.bm_opid.empty()) {
7✔
1388
            sqlite3_bind_null(stmt, 9);
7✔
1389
        } else {
UNCOV
1390
            bind_to_sqlite(stmt, 9, line_meta.bm_opid);
×
1391
        }
1392

1393
        sqlite3_bind_int(stmt, 10, 0);
7✔
1394

1395
        if (sqlite3_step(stmt) != SQLITE_DONE) {
7✔
UNCOV
1396
            log_error("could not execute bookmark insert statement -- %s",
×
1397
                      sqlite3_errmsg(db));
UNCOV
1398
            return;
×
1399
        }
1400

1401
        marked_session_lines.emplace_back(lf->original_line_time(line_iter),
7✔
1402
                                          lf->get_format_ptr()->get_name(),
7✔
1403
                                          line_hash);
1404

1405
        sqlite3_reset(stmt);
7✔
1406
    }
41✔
1407
}
1408

1409
static void
1410
save_text_bookmarks(sqlite3* db)
42✔
1411
{
1412
    auto& tss = lnav_data.ld_text_source;
42✔
1413

1414
    if (tss.empty()) {
42✔
1415
        return;
36✔
1416
    }
1417

1418
    tss.copy_bookmarks_to_current_file();
6✔
1419

1420
    auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
6✔
1421

1422
    if (sqlite3_prepare_v2(db,
6✔
1423
                           "DELETE FROM text_bookmarks2 WHERE session_time = ?",
1424
                           -1,
1425
                           stmt.out(),
1426
                           nullptr)
1427
        != SQLITE_OK)
6✔
1428
    {
UNCOV
1429
        log_error("could not prepare text_bookmarks delete -- %s",
×
1430
                  sqlite3_errmsg(db));
UNCOV
1431
        return;
×
1432
    }
1433
    sqlite3_bind_int64(stmt.in(), 1, lnav_data.ld_session_load_time);
6✔
1434
    if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
6✔
1435
        log_error("could not execute text_bookmarks delete -- %s",
×
1436
                  sqlite3_errmsg(db));
UNCOV
1437
        return;
×
1438
    }
1439

1440
    if (sqlite3_prepare_v2(db,
6✔
1441
                           "REPLACE INTO text_bookmarks2"
1442
                           " (file_path, line_number, line_hash, mark_type,"
1443
                           "  session_time, anchor, anchor_offset)"
1444
                           " VALUES (?, ?, ?, ?, ?, ?, ?)",
1445
                           -1,
1446
                           stmt.out(),
1447
                           nullptr)
1448
        != SQLITE_OK)
6✔
1449
    {
UNCOV
1450
        log_error("could not prepare text_bookmarks replace statement -- %s",
×
1451
                  sqlite3_errmsg(db));
UNCOV
1452
        return;
×
1453
    }
1454

1455
    for (const auto& fvs : tss.get_file_states()) {
12✔
1456
        auto& lf = fvs->fvs_file;
6✔
1457
        if (lf == nullptr || lf->size() == 0) {
6✔
UNCOV
1458
            continue;
×
1459
        }
1460

1461
        auto file_path = lf->get_path_for_key().string();
6✔
1462
        auto line_count = static_cast<int>(lf->size());
6✔
1463

1464
        static const bookmark_type_t* SAVE_TYPES[] = {
1465
            &textview_curses::BM_USER,
1466
            &textview_curses::BM_STICKY,
1467
        };
1468

1469
        for (const auto* bm_type : SAVE_TYPES) {
18✔
1470
            auto& bv = fvs->fvs_content_marks[bm_type];
12✔
1471
            if (bv.empty()) {
12✔
1472
                continue;
6✔
1473
            }
1474

1475
            for (const auto& vl : bv.bv_tree) {
12✔
1476
                if (static_cast<int>(vl) >= line_count) {
6✔
UNCOV
1477
                    continue;
×
1478
                }
1479

1480
                auto line_hash_opt
1481
                    = get_text_line_hash(lf.get(), static_cast<int>(vl));
6✔
1482
                if (!line_hash_opt) {
6✔
UNCOV
1483
                    continue;
×
1484
                }
1485
                auto line_hash = line_hash_opt.value();
6✔
1486

1487
                auto anchor_opt = fvs->anchor_for_row(
1488
                    tss.get_view_mode(), vis_line_t(static_cast<int>(vl)));
6✔
1489
                std::optional<int64_t> anchor_offset_opt;
6✔
1490
                if (anchor_opt.has_value()) {
6✔
1491
                    auto anchor_row_opt = fvs->row_for_anchor(
6✔
1492
                        tss.get_view_mode(), anchor_opt.value());
3✔
1493
                    if (anchor_row_opt.has_value()) {
3✔
UNCOV
1494
                        anchor_offset_opt = static_cast<int64_t>(vl)
×
1495
                            - static_cast<int64_t>(anchor_row_opt.value());
3✔
1496
                    }
1497
                }
1498

1499
                sqlite3_clear_bindings(stmt.in());
6✔
1500
                bind_to_sqlite(stmt.in(), 1, file_path);
6✔
1501
                sqlite3_bind_int(stmt.in(), 2, static_cast<int>(vl));
6✔
1502
                bind_to_sqlite(stmt.in(), 3, line_hash);
6✔
1503
                bind_to_sqlite(stmt.in(), 4, bm_type->get_name());
6✔
1504
                sqlite3_bind_int64(stmt.in(), 5, lnav_data.ld_session_time);
6✔
1505
                bind_to_sqlite(stmt.in(), 6, anchor_opt);
6✔
1506
                bind_to_sqlite(stmt.in(), 7, anchor_offset_opt);
6✔
1507

1508
                if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
6✔
UNCOV
1509
                    log_error("could not execute text_bookmarks insert -- %s",
×
1510
                              sqlite3_errmsg(db));
UNCOV
1511
                    return;
×
1512
                }
1513

1514
                sqlite3_reset(stmt.in());
6✔
1515
            }
6✔
1516
        }
1517
    }
6✔
1518
}
6✔
1519

1520
static void
1521
load_text_bookmarks(sqlite3* db)
38✔
1522
{
1523
    static const char* const TEXT_BOOKMARK_STMT = R"(
1524
        SELECT line_number, line_hash, mark_type, session_time,
1525
               session_time=? AS same_session, anchor, anchor_offset
1526
        FROM text_bookmarks2
1527
        WHERE file_path = ?
1528
        ORDER BY same_session DESC, session_time DESC
1529
    )";
1530

1531
    auto& tss = lnav_data.ld_text_source;
38✔
1532
    auto& tc = lnav_data.ld_views[LNV_TEXT];
38✔
1533

1534
    if (tss.empty()) {
38✔
1535
        return;
32✔
1536
    }
1537

1538
    auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
6✔
1539
    if (sqlite3_prepare_v2(db, TEXT_BOOKMARK_STMT, -1, stmt.out(), nullptr)
6✔
1540
        != SQLITE_OK)
6✔
1541
    {
1542
        log_error("could not prepare text_bookmarks select -- %s",
×
1543
                  sqlite3_errmsg(db));
UNCOV
1544
        return;
×
1545
    }
1546

1547
    for (const auto& fvs : tss.get_file_states()) {
12✔
1548
        auto& lf = fvs->fvs_file;
6✔
1549
        if (lf == nullptr || lf->size() == 0) {
6✔
UNCOV
1550
            continue;
×
1551
        }
1552

1553
        auto file_path = lf->get_path_for_key().string();
6✔
1554
        auto line_count = static_cast<int>(lf->size());
6✔
1555
        sqlite3_reset(stmt.in());
6✔
1556
        sqlite3_clear_bindings(stmt.in());
6✔
1557
        sqlite3_bind_int64(stmt.in(), 1, lnav_data.ld_session_load_time);
6✔
1558
        bind_to_sqlite(stmt.in(), 2, file_path);
6✔
1559

1560
        size_t marked_count = 0;
6✔
1561
        size_t sticky_count = 0;
6✔
1562
        int64_t last_session_time = -1;
6✔
1563
        bool done = false;
6✔
1564
        while (!done) {
18✔
1565
            auto rc = sqlite3_step(stmt.in());
12✔
1566

1567
            switch (rc) {
12✔
1568
                case SQLITE_OK:
6✔
1569
                case SQLITE_DONE:
1570
                    done = true;
6✔
1571
                    break;
6✔
1572

1573
                case SQLITE_ROW: {
6✔
1574
                    auto line_number = sqlite3_column_int(stmt.in(), 0);
6✔
1575
                    const auto stored_hash
1576
                        = from_stmt<string_fragment>(stmt.in(), 1);
6✔
1577
                    const auto mark_type
1578
                        = from_stmt<string_fragment>(stmt.in(), 2);
6✔
1579
                    auto session_time = sqlite3_column_int64(stmt.in(), 3);
6✔
1580
                    const auto stored_anchor
1581
                        = from_stmt<string_fragment>(stmt.in(), 5);
6✔
1582
                    auto stored_anchor_offset
1583
                        = from_stmt<std::optional<int64_t>>(stmt.in(), 6);
6✔
1584

1585
                    if (last_session_time == -1) {
6✔
1586
                        last_session_time = session_time;
6✔
1587
                    } else if (last_session_time != session_time) {
×
UNCOV
1588
                        done = true;
×
UNCOV
1589
                        continue;
×
1590
                    }
1591

1592
                    auto bm_type_opt = bookmark_type_t::find_type(mark_type);
6✔
1593
                    if (!bm_type_opt) {
6✔
UNCOV
1594
                        log_warning(
×
1595
                            "unknown bookmark type '%.*s' in file %s -- "
1596
                            "skipping",
1597
                            mark_type.length(),
1598
                            mark_type.data(),
1599
                            file_path.c_str());
UNCOV
1600
                        continue;
×
1601
                    }
1602

1603
                    // Pick a pivot line: prefer the anchor's current
1604
                    // position plus the saved offset if the anchor
1605
                    // resolves; otherwise the stored line number.
1606
                    int pivot = line_number;
6✔
1607
                    if (!stored_anchor.empty()) {
6✔
1608
                        auto anchor_row_opt = fvs->row_for_anchor(
6✔
1609
                            tss.get_view_mode(), stored_anchor.to_string());
6✔
1610
                        if (anchor_row_opt.has_value()) {
3✔
1611
                            pivot = anchor_row_opt.value()
2✔
1612
                                + stored_anchor_offset.value_or(0);
2✔
1613
                        }
1614
                    }
1615

1616
                    // Scan outward from the pivot for a line whose hash
1617
                    // matches the stored hash.
1618
                    auto matched_line
1619
                        = lf.get()
6✔
1620
                        | lnav::itertools::middle_out(
6✔
1621
                              pivot, get_text_line_hash, stored_hash);
6✔
1622

1623
                    int final_line;
1624
                    if (matched_line) {
6✔
1625
                        final_line = matched_line.value();
5✔
1626
                    } else if (pivot >= 0 && pivot < line_count) {
1✔
1627
                        // Best effort: land on the pivot even though
1628
                        // the content has changed.
1629
                        log_warning(
1✔
1630
                            "text bookmark hash mismatch at %s:%d -- "
1631
                            "falling back to pivot line",
1632
                            file_path.c_str(),
1633
                            pivot);
1634
                        final_line = pivot;
1✔
1635
                    } else {
UNCOV
1636
                        continue;
×
1637
                    }
1638

1639
                    auto vl = vis_line_t(final_line);
6✔
1640
                    fvs->fvs_bookmarks[bm_type_opt.value()].insert_once(vl);
6✔
1641
                    fvs->fvs_content_marks[bm_type_opt.value()].insert_once(
6✔
1642
                        static_cast<uint32_t>(final_line));
1643
                    if (bm_type_opt.value() == &textview_curses::BM_STICKY) {
6✔
1644
                        sticky_count += 1;
1✔
1645
                    } else {
1646
                        marked_count += 1;
5✔
1647
                    }
1648
                    break;
6✔
1649
                }
1650

UNCOV
1651
                default:
×
UNCOV
1652
                    log_error("text bookmark select error: %d -- %s",
×
1653
                              rc,
1654
                              sqlite3_errmsg(db));
UNCOV
1655
                    done = true;
×
UNCOV
1656
                    break;
×
1657
            }
1658
        }
1659
        if (marked_count > 0 || sticky_count > 0) {
6✔
1660
            log_info("  loaded %zu marked and %zu sticky lines from %s",
6✔
1661
                     marked_count,
1662
                     sticky_count,
1663
                     file_path.c_str());
1664
        }
1665
    }
6✔
1666

1667
    // Copy the front file's bookmarks into the textview
1668
    if (!tss.get_file_states().empty()) {
6✔
1669
        auto& front = tss.get_file_states().front();
6✔
1670
        tc.get_bookmarks()[&textview_curses::BM_USER]
6✔
1671
            = front->fvs_bookmarks[&textview_curses::BM_USER];
12✔
1672
        tc.get_bookmarks()[&textview_curses::BM_STICKY]
6✔
1673
            = front->fvs_bookmarks[&textview_curses::BM_STICKY];
12✔
1674
    }
1675

1676
    tc.reload_data();
6✔
1677
}
6✔
1678

1679
static void
1680
save_timeline_bookmarks(sqlite3* db)
42✔
1681
{
1682
    auto& tc = lnav_data.ld_views[LNV_TIMELINE];
42✔
1683
    auto* tss = static_cast<timeline_source*>(tc.get_sub_source());
42✔
1684

1685
    if (tss == nullptr) {
42✔
UNCOV
1686
        return;
×
1687
    }
1688

1689
    auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
42✔
1690

1691
    if (sqlite3_prepare_v2(db,
42✔
1692
                           "DELETE FROM timeline_bookmarks"
1693
                           " WHERE session_time = ?",
1694
                           -1,
1695
                           stmt.out(),
1696
                           nullptr)
1697
        != SQLITE_OK)
42✔
1698
    {
UNCOV
1699
        log_error("could not prepare timeline_bookmarks delete -- %s",
×
1700
                  sqlite3_errmsg(db));
UNCOV
1701
        return;
×
1702
    }
1703
    sqlite3_bind_int64(stmt.in(), 1, lnav_data.ld_session_load_time);
42✔
1704
    if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
42✔
UNCOV
1705
        log_error("could not execute timeline_bookmarks delete -- %s",
×
1706
                  sqlite3_errmsg(db));
UNCOV
1707
        return;
×
1708
    }
1709

1710
    if (sqlite3_prepare_v2(db,
42✔
1711
                           "REPLACE INTO timeline_bookmarks"
1712
                           " (row_type, row_name, mark_type, session_time)"
1713
                           " VALUES (?, ?, ?, ?)",
1714
                           -1,
1715
                           stmt.out(),
1716
                           nullptr)
1717
        != SQLITE_OK)
42✔
1718
    {
UNCOV
1719
        log_error(
×
1720
            "could not prepare timeline_bookmarks replace statement -- %s",
1721
            sqlite3_errmsg(db));
UNCOV
1722
        return;
×
1723
    }
1724

1725
    static const bookmark_type_t* SAVE_TYPES[] = {
1726
        &textview_curses::BM_USER,
1727
        &textview_curses::BM_STICKY,
1728
    };
1729

1730
    for (const auto* bm_type : SAVE_TYPES) {
126✔
1731
        const auto& bv = tc.get_bookmarks()[bm_type];
84✔
1732
        if (bv.empty()) {
84✔
1733
            continue;
82✔
1734
        }
1735

1736
        for (const auto& vl : bv.bv_tree) {
4✔
1737
            auto line = static_cast<size_t>(vl);
2✔
1738
            if (line >= tss->ts_time_order.size()) {
2✔
1739
                continue;
×
1740
            }
1741

1742
            const auto& row = *tss->ts_time_order[line];
2✔
1743

1744
            sqlite3_clear_bindings(stmt.in());
2✔
1745
            bind_to_sqlite(
2✔
1746
                stmt.in(), 1, timeline_source::row_type_to_string(row.or_type));
2✔
1747
            bind_to_sqlite(stmt.in(), 2, row.or_name.to_string());
2✔
1748
            bind_to_sqlite(stmt.in(), 3, bm_type->get_name());
2✔
1749
            sqlite3_bind_int64(stmt.in(), 4, lnav_data.ld_session_time);
2✔
1750

1751
            if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
2✔
1752
                log_error("could not execute timeline_bookmarks insert -- %s",
×
1753
                          sqlite3_errmsg(db));
UNCOV
1754
                return;
×
1755
            }
1756

1757
            sqlite3_reset(stmt.in());
2✔
1758
        }
1759
    }
1760
}
42✔
1761

1762
static void
1763
load_timeline_bookmarks(sqlite3* db)
38✔
1764
{
1765
    static const char* const TIMELINE_BOOKMARK_STMT = R"(
1766
        SELECT row_type, row_name, mark_type, session_time,
1767
               session_time=? AS same_session
1768
        FROM timeline_bookmarks
1769
        ORDER BY same_session DESC, session_time DESC
1770
    )";
1771

1772
    auto* tss = static_cast<timeline_source*>(
1773
        lnav_data.ld_views[LNV_TIMELINE].get_sub_source());
38✔
1774

1775
    if (tss == nullptr) {
38✔
UNCOV
1776
        return;
×
1777
    }
1778

1779
    auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
38✔
1780
    if (sqlite3_prepare_v2(db, TIMELINE_BOOKMARK_STMT, -1, stmt.out(), nullptr)
38✔
1781
        != SQLITE_OK)
38✔
1782
    {
UNCOV
1783
        log_debug("could not prepare timeline_bookmarks select -- %s",
×
1784
                  sqlite3_errmsg(db));
UNCOV
1785
        return;
×
1786
    }
1787

1788
    sqlite3_bind_int64(stmt.in(), 1, lnav_data.ld_session_load_time);
38✔
1789

1790
    int64_t last_session_time = -1;
38✔
1791
    bool done = false;
38✔
1792
    while (!done) {
78✔
1793
        auto rc = sqlite3_step(stmt.in());
40✔
1794

1795
        switch (rc) {
40✔
1796
            case SQLITE_OK:
38✔
1797
            case SQLITE_DONE:
1798
                done = true;
38✔
1799
                break;
38✔
1800

1801
            case SQLITE_ROW: {
2✔
1802
                auto* row_type_str
1803
                    = (const char*) sqlite3_column_text(stmt.in(), 0);
2✔
1804
                auto* row_name
1805
                    = (const char*) sqlite3_column_text(stmt.in(), 1);
2✔
1806
                auto mark_type = from_stmt<string_fragment>(stmt.in(), 2);
2✔
1807
                auto session_time = sqlite3_column_int64(stmt.in(), 3);
2✔
1808

1809
                if (last_session_time == -1) {
2✔
1810
                    last_session_time = session_time;
2✔
UNCOV
1811
                } else if (last_session_time != session_time) {
×
UNCOV
1812
                    done = true;
×
UNCOV
1813
                    continue;
×
1814
                }
1815

1816
                if (row_type_str == nullptr || row_name == nullptr) {
2✔
UNCOV
1817
                    continue;
×
1818
                }
1819

1820
                auto bm_type_opt = bookmark_type_t::find_type(mark_type);
2✔
1821
                if (!bm_type_opt) {
2✔
UNCOV
1822
                    continue;
×
1823
                }
1824

1825
                auto rt_opt
1826
                    = timeline_source::row_type_from_string(row_type_str);
2✔
1827
                if (!rt_opt) {
2✔
UNCOV
1828
                    continue;
×
1829
                }
1830

1831
                tss->ts_pending_bookmarks.emplace_back(
4✔
1832
                    timeline_source::pending_bookmark{
4✔
1833
                        rt_opt.value(),
2✔
1834
                        row_name,
1835
                        bm_type_opt.value(),
2✔
1836
                    });
1837
                break;
2✔
1838
            }
1839

UNCOV
1840
            default:
×
UNCOV
1841
                log_error("timeline bookmark select error: %d -- %s",
×
1842
                          rc,
1843
                          sqlite3_errmsg(db));
UNCOV
1844
                done = true;
×
UNCOV
1845
                break;
×
1846
        }
1847
    }
1848
}
38✔
1849

1850
static void
1851
save_time_bookmarks()
42✔
1852
{
1853
    auto_sqlite3 db;
42✔
1854
    auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
42✔
1855
    auto_mem<char, sqlite3_free> errmsg;
42✔
1856
    auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
42✔
1857

1858
    if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
42✔
UNCOV
1859
        log_error("unable to open bookmark DB -- %s", db_path.c_str());
×
UNCOV
1860
        return;
×
1861
    }
1862

1863
    if (sqlite3_exec(db.in(), META_TABLE_DEF, nullptr, nullptr, errmsg.out())
42✔
1864
        != SQLITE_OK)
42✔
1865
    {
UNCOV
1866
        log_error("unable to make bookmark table -- %s", errmsg.in());
×
1867
        return;
×
1868
    }
1869

1870
    if (sqlite3_exec(
42✔
1871
            db.in(), "BEGIN TRANSACTION", nullptr, nullptr, errmsg.out())
1872
        != SQLITE_OK)
42✔
1873
    {
UNCOV
1874
        log_error("unable to begin transaction -- %s", errmsg.in());
×
UNCOV
1875
        return;
×
1876
    }
1877

1878
    {
1879
        static const char* UPDATE_NETLOCS_STMT
1880
            = R"(REPLACE INTO recent_netlocs (netloc) VALUES (?))";
1881

1882
        std::set<std::string> netlocs;
42✔
1883

1884
        isc::to<tailer::looper&, services::remote_tailer_t>().send_and_wait(
42✔
1885
            [&netlocs](auto& tlooper) { netlocs = tlooper.active_netlocs(); });
84✔
1886

1887
        if (sqlite3_prepare_v2(
42✔
1888
                db.in(), UPDATE_NETLOCS_STMT, -1, stmt.out(), nullptr)
1889
            != SQLITE_OK)
42✔
1890
        {
UNCOV
1891
            log_error("could not prepare recent_netlocs statement -- %s",
×
1892
                      sqlite3_errmsg(db));
UNCOV
1893
            return;
×
1894
        }
1895

1896
        for (const auto& netloc : netlocs) {
42✔
UNCOV
1897
            bind_to_sqlite(stmt.in(), 1, netloc);
×
1898

UNCOV
1899
            if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
×
UNCOV
1900
                log_error("could not execute bookmark insert statement -- %s",
×
1901
                          sqlite3_errmsg(db));
UNCOV
1902
                return;
×
1903
            }
1904

1905
            sqlite3_reset(stmt.in());
×
1906
        }
1907
        recent_refs.rr_netlocs.insert(netlocs.begin(), netlocs.end());
42✔
1908
    }
42✔
1909

1910
    auto& lss = lnav_data.ld_log_source;
42✔
1911
    auto& bm = lss.get_user_bookmarks();
42✔
1912

1913
    if (sqlite3_prepare_v2(db.in(),
42✔
1914
                           "DELETE FROM bookmarks WHERE "
1915
                           " log_time = ? and log_format = ? and log_hash = ? "
1916
                           " and session_time = ?",
1917
                           -1,
1918
                           stmt.out(),
1919
                           nullptr)
1920
        != SQLITE_OK)
42✔
1921
    {
UNCOV
1922
        log_error("could not prepare bookmark delete statement -- %s",
×
1923
                  sqlite3_errmsg(db));
UNCOV
1924
        return;
×
1925
    }
1926

1927
    for (auto& marked_session_line : marked_session_lines) {
44✔
1928
        sqlite3_clear_bindings(stmt.in());
2✔
1929

1930
        if (bind_values(stmt,
4✔
1931
                        marked_session_line.sl_time,
1932
                        marked_session_line.sl_format_name,
1933
                        marked_session_line.sl_line_hash,
2✔
1934
                        lnav_data.ld_session_time)
1935
            != SQLITE_OK)
2✔
1936
        {
UNCOV
1937
            continue;
×
1938
        }
1939

1940
        if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
2✔
UNCOV
1941
            log_error("could not execute bookmark insert statement -- %s",
×
1942
                      sqlite3_errmsg(db));
UNCOV
1943
            return;
×
1944
        }
1945

1946
        sqlite3_reset(stmt.in());
2✔
1947
    }
1948

1949
    marked_session_lines.clear();
42✔
1950

1951
    if (sqlite3_prepare_v2(db.in(),
42✔
1952
                           "REPLACE INTO bookmarks"
1953
                           " (log_time, log_format, log_hash, session_time, "
1954
                           "part_name, comment, tags, annotations, log_opid,"
1955
                           " sticky)"
1956
                           " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1957
                           -1,
1958
                           stmt.out(),
1959
                           nullptr)
1960
        != SQLITE_OK)
42✔
1961
    {
UNCOV
1962
        log_error("could not prepare bookmark replace statement -- %s",
×
1963
                  sqlite3_errmsg(db));
1964
        return;
×
1965
    }
1966

1967
    {
1968
        for (auto file_iter = lnav_data.ld_log_source.begin();
42✔
1969
             file_iter != lnav_data.ld_log_source.end();
83✔
1970
             ++file_iter)
41✔
1971
        {
1972
            auto lf = (*file_iter)->get_file();
41✔
1973

1974
            if (lf == nullptr) {
41✔
UNCOV
1975
                continue;
×
1976
            }
1977
            if (lf->size() == 0) {
41✔
UNCOV
1978
                continue;
×
1979
            }
1980

1981
            content_line_t base_content_line;
41✔
1982
            base_content_line = lss.get_file_base_content_line(file_iter);
41✔
1983
            base_content_line
1984
                = content_line_t(base_content_line + lf->size() - 1);
41✔
1985

1986
            if (!bind_line(db.in(),
41✔
1987
                           stmt.in(),
1988
                           base_content_line,
1989
                           lnav_data.ld_session_time))
41✔
1990
            {
UNCOV
1991
                continue;
×
1992
            }
1993

1994
            if (sqlite3_bind_null(stmt.in(), 5) != SQLITE_OK) {
41✔
UNCOV
1995
                log_error("could not bind log hash -- %s",
×
1996
                          sqlite3_errmsg(db.in()));
UNCOV
1997
                return;
×
1998
            }
1999

2000
            sqlite3_bind_int(stmt.in(), 10, 0);
41✔
2001

2002
            if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
41✔
2003
                log_error("could not execute bookmark insert statement -- %s",
×
2004
                          sqlite3_errmsg(db));
2005
                return;
×
2006
            }
2007

2008
            sqlite3_reset(stmt.in());
41✔
2009
        }
41✔
2010
    }
2011

2012
    save_user_bookmarks(db.in(),
42✔
2013
                        stmt.in(),
2014
                        bm[&textview_curses::BM_USER],
2015
                        bm[&textview_curses::BM_STICKY]);
2016
    for (const auto& ldd : lss) {
83✔
2017
        auto* lf = ldd->get_file_ptr();
41✔
2018
        if (lf == nullptr) {
41✔
UNCOV
2019
            continue;
×
2020
        }
2021

2022
        save_meta_bookmarks(db.in(), stmt.in(), lf);
41✔
2023
    }
2024

2025
    if (sqlite3_prepare_v2(db.in(),
42✔
2026
                           "DELETE FROM time_offset WHERE "
2027
                           " log_time = ? and log_format = ? and log_hash = ? "
2028
                           " and session_time = ?",
2029
                           -1,
2030
                           stmt.out(),
2031
                           NULL)
2032
        != SQLITE_OK)
42✔
2033
    {
2034
        log_error("could not prepare time_offset delete statement -- %s",
×
2035
                  sqlite3_errmsg(db));
UNCOV
2036
        return;
×
2037
    }
2038

2039
    for (auto& offset_session_line : offset_session_lines) {
43✔
2040
        sqlite3_clear_bindings(stmt.in());
1✔
2041

2042
        if (bind_values(stmt,
2✔
2043
                        offset_session_line.sl_time,
2044
                        offset_session_line.sl_format_name,
2045
                        offset_session_line.sl_line_hash,
1✔
2046
                        lnav_data.ld_session_time)
2047
            != SQLITE_OK)
1✔
2048
        {
UNCOV
2049
            continue;
×
2050
        }
2051

2052
        if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
1✔
UNCOV
2053
            log_error("could not execute bookmark insert statement -- %s",
×
2054
                      sqlite3_errmsg(db));
UNCOV
2055
            return;
×
2056
        }
2057

2058
        sqlite3_reset(stmt.in());
1✔
2059
    }
2060

2061
    offset_session_lines.clear();
42✔
2062

2063
    if (sqlite3_prepare_v2(db.in(),
42✔
2064
                           "REPLACE INTO time_offset"
2065
                           " (log_time, log_format, log_hash, session_time, "
2066
                           "offset_sec, offset_usec)"
2067
                           " VALUES (?, ?, ?, ?, ?, ?)",
2068
                           -1,
2069
                           stmt.out(),
2070
                           nullptr)
2071
        != SQLITE_OK)
42✔
2072
    {
UNCOV
2073
        log_error("could not prepare time_offset replace statement -- %s",
×
2074
                  sqlite3_errmsg(db));
2075
        return;
×
2076
    }
2077

2078
    {
2079
        for (auto file_iter = lnav_data.ld_log_source.begin();
42✔
2080
             file_iter != lnav_data.ld_log_source.end();
83✔
2081
             ++file_iter)
41✔
2082
        {
2083
            auto lf = (*file_iter)->get_file();
41✔
2084
            if (lf == nullptr) {
41✔
UNCOV
2085
                continue;
×
2086
            }
2087
            if (lf->size() == 0) {
41✔
UNCOV
2088
                continue;
×
2089
            }
2090

2091
            if (bind_values(stmt,
123✔
2092
                            lf->original_line_time(lf->begin()),
2093
                            lf->get_format()->get_name(),
82✔
2094
                            lf->get_content_id(),
41✔
2095
                            lnav_data.ld_session_time)
2096
                != SQLITE_OK)
41✔
2097
            {
UNCOV
2098
                continue;
×
2099
            }
2100

2101
            if (sqlite3_bind_null(stmt.in(), 5) != SQLITE_OK) {
41✔
UNCOV
2102
                log_error("could not bind log hash -- %s",
×
2103
                          sqlite3_errmsg(db.in()));
2104
                return;
×
2105
            }
2106

2107
            if (sqlite3_bind_null(stmt.in(), 6) != SQLITE_OK) {
41✔
UNCOV
2108
                log_error("could not bind log hash -- %s",
×
2109
                          sqlite3_errmsg(db.in()));
2110
                return;
×
2111
            }
2112

2113
            if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
41✔
UNCOV
2114
                log_error("could not execute bookmark insert statement -- %s",
×
2115
                          sqlite3_errmsg(db));
UNCOV
2116
                return;
×
2117
            }
2118

2119
            sqlite3_reset(stmt.in());
41✔
2120
        }
41✔
2121
    }
2122

2123
    for (const auto& ls : lss) {
83✔
2124
        if (ls->get_file() == nullptr) {
41✔
2125
            continue;
38✔
2126
        }
2127

2128
        const auto lf = ls->get_file();
41✔
2129
        if (!lf->is_time_adjusted()) {
41✔
2130
            continue;
38✔
2131
        }
2132

2133
        auto line_iter = lf->begin() + lf->get_time_offset_line();
3✔
2134
        auto offset = lf->get_time_offset();
3✔
2135

2136
        bind_values(stmt.in(),
6✔
2137
                    lf->original_line_time(line_iter),
2138
                    lf->get_format()->get_name(),
6✔
2139
                    lf->get_content_id(),
3✔
2140
                    lnav_data.ld_session_time,
2141
                    offset.tv_sec,
2142
                    offset.tv_usec);
2143

2144
        if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
3✔
UNCOV
2145
            log_error("could not execute bookmark insert statement -- %s",
×
2146
                      sqlite3_errmsg(db));
UNCOV
2147
            return;
×
2148
        }
2149

2150
        sqlite3_reset(stmt.in());
3✔
2151
    }
41✔
2152

2153
    log_info("saved %d log bookmarks", sqlite3_changes(db.in()));
42✔
2154
    save_text_bookmarks(db.in());
42✔
2155
    log_info("saved %d text bookmarks", sqlite3_changes(db.in()));
42✔
2156
    save_timeline_bookmarks(db.in());
42✔
2157
    log_info("saved %d timeline bookmarks", sqlite3_changes(db.in()));
42✔
2158

2159
    if (sqlite3_exec(db.in(), "COMMIT", nullptr, nullptr, errmsg.out())
42✔
2160
        != SQLITE_OK)
42✔
2161
    {
2162
        log_error("unable to begin transaction -- %s", errmsg.in());
×
UNCOV
2163
        return;
×
2164
    }
2165

2166
    if (sqlite3_exec(db.in(), BOOKMARK_LRU_STMT, nullptr, nullptr, errmsg.out())
42✔
2167
        != SQLITE_OK)
42✔
2168
    {
UNCOV
2169
        log_error("unable to delete old bookmarks -- %s", errmsg.in());
×
UNCOV
2170
        return;
×
2171
    }
2172
    auto bookmark_changes = sqlite3_changes(db.in());
42✔
2173
    if (bookmark_changes > 0) {
42✔
UNCOV
2174
        log_info("deleted %d old bookmarks", bookmark_changes);
×
2175
    }
2176

2177
    if (sqlite3_exec(db.in(), NETLOC_LRU_STMT, nullptr, nullptr, errmsg.out())
42✔
2178
        != SQLITE_OK)
42✔
2179
    {
UNCOV
2180
        log_error("unable to delete old netlocs -- %s", errmsg.in());
×
UNCOV
2181
        return;
×
2182
    }
2183
    auto netloc_changes = sqlite3_changes(db.in());
42✔
2184
    if (netloc_changes > 0) {
42✔
UNCOV
2185
        log_info("deleted %d old netlocs", netloc_changes);
×
2186
    }
2187

2188
    if (sqlite3_exec(
42✔
2189
            db.in(), TEXT_BOOKMARK_LRU_STMT, nullptr, nullptr, errmsg.out())
2190
        != SQLITE_OK)
42✔
2191
    {
UNCOV
2192
        log_error("unable to delete old text bookmarks -- %s", errmsg.in());
×
UNCOV
2193
        return;
×
2194
    }
2195
    auto text_bm_changes = sqlite3_changes(db.in());
42✔
2196
    if (text_bm_changes > 0) {
42✔
UNCOV
2197
        log_info("deleted %d old text bookmarks", text_bm_changes);
×
2198
    }
2199

2200
    if (sqlite3_exec(
42✔
2201
            db.in(), TIMELINE_BOOKMARK_LRU_STMT, nullptr, nullptr, errmsg.out())
2202
        != SQLITE_OK)
42✔
2203
    {
UNCOV
2204
        log_error("unable to delete old timeline bookmarks -- %s", errmsg.in());
×
UNCOV
2205
        return;
×
2206
    }
2207
    auto timeline_bm_changes = sqlite3_changes(db.in());
42✔
2208
    if (timeline_bm_changes > 0) {
42✔
UNCOV
2209
        log_info("deleted %d old timeline bookmarks", timeline_bm_changes);
×
2210
    }
2211
}
42✔
2212

2213
static void
2214
save_session_with_id(const std::string& session_id)
41✔
2215
{
2216
    auto_mem<FILE> file(fclose);
41✔
2217
    yajl_gen handle = nullptr;
41✔
2218

2219
    /* TODO: save the last search query */
2220

2221
    log_info("saving session with id: %s", session_id.c_str());
41✔
2222

2223
    auto view_base_name
2224
        = fmt::format(FMT_STRING("view-info-{}.ts{}.ppid{}.json"),
82✔
2225
                      session_id,
2226
                      lnav_data.ld_session_time,
2227
                      getppid());
41✔
2228
    auto view_file_name = lnav::paths::dotlnav() / view_base_name;
41✔
2229
    auto view_file_tmp_name = view_file_name.string() + ".tmp";
41✔
2230

2231
    if ((file = fopen(view_file_tmp_name.c_str(), "w")) == nullptr) {
41✔
UNCOV
2232
        perror("Unable to open session file");
×
2233
    } else if (nullptr == (handle = yajl_gen_alloc(nullptr))) {
41✔
UNCOV
2234
        perror("Unable to create yajl_gen object");
×
2235
    } else {
2236
        yajl_gen_config(
41✔
2237
            handle, yajl_gen_print_callback, yajl_writer, file.in());
2238

2239
        {
2240
            yajlpp_map root_map(handle);
41✔
2241

2242
            root_map.gen("save-time");
41✔
2243
            root_map.gen((long long) time(nullptr));
41✔
2244

2245
            root_map.gen("time-offset");
41✔
2246
            root_map.gen(lnav_data.ld_log_source.is_time_offset_enabled());
41✔
2247

2248
            root_map.gen("files");
41✔
2249

2250
            {
2251
                yajlpp_array file_list(handle);
41✔
2252

2253
                for (auto& ld_file_name :
41✔
2254
                     lnav_data.ld_active_files.fc_file_names)
129✔
2255
                {
2256
                    file_list.gen(ld_file_name.first);
47✔
2257
                }
2258
            }
41✔
2259

2260
            root_map.gen("file-states");
41✔
2261

2262
            {
2263
                yajlpp_map file_states(handle);
41✔
2264

2265
                for (auto& lf : lnav_data.ld_active_files.fc_files) {
88✔
2266
                    auto ld_opt = lnav_data.ld_log_source.find_data(lf);
47✔
2267

2268
                    file_states.gen(lf->get_filename().native());
47✔
2269

2270
                    {
2271
                        yajlpp_map file_state(handle);
47✔
2272

2273
                        file_state.gen("visible");
47✔
2274
                        file_state.gen(!ld_opt || ld_opt.value()->ld_visible);
47✔
2275
                    }
47✔
2276
                }
2277
            }
41✔
2278

2279
            root_map.gen("views");
41✔
2280

2281
            {
2282
                yajlpp_map top_view_map(handle);
41✔
2283

2284
                for (int lpc = 0; lpc < LNV__MAX; lpc++) {
410✔
2285
                    auto& tc = lnav_data.ld_views[lpc];
369✔
2286
                    auto* ta = dynamic_cast<text_anchors*>(tc.get_sub_source());
369✔
2287

2288
                    top_view_map.gen(lnav_view_strings[lpc]);
369✔
2289

2290
                    yajlpp_map view_map(handle);
369✔
2291

2292
                    view_map.gen("top_line");
369✔
2293

2294
                    if (tc.get_top() >= tc.get_top_for_last_row()) {
369✔
2295
                        view_map.gen(-1LL);
348✔
2296
                    } else {
2297
                        view_map.gen((long long) tc.get_top());
21✔
2298
                    }
2299

2300
                    if (tc.is_selectable() && tc.get_selection() >= 0_vl
456✔
2301
                        && tc.get_inner_height() > 0_vl
4✔
2302
                        && tc.get_selection() != tc.get_inner_height() - 1)
456✔
2303
                    {
2304
                        auto sel = tc.get_selection();
3✔
2305
                        if (sel) {
3✔
2306
                            view_map.gen("focused_line");
3✔
2307
                            view_map.gen((long long) sel.value());
3✔
2308

2309
                            if (ta != nullptr) {
3✔
2310
                                auto anchor_opt
2311
                                    = ta->anchor_for_row(sel.value());
1✔
2312
                                if (anchor_opt) {
1✔
2313
                                    view_map.gen("anchor");
1✔
2314
                                    view_map.gen(anchor_opt.value());
1✔
2315
                                    auto anchor_row_opt = ta->row_for_anchor(
2✔
2316
                                        anchor_opt.value());
1✔
2317
                                    if (anchor_row_opt) {
1✔
2318
                                        view_map.gen("anchor_offset");
1✔
2319
                                        view_map.gen(
1✔
2320
                                            (long long) (sel.value()
2✔
2321
                                                         - anchor_row_opt
1✔
2322
                                                               .value()));
1✔
2323
                                    }
2324
                                }
2325
                            }
1✔
2326
                        }
2327
                    }
2328

2329
                    view_map.gen("search");
369✔
2330
                    view_map.gen(lnav_data.ld_views[lpc].get_current_search());
369✔
2331

2332
                    view_map.gen("word_wrap");
369✔
2333
                    view_map.gen(tc.get_word_wrap());
369✔
2334

2335
                    auto* tss = tc.get_sub_source();
369✔
2336
                    if (tss == nullptr) {
369✔
2337
                        continue;
82✔
2338
                    }
2339

2340
                    view_map.gen("filtering");
287✔
2341
                    view_map.gen(tss->tss_apply_filters);
287✔
2342

2343
                    if (tss->get_min_log_level() != LEVEL_UNKNOWN) {
287✔
UNCOV
2344
                        view_map.gen("min_level");
×
UNCOV
2345
                        view_map.gen(level_names[tss->get_min_log_level()]);
×
2346
                    }
2347

2348
                    view_map.gen("commands");
287✔
2349
                    yajlpp_array cmd_array(handle);
287✔
2350

2351
                    tss->add_commands_for_session(
287✔
2352
                        [&](auto& cmd) { cmd_array.gen(cmd); });
306✔
2353
                }
369✔
2354
            }
41✔
2355
        }
41✔
2356

2357
        yajl_gen_clear(handle);
41✔
2358
        yajl_gen_free(handle);
41✔
2359

2360
        fclose(file.release());
41✔
2361

2362
        log_perror(rename(view_file_tmp_name.c_str(), view_file_name.c_str()));
41✔
2363

2364
        log_info("Saved session: %s", view_file_name.c_str());
41✔
2365
    }
2366
}
41✔
2367

2368
void
2369
save_session()
42✔
2370
{
2371
    if (lnav_data.ld_flags.is_set<lnav_flags::secure_mode>()) {
42✔
UNCOV
2372
        log_info("secure mode is enabled, not saving session");
×
UNCOV
2373
        return;
×
2374
    }
2375

2376
    static auto op = lnav_operation{"save_session"};
42✔
2377

2378
    auto op_guard = lnav_opid_guard::internal(op);
42✔
2379

2380
    log_debug("BEGIN save_session");
42✔
2381
    save_time_bookmarks();
42✔
2382

2383
    const auto opt_session_id = compute_session_id();
42✔
2384
    opt_session_id | [](auto& session_id) { save_session_with_id(session_id); };
82✔
2385
    for (const auto& pair : lnav_data.ld_session_id) {
47✔
2386
        if (opt_session_id && pair.first == opt_session_id.value()) {
5✔
2387
            continue;
4✔
2388
        }
2389
        save_session_with_id(pair.first);
1✔
2390
    }
2391
    log_debug("END save_session");
42✔
2392
}
42✔
2393

2394
void
2395
reset_session()
11✔
2396
{
2397
    log_info("reset session: time=%lld", lnav_data.ld_session_time);
11✔
2398

2399
    save_session();
11✔
2400

2401
    lnav_data.ld_session_time = time(nullptr);
11✔
2402
    session_data.sd_file_states.clear();
11✔
2403

2404
    for (auto& tc : lnav_data.ld_views) {
110✔
2405
        auto* ttt = dynamic_cast<text_time_translator*>(tc.get_sub_source());
99✔
2406
        auto& hmap = tc.get_highlights();
99✔
2407
        auto hl_iter = hmap.begin();
99✔
2408

2409
        while (hl_iter != hmap.end()) {
1,012✔
2410
            if (hl_iter->first.first != highlight_source_t::INTERACTIVE) {
913✔
2411
                ++hl_iter;
913✔
2412
            } else {
UNCOV
2413
                hmap.erase(hl_iter++);
×
2414
            }
2415
        }
2416

2417
        if (ttt != nullptr) {
99✔
2418
            ttt->clear_min_max_row_times();
66✔
2419
        }
2420
    }
2421

2422
    for (const auto& lf : lnav_data.ld_active_files.fc_files) {
26✔
2423
        lf->reset_state();
15✔
2424
    }
2425
    // Drop format-level user state (e.g. `:hide-fields` choices) so a
2426
    // fresh session starts without inherited hide state.
2427
    for (const auto& format : log_format::get_root_formats()) {
883✔
2428
        format->reset_user_field_state();
872✔
2429
    }
2430

2431
    lnav_data.ld_log_source.get_breakpoints().clear();
11✔
2432
    lnav_data.ld_log_source.lss_highlighters.clear();
11✔
2433
    lnav_data.ld_log_source.set_force_rebuild();
11✔
2434
    lnav_data.ld_log_source.set_marked_only(false);
11✔
2435
    lnav_data.ld_log_source.set_min_log_level(LEVEL_UNKNOWN);
11✔
2436
    lnav_data.ld_log_source.set_sql_filter("", nullptr);
22✔
2437
    lnav_data.ld_log_source.set_sql_marker("", nullptr);
11✔
2438
    lnav_data.ld_log_source.clear_bookmark_metadata();
11✔
2439
    rebuild_indexes(std::nullopt);
11✔
2440

2441
    lnav_data.ld_db_row_source.reset_user_state();
11✔
2442

2443
    auto* tss = static_cast<timeline_source*>(
2444
        lnav_data.ld_views[LNV_TIMELINE].get_sub_source());
11✔
2445
    if (tss != nullptr) {
11✔
2446
        tss->ts_hidden_row_types.clear();
11✔
2447
        tss->ts_pending_bookmarks.clear();
11✔
2448
        tss->clear_metrics();
11✔
2449
    }
2450

2451
    for (auto& tc : lnav_data.ld_views) {
110✔
2452
        auto* tss = tc.get_sub_source();
99✔
2453

2454
        if (tss == nullptr) {
99✔
2455
            continue;
22✔
2456
        }
2457
        tss->get_filters().clear_filters();
77✔
2458
        tss->tss_apply_filters = true;
77✔
2459
        tss->text_clear_marks(&textview_curses::BM_USER);
77✔
2460
        tss->text_clear_marks(&textview_curses::BM_STICKY);
77✔
2461
        tc.get_bookmarks()[&textview_curses::BM_USER].clear();
77✔
2462
        tc.get_bookmarks()[&textview_curses::BM_STICKY].clear();
77✔
2463
        tss->text_filters_changed();
77✔
2464
        tc.reload_data();
77✔
2465
    }
2466

2467
    for (auto& fvs : lnav_data.ld_text_source.get_file_states()) {
11✔
UNCOV
2468
        fvs->fvs_bookmarks.clear();
×
2469
    }
2470

2471
    lnav_data.ld_filter_view.reload_data();
11✔
2472
    lnav_data.ld_files_view.reload_data();
11✔
2473
    for (const auto& format : log_format::get_root_formats()) {
883✔
2474
        auto* elf = dynamic_cast<external_log_format*>(format.get());
872✔
2475

2476
        if (elf == nullptr) {
872✔
2477
            continue;
66✔
2478
        }
2479

2480
        bool changed = false;
806✔
2481
        for (const auto& vd : elf->elf_value_defs) {
11,839✔
2482
            if (vd.second->vd_meta.lvm_user_hidden) {
11,033✔
2483
                vd.second->vd_meta.lvm_user_hidden = std::nullopt;
9,432✔
2484
                changed = true;
9,432✔
2485
            }
2486
        }
2487
        if (changed) {
806✔
2488
            elf->elf_value_defs_state->vds_generation += 1;
806✔
2489
        }
2490
    }
2491
}
11✔
2492

2493
void
2494
lnav::session::apply_view_commands()
39✔
2495
{
2496
    static auto op = lnav_operation{__FUNCTION__};
39✔
2497

2498
    auto op_guard = lnav_opid_guard::internal(op);
39✔
2499

2500
    log_debug("applying view commands");
39✔
2501
    for (size_t view_index = 0; view_index < LNV__MAX; view_index++) {
390✔
2502
        const auto& vs = session_data.sd_view_states[view_index];
351✔
2503
        auto& tview = lnav_data.ld_views[view_index];
351✔
2504

2505
        log_debug("  view: %s", tview.get_title().c_str());
351✔
2506
        lnav::set::small<std::string> curr_cmds;
351✔
2507
        auto* tss = tview.get_sub_source();
351✔
2508
        if (tview.get_sub_source() != nullptr) {
351✔
2509
            tss->tss_apply_filters = vs.vs_filtering;
273✔
2510
            if (vs.vs_min_log_level) {
273✔
UNCOV
2511
                tss->set_min_log_level(vs.vs_min_log_level.value());
×
2512
            }
2513
            tss->add_commands_for_session([&](auto& cmd) {
273✔
2514
                auto cmd_sf = string_fragment::from_str(cmd)
5✔
2515
                                  .split_when(string_fragment::tag1{' '})
5✔
2516
                                  .first;
2517
                curr_cmds.insert(cmd_sf.to_string());
5✔
2518
            });
5✔
2519
        }
2520
        if (vs.vs_commands.empty()) {
351✔
2521
            continue;
341✔
2522
        }
2523
        auto pop_view = false;
10✔
2524
        if (lnav_data.ld_view_stack.top() != &tview) {
10✔
2525
            toggle_view(&tview);
×
2526
            pop_view = true;
×
2527
        }
2528
        for (const auto& cmdline : vs.vs_commands) {
21✔
2529
            auto cmdline_sf = string_fragment::from_str(cmdline);
11✔
2530
            auto [cmd_sf, _cmdline_rem]
11✔
2531
                = cmdline_sf.split_when(string_fragment::tag1{' '});
11✔
2532
            if (curr_cmds.contains(cmd_sf.to_string())) {
11✔
2533
                log_debug("view %s command '%.*s' already active",
3✔
2534
                          tview.get_title().c_str(),
2535
                          cmd_sf.length(),
2536
                          cmd_sf.data());
2537
                continue;
3✔
2538
            }
2539
            auto exec_cmd_res
2540
                = execute_command(lnav_data.ld_exec_context, cmdline);
8✔
2541
            if (exec_cmd_res.isOk()) {
8✔
2542
                log_info("Result: %s", exec_cmd_res.unwrap().c_str());
8✔
2543
            } else {
2544
                log_error("Result: %s",
×
2545
                          exec_cmd_res.unwrapErr()
2546
                              .to_attr_line()
2547
                              .get_string()
2548
                              .c_str());
2549
            }
2550
        }
8✔
2551
        if (pop_view) {
10✔
UNCOV
2552
            lnav_data.ld_view_stack.pop_back();
×
UNCOV
2553
            lnav_data.ld_view_stack.top() | [](auto* tc) {
×
2554
                // XXX
UNCOV
2555
                if (tc == &lnav_data.ld_views[LNV_TIMELINE]) {
×
UNCOV
2556
                    auto tss = tc->get_sub_source();
×
UNCOV
2557
                    tss->text_filters_changed();
×
UNCOV
2558
                    tc->reload_data();
×
2559
                }
2560
            };
2561
        }
2562
    }
351✔
2563
}
39✔
2564

2565
void
2566
lnav::session::restore_view_states()
39✔
2567
{
2568
    static auto op = lnav_operation{__FUNCTION__};
39✔
2569

2570
    auto op_guard = lnav_opid_guard::internal(op);
39✔
2571

2572
    log_debug("restoring view states");
39✔
2573
    for (size_t view_index = 0; view_index < LNV__MAX; view_index++) {
390✔
2574
        const auto& vs = session_data.sd_view_states[view_index];
351✔
2575
        auto& tview = lnav_data.ld_views[view_index];
351✔
2576
        auto* ta = dynamic_cast<text_anchors*>(tview.get_sub_source());
351✔
2577

2578
        if (!vs.vs_search.empty()) {
351✔
2579
            tview.execute_search(vs.vs_search);
×
UNCOV
2580
            tview.set_follow_search_for(-1, {});
×
2581
        }
2582
        tview.set_word_wrap(vs.vs_word_wrap);
351✔
2583
        auto has_loc = tview.get_selection().has_value();
351✔
2584
        if (!has_loc && vs.vs_top >= 0
87✔
2585
            && (view_index == LNV_LOG || tview.get_top() == 0_vl
438✔
2586
                || tview.get_top() == tview.get_top_for_last_row()))
351✔
2587
        {
2588
            log_info("restoring %s view top: %d",
20✔
2589
                     lnav_view_strings[view_index].data(),
2590
                     (int) vs.vs_top);
2591
            tview.set_top(vis_line_t(vs.vs_top), true);
20✔
2592
        }
2593
        if (!has_loc && vs.vs_selection) {
351✔
2594
            // Prefer the anchor + offset if one was saved and still
2595
            // resolves in the current document; otherwise fall back
2596
            // to the saved absolute line number.
2597
            auto target = vis_line_t(vs.vs_selection.value());
2✔
2598
            if (ta != nullptr && vs.vs_anchor && !vs.vs_anchor->empty()) {
2✔
UNCOV
2599
                auto row_opt = ta->row_for_anchor(vs.vs_anchor.value());
×
UNCOV
2600
                if (row_opt) {
×
2601
                    auto offset = vs.vs_anchor_offset.value_or(0);
×
UNCOV
2602
                    target = row_opt.value()
×
2603
                        + vis_line_t(static_cast<int>(offset));
×
2604
                }
2605
            }
2606
            auto max_line = std::max(tview.get_inner_height() - 1_vl, 0_vl);
2✔
2607
            target = std::clamp(target, 0_vl, max_line);
2✔
2608
            log_info("restoring %s view selection: %d",
2✔
2609
                     lnav_view_strings[view_index].data(),
2610
                     (int) target);
2611
            tview.set_selection(target);
2✔
2612
        }
2613
        auto sel = tview.get_selection();
351✔
2614
        if (!sel) {
351✔
2615
            auto height = tview.get_inner_height();
87✔
2616
            if (height == 0) {
87✔
2617
            } else if (view_index == LNV_TEXT) {
1✔
UNCOV
2618
                auto lf = lnav_data.ld_text_source.current_file();
×
UNCOV
2619
                if (lf != nullptr) {
×
UNCOV
2620
                    switch (lf->get_text_format().value_or(
×
UNCOV
2621
                        text_format_t::TF_BINARY))
×
2622
                    {
UNCOV
2623
                        case text_format_t::TF_PLAINTEXT:
×
2624
                        case text_format_t::TF_LOG: {
UNCOV
2625
                            if (height > 0_vl) {
×
UNCOV
2626
                                tview.set_selection(height - 1_vl);
×
2627
                            }
2628
                            break;
×
2629
                        }
UNCOV
2630
                        default:
×
2631
                            tview.set_selection(0_vl);
×
2632
                            break;
×
2633
                    }
2634
                }
2635
            } else if (view_index == LNV_LOG) {
1✔
2636
                tview.set_selection(height - 1_vl);
×
2637
            } else {
2638
                tview.set_selection(0_vl);
1✔
2639
            }
2640
        }
2641
        log_info("%s view actual top/selection: %d/%d",
351✔
2642
                 lnav_view_strings[view_index].data(),
2643
                 (int) tview.get_top(),
2644
                 (int) tview.get_selection().value_or(-1_vl));
2645
    }
2646
}
39✔
2647

2648
void
UNCOV
2649
lnav::session::regex101::insert_entry(const lnav::session::regex101::entry& ei)
×
2650
{
2651
    constexpr const char* STMT = R"(
×
2652
       INSERT INTO regex101_entries
2653
          (format_name, regex_name, permalink, delete_code)
2654
          VALUES (?, ?, ?, ?);
2655
)";
2656

2657
    auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
×
UNCOV
2658
    auto_sqlite3 db;
×
2659

UNCOV
2660
    if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
×
UNCOV
2661
        return;
×
2662
    }
2663

UNCOV
2664
    auto_mem<char, sqlite3_free> errmsg;
×
UNCOV
2665
    if (sqlite3_exec(db.in(), META_TABLE_DEF, nullptr, nullptr, errmsg.out())
×
2666
        != SQLITE_OK)
×
2667
    {
UNCOV
2668
        log_error("unable to make bookmark table -- %s", errmsg.in());
×
UNCOV
2669
        return;
×
2670
    }
2671

2672
    auto prep_res = prepare_stmt(db.in(),
2673
                                 STMT,
2674
                                 ei.re_format_name,
×
2675
                                 ei.re_regex_name,
×
UNCOV
2676
                                 ei.re_permalink,
×
UNCOV
2677
                                 ei.re_delete_code);
×
2678

UNCOV
2679
    if (prep_res.isErr()) {
×
2680
        return;
×
2681
    }
2682

UNCOV
2683
    auto ps = prep_res.unwrap();
×
2684

UNCOV
2685
    ps.execute();
×
2686
}
2687

2688
template<>
2689
struct from_sqlite<lnav::session::regex101::entry> {
2690
    lnav::session::regex101::entry operator()(int argc,
×
2691
                                              sqlite3_value** argv,
2692
                                              int argi)
2693
    {
2694
        return {
UNCOV
2695
            from_sqlite<std::string>()(argc, argv, argi + 0),
×
UNCOV
2696
            from_sqlite<std::string>()(argc, argv, argi + 1),
×
UNCOV
2697
            from_sqlite<std::string>()(argc, argv, argi + 2),
×
2698
            from_sqlite<std::string>()(argc, argv, argi + 3),
×
2699
        };
2700
    }
2701
};
2702

2703
Result<std::vector<lnav::session::regex101::entry>, std::string>
UNCOV
2704
lnav::session::regex101::get_entries()
×
2705
{
2706
    constexpr const char* STMT = R"(
×
2707
       SELECT * FROM regex101_entries;
2708
)";
2709

2710
    auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
×
UNCOV
2711
    auto_sqlite3 db;
×
2712

2713
    if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
×
2714
        return Err(std::string());
×
2715
    }
2716

UNCOV
2717
    auto_mem<char, sqlite3_free> errmsg;
×
2718
    if (sqlite3_exec(db.in(), META_TABLE_DEF, nullptr, nullptr, errmsg.out())
×
2719
        != SQLITE_OK)
×
2720
    {
2721
        log_error("unable to make bookmark table -- %s", errmsg.in());
×
UNCOV
2722
        return Err(std::string(errmsg));
×
2723
    }
2724

UNCOV
2725
    auto ps = TRY(prepare_stmt(db.in(), STMT));
×
2726
    bool done = false;
×
UNCOV
2727
    std::vector<entry> retval;
×
2728

UNCOV
2729
    while (!done) {
×
UNCOV
2730
        auto fetch_res = ps.fetch_row<entry>();
×
2731

UNCOV
2732
        if (fetch_res.is<prepared_stmt::fetch_error>()) {
×
UNCOV
2733
            return Err(fetch_res.get<prepared_stmt::fetch_error>().fe_msg);
×
2734
        }
2735

UNCOV
2736
        fetch_res.match(
×
UNCOV
2737
            [&done](const prepared_stmt::end_of_rows&) { done = true; },
×
UNCOV
2738
            [](const prepared_stmt::fetch_error&) {},
×
UNCOV
2739
            [&retval](entry en) { retval.emplace_back(en); });
×
2740
    }
UNCOV
2741
    return Ok(retval);
×
2742
}
2743

2744
void
UNCOV
2745
lnav::session::regex101::delete_entry(const std::string& format_name,
×
2746
                                      const std::string& regex_name)
2747
{
UNCOV
2748
    constexpr const char* STMT = R"(
×
2749
       DELETE FROM regex101_entries WHERE
2750
          format_name = ? AND regex_name = ?;
2751
)";
2752

UNCOV
2753
    auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
×
UNCOV
2754
    auto_sqlite3 db;
×
2755

UNCOV
2756
    if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
×
UNCOV
2757
        return;
×
2758
    }
2759

UNCOV
2760
    auto prep_res = prepare_stmt(db.in(), STMT, format_name, regex_name);
×
2761

UNCOV
2762
    if (prep_res.isErr()) {
×
UNCOV
2763
        return;
×
2764
    }
2765

UNCOV
2766
    auto ps = prep_res.unwrap();
×
2767

UNCOV
2768
    ps.execute();
×
2769
}
2770

2771
lnav::session::regex101::get_result_t
UNCOV
2772
lnav::session::regex101::get_entry(const std::string& format_name,
×
2773
                                   const std::string& regex_name)
2774
{
UNCOV
2775
    constexpr const char* STMT = R"(
×
2776
       SELECT * FROM regex101_entries WHERE
2777
          format_name = ? AND regex_name = ?;
2778
    )";
2779

UNCOV
2780
    auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
×
UNCOV
2781
    auto_sqlite3 db;
×
2782

UNCOV
2783
    if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
×
UNCOV
2784
        return error{std::string()};
×
2785
    }
2786

UNCOV
2787
    auto_mem<char, sqlite3_free> errmsg;
×
UNCOV
2788
    if (sqlite3_exec(db.in(), META_TABLE_DEF, nullptr, nullptr, errmsg.out())
×
UNCOV
2789
        != SQLITE_OK)
×
2790
    {
UNCOV
2791
        log_error("unable to make bookmark table -- %s", errmsg.in());
×
UNCOV
2792
        return error{std::string(errmsg)};
×
2793
    }
2794

UNCOV
2795
    auto prep_res = prepare_stmt(db.in(), STMT, format_name, regex_name);
×
UNCOV
2796
    if (prep_res.isErr()) {
×
UNCOV
2797
        return error{prep_res.unwrapErr()};
×
2798
    }
2799

UNCOV
2800
    auto ps = prep_res.unwrap();
×
UNCOV
2801
    return ps.fetch_row<entry>().match(
×
UNCOV
2802
        [](const prepared_stmt::fetch_error& fe) -> get_result_t {
×
UNCOV
2803
            return error{fe.fe_msg};
×
2804
        },
UNCOV
2805
        [](const prepared_stmt::end_of_rows&) -> get_result_t {
×
UNCOV
2806
            return no_entry{};
×
2807
        },
UNCOV
2808
        [](const entry& en) -> get_result_t { return en; });
×
2809
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc