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

mendersoftware / mender / 2270999871

19 Jan 2026 09:47AM UTC coverage: 81.351% (-0.02%) from 81.369%
2270999871

push

gitlab-ci

lluiscampos
ci: Add an explicit job for testing boost download

As long as we support this option in the build system is good to be
tested in CI.

Before it was indirectly tested on `test:backward-compat`.

Ticket: MEN-8687

Signed-off-by: Lluis Campos <lluis.campos@northern.tech>

8794 of 10810 relevant lines covered (81.35%)

20323.46 hits per line

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

83.47
/src/common/processes/platform/tiny_process_library/tiny_process_library.cpp
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
#include <common/processes.hpp>
16

17
#include <string>
18
#include <string_view>
19

20
#include <common/events_io.hpp>
21
#include <common/io.hpp>
22
#include <common/log.hpp>
23
#include <common/path.hpp>
24

25
using namespace std;
26

27
namespace mender {
28
namespace common {
29
namespace processes {
30

31
const chrono::seconds MAX_TERMINATION_TIME(10);
32

33
namespace io = mender::common::io;
34
namespace log = mender::common::log;
35
namespace path = mender::common::path;
36

37
class ProcessReaderFunctor {
7,328✔
38
public:
39
        void operator()(const char *bytes, size_t n);
40

41
        // Note: ownership is not held here, but in Process.
42
        int fd_;
43

44
        OutputCallback callback_;
45
};
46

47
Process::Process(const vector<string> &args) :
5,206✔
48
        args_ {args},
2,603✔
49
        max_termination_time_ {MAX_TERMINATION_TIME} {
2,603✔
50
        async_wait_data_ = make_shared<AsyncWaitData>();
2,603✔
51
}
5,206✔
52

53
Process::~Process() {
6,039✔
54
        {
55
                unique_lock lock(async_wait_data_->data_mutex);
2,603✔
56
                // DoCancel() requires being locked.
57
                DoCancel();
2,603✔
58
        }
2,603✔
59

60
        EnsureTerminated();
2,603✔
61

62
        if (stdout_pipe_ >= 0) {
2,603✔
63
                close(stdout_pipe_);
×
64
        }
65
        if (stderr_pipe_ >= 0) {
2,603✔
66
                close(stderr_pipe_);
×
67
        }
68
}
8,642✔
69

70
error::Error Process::Start(OutputCallback stdout_callback, OutputCallback stderr_callback) {
2,549✔
71
        if (proc_) {
2,549✔
72
                return MakeError(ProcessAlreadyStartedError, "Cannot start process");
×
73
        }
74

75
        // Tiny-process-library doesn't give a good error if the command isn't found (just returns
76
        // exit code 1). If the path is absolute, it's pretty easy to check if it exists. This won't
77
        // cover all errors (non-absolute or unset executable bit, for example), but helps a little,
78
        // at least.
79
        if (args_.size() > 0 && path::IsAbsolute(args_[0])) {
2,549✔
80
                ifstream f(args_[0]);
1,882✔
81
                if (!f.good()) {
1,882✔
82
                        int errnum = errno;
1✔
83
                        return error::Error(
84
                                generic_category().default_error_condition(errnum), "Cannot launch " + args_[0]);
2✔
85
                }
86
        }
1,882✔
87

88
        OutputCallback maybe_stdout_callback;
2,548✔
89
        if (stdout_pipe_ >= 0 || stdout_callback) {
2,548✔
90
                maybe_stdout_callback = ProcessReaderFunctor {stdout_pipe_, stdout_callback};
3,664✔
91
        }
92
        OutputCallback maybe_stderr_callback;
2,548✔
93
        if (stderr_pipe_ >= 0 || stderr_callback) {
2,548✔
94
                maybe_stderr_callback = ProcessReaderFunctor {stderr_pipe_, stderr_callback};
3,664✔
95
        }
96

97
        proc_ =
98
                make_unique<tpl::Process>(args_, work_dir_, maybe_stdout_callback, maybe_stderr_callback);
5,096✔
99

100
        if (proc_->get_id() == -1) {
2,548✔
101
                proc_.reset();
102
                return MakeError(
103
                        ProcessesErrorCode::SpawnError,
104
                        "Failed to spawn '" + (args_.size() >= 1 ? args_[0] : "<null>") + "'");
×
105
        }
106

107
        SetupAsyncWait();
2,548✔
108

109
        return error::NoError;
2,548✔
110
}
111

112
error::Error Process::Run() {
629✔
113
        auto err = Start();
1,258✔
114
        if (err != error::NoError) {
629✔
115
                log::Error(err.String());
×
116
        }
117
        return Wait();
1,258✔
118
}
119

120
static error::Error ErrorBasedOnExitStatus(int exit_status) {
4,637✔
121
        if (exit_status != 0) {
4,637✔
122
                return MakeError(
123
                        NonZeroExitStatusError, "Process exited with status " + to_string(exit_status));
954✔
124
        } else {
125
                return error::NoError;
4,160✔
126
        }
127
}
128

129
error::Error Process::Wait() {
2,720✔
130
        if (proc_) {
2,720✔
131
                exit_status_ = future_exit_status_.get();
2,459✔
132
                proc_.reset();
133
                if (stdout_pipe_ >= 0) {
2,459✔
134
                        close(stdout_pipe_);
2✔
135
                        stdout_pipe_ = -1;
2✔
136
                }
137
                if (stderr_pipe_ >= 0) {
2,459✔
138
                        close(stderr_pipe_);
2✔
139
                        stderr_pipe_ = -1;
2✔
140
                }
141
        }
142
        return ErrorBasedOnExitStatus(exit_status_);
2,720✔
143
}
144

145
error::Error Process::Wait(chrono::nanoseconds timeout) {
90✔
146
        if (proc_) {
90✔
147
                auto result = future_exit_status_.wait_for(timeout);
90✔
148
                if (result == future_status::timeout) {
90✔
149
                        return error::Error(
150
                                make_error_condition(errc::timed_out), "Timed out while waiting for process");
×
151
                }
152
                AssertOrReturnError(result == future_status::ready);
90✔
153
                exit_status_ = future_exit_status_.get();
90✔
154
                proc_.reset();
155
                if (stdout_pipe_ >= 0) {
90✔
156
                        close(stdout_pipe_);
×
157
                        stdout_pipe_ = -1;
×
158
                }
159
                if (stderr_pipe_ >= 0) {
90✔
160
                        close(stderr_pipe_);
×
161
                        stderr_pipe_ = -1;
×
162
                }
163
        }
164
        return ErrorBasedOnExitStatus(exit_status_);
90✔
165
}
166

167
error::Error Process::AsyncWait(events::EventLoop &loop, AsyncWaitHandler handler) {
1,835✔
168
        unique_lock lock(async_wait_data_->data_mutex);
1,835✔
169

170
        if (async_wait_data_->handler) {
1,835✔
171
                return error::Error(make_error_condition(errc::operation_in_progress), "Cannot AsyncWait");
×
172
        }
173

174
        async_wait_data_->event_loop = &loop;
1,835✔
175
        async_wait_data_->handler = handler;
1,835✔
176

177
        if (async_wait_data_->process_ended) {
1,835✔
178
                // The process has already ended. Schedule the handler immediately.
179
                auto &async_wait_data = async_wait_data_;
180
                async_wait_data->event_loop->Post(
×
181
                        [async_wait_data, this]() { AsyncWaitInternalHandler(async_wait_data); });
×
182
        }
183

184
        return error::NoError;
1,835✔
185
}
1,835✔
186

187
error::Error Process::AsyncWait(
1,830✔
188
        events::EventLoop &loop, AsyncWaitHandler handler, chrono::nanoseconds timeout) {
189
        timeout_timer_.reset(new events::Timer(loop));
1,830✔
190

191
        auto err = AsyncWait(loop, handler);
1,830✔
192
        if (err != error::NoError) {
1,830✔
193
                return err;
×
194
        }
195

196
        timeout_timer_->AsyncWait(timeout, [this, handler](error::Error err) {
5,490✔
197
                // Move timer here so it gets destroyed after this handler.
198
                auto timer = std::move(timeout_timer_);
5✔
199
                // Cancel normal AsyncWait() (the process part of it).
200
                {
201
                        // DoCancel() requires being locked.
202
                        unique_lock lock(async_wait_data_->data_mutex);
5✔
203
                        DoCancel();
5✔
204
                }
5✔
205
                if (err != error::NoError) {
5✔
206
                        handler(err.WithContext("Process::Timer"));
×
207
                }
208

209
                handler(error::Error(make_error_condition(errc::timed_out), "Process::Timer"));
15✔
210
        });
5✔
211

212
        return error::NoError;
1,830✔
213
}
214

215
void Process::Cancel() {
7✔
216
        unique_lock lock(async_wait_data_->data_mutex);
7✔
217

218
        if (async_wait_data_->handler && !async_wait_data_->process_ended) {
7✔
219
                auto &async_wait_data = async_wait_data_;
220
                auto &handler = async_wait_data->handler;
1✔
221
                async_wait_data_->event_loop->Post([handler]() {
6✔
222
                        handler(error::Error(
1✔
223
                                make_error_condition(errc::operation_canceled), "Process::AsyncWait canceled"));
2✔
224
                });
1✔
225
        }
226

227
        // DoCancel() requires being locked.
228
        DoCancel();
7✔
229
}
7✔
230

231
void Process::DoCancel() {
2,615✔
232
        // Should already be locked by caller.
233
        //   unique_lock lock(async_wait_data_->data_mutex);
234

235
        timeout_timer_.reset();
236

237
        async_wait_data_->event_loop = nullptr;
2,615✔
238
        async_wait_data_->handler = nullptr;
2,615✔
239
        async_wait_data_->process_ended = false;
2,615✔
240
}
2,615✔
241

242
void Process::SetupAsyncWait() {
2,638✔
243
        future_exit_status_ = async(launch::async, [this]() -> int {
2,638✔
244
                // This function is executed in a separate thread, but the object is guaranteed to
245
                // still exist while we're in here (because we call `future_exit_status_.get()`
246
                // during destruction.
247

248
                auto status = proc_->get_exit_status();
2,638✔
249

250
                auto &async_wait_data = async_wait_data_;
2,638✔
251

252
                unique_lock lock(async_wait_data->data_mutex);
2,638✔
253

254
                if (async_wait_data->handler) {
2,638✔
255
                        async_wait_data_->event_loop->Post(
3,656✔
256
                                [async_wait_data, this]() { AsyncWaitInternalHandler(async_wait_data); });
10,966✔
257
                }
258

259
                async_wait_data->process_ended = true;
2,638✔
260

261
                return status;
2,638✔
262
        });
5,276✔
263
}
2,638✔
264

265
void Process::AsyncWaitInternalHandler(shared_ptr<AsyncWaitData> async_wait_data) {
1,827✔
266
        timeout_timer_.reset();
267

268
        // This function is meant to be executed on the event loop, and the object may have been
269
        // either cancelled or destroyed before we get here. By having our own copy of the
270
        // async_wait_data pointer pointer, it survives destruction, and we can test whether we
271
        // should still proceed here. So note the use of `async_wait_data` instead of
272
        // `async_wait_data_`.
273
        unique_lock lock(async_wait_data->data_mutex);
1,827✔
274

275
        if (async_wait_data->handler) {
1,827✔
276
                auto handler = async_wait_data->handler;
1,827✔
277

278
                // For next iteration.
279
                async_wait_data->event_loop = nullptr;
1,827✔
280
                async_wait_data->handler = nullptr;
1,827✔
281
                async_wait_data->process_ended = false;
1,827✔
282

283
                auto status = GetExitStatus();
1,827✔
284

285
                // Unlock in case the handler calls back into this object.
286
                lock.unlock();
1,827✔
287
                handler(ErrorBasedOnExitStatus(status));
3,654✔
288
        }
289
}
1,827✔
290

291
static void CollectLineData(
96✔
292
        string &trailing_line, vector<string> &lines, const char *bytes, size_t len) {
293
        auto bytes_view = string_view(bytes, len);
294
        size_t line_start_idx = 0;
295
        size_t line_end_idx = bytes_view.find("\n", 0);
96✔
296
        if ((trailing_line != "") && (line_end_idx != string_view::npos)) {
96✔
297
                lines.push_back(trailing_line + string(bytes_view, 0, line_end_idx));
×
298
                line_start_idx = line_end_idx + 1;
×
299
                line_end_idx = bytes_view.find("\n", line_start_idx);
×
300
                trailing_line = "";
×
301
        }
302

303
        while ((line_start_idx < (len - 1)) && (line_end_idx != string_view::npos)) {
268✔
304
                lines.push_back(string(bytes_view, line_start_idx, (line_end_idx - line_start_idx)));
172✔
305
                line_start_idx = line_end_idx + 1;
172✔
306
                line_end_idx = bytes_view.find("\n", line_start_idx);
172✔
307
        }
308

309
        if ((line_end_idx == string_view::npos) && (line_start_idx < len)) {
96✔
310
                trailing_line += string(bytes_view, line_start_idx, (len - line_start_idx));
4✔
311
        }
312
}
96✔
313

314
ExpectedLineData Process::GenerateLineData(chrono::nanoseconds timeout) {
90✔
315
        if (proc_) {
90✔
316
                return expected::unexpected(
×
317
                        MakeError(ProcessAlreadyStartedError, "Cannot generate line data"));
×
318
        }
319

320
        if (args_.size() == 0) {
90✔
321
                return expected::unexpected(MakeError(
×
322
                        ProcessesErrorCode::SpawnError, "No arguments given, cannot spawn a process"));
×
323
        }
324

325
        // Tiny-process-library doesn't give a good error if the command isn't found (just returns
326
        // exit code 1). If the path is absolute, it's pretty easy to check if it exists. This won't
327
        // cover all errors (non-absolute or unset executable bit, for example), but helps a little,
328
        // at least.
329
        if (path::IsAbsolute(args_[0])) {
90✔
330
                ifstream f(args_[0]);
84✔
331
                if (!f.good()) {
84✔
332
                        int errnum = errno;
×
333
                        return expected::unexpected(error::Error(
×
334
                                generic_category().default_error_condition(errnum), "Cannot launch " + args_[0]));
×
335
                }
336
        }
84✔
337

338
        string trailing_line;
339
        vector<string> ret;
340
        proc_ = make_unique<tpl::Process>(
90✔
341
                args_, work_dir_, [&trailing_line, &ret](const char *bytes, size_t len) {
180✔
342
                        CollectLineData(trailing_line, ret, bytes, len);
96✔
343
                });
96✔
344

345
        if (proc_->get_id() == -1) {
90✔
346
                proc_.reset();
347
                return expected::unexpected(MakeError(
×
348
                        ProcessesErrorCode::SpawnError,
349
                        "Failed to spawn '" + (args_.size() >= 1 ? args_[0] : "<null>") + "'"));
×
350
        }
351

352
        SetupAsyncWait();
90✔
353

354
        auto err = Wait(timeout);
90✔
355
        if (err != error::NoError) {
90✔
356
                return expected::unexpected(err);
2✔
357
        }
358

359
        if (trailing_line != "") {
89✔
360
                ret.push_back(trailing_line);
2✔
361
        }
362

363
        return ExpectedLineData(ret);
89✔
364
}
90✔
365

366
io::ExpectedAsyncReaderPtr Process::GetProcessReader(events::EventLoop &loop, int &pipe_ref) {
4✔
367
        if (proc_) {
4✔
368
                return expected::unexpected(
×
369
                        MakeError(ProcessAlreadyStartedError, "Cannot get process output"));
×
370
        }
371

372
        if (pipe_ref >= 0) {
4✔
373
                close(pipe_ref);
×
374
                pipe_ref = -1;
×
375
        }
376

377
        int fds[2];
378
        int ret = pipe(fds);
4✔
379
        if (ret < 0) {
4✔
380
                int err = errno;
×
381
                return expected::unexpected(error::Error(
×
382
                        generic_category().default_error_condition(err),
×
383
                        "Could not get process stdout reader"));
×
384
        }
385

386
        pipe_ref = fds[1];
4✔
387

388
        return make_shared<events::io::AsyncFileDescriptorReader>(loop, fds[0]);
8✔
389
}
390

391
io::ExpectedAsyncReaderPtr Process::GetAsyncStdoutReader(events::EventLoop &loop) {
2✔
392
        return GetProcessReader(loop, stdout_pipe_);
2✔
393
}
394

395
io::ExpectedAsyncReaderPtr Process::GetAsyncStderrReader(events::EventLoop &loop) {
2✔
396
        return GetProcessReader(loop, stderr_pipe_);
2✔
397
}
398

399
int Process::EnsureTerminated() {
2,628✔
400
        if (!proc_) {
2,628✔
401
                return exit_status_;
2,539✔
402
        }
403

404
        log::Info("Sending SIGTERM to PID " + to_string(proc_->get_id()));
89✔
405

406
        Terminate();
89✔
407

408
        auto result = future_exit_status_.wait_for(max_termination_time_);
89✔
409
        if (result == future_status::timeout) {
89✔
410
                log::Info("Sending SIGKILL to PID " + to_string(proc_->get_id()));
1✔
411
                Kill();
1✔
412
                result = future_exit_status_.wait_for(max_termination_time_);
1✔
413
                if (result != future_status::ready) {
1✔
414
                        // This should not be possible, SIGKILL always terminates.
415
                        log::Error(
×
416
                                "PID " + to_string(proc_->get_id()) + " still not terminated after SIGKILL.");
×
417
                        return -1;
×
418
                }
419
        }
420
        assert(result == future_status::ready);
421
        exit_status_ = future_exit_status_.get();
89✔
422

423
        log::Info(
89✔
424
                "PID " + to_string(proc_->get_id()) + " exited with status " + to_string(exit_status_));
178✔
425

426
        proc_.reset();
427

428
        return exit_status_;
89✔
429
}
430

431
void Process::Terminate() {
90✔
432
        if (proc_) {
90✔
433
                // At the time of writing, tiny-process-library kills using SIGINT and SIGTERM, for
434
                // `force = false/true`, respectively. But we want to kill with SIGTERM and SIGKILL,
435
                // because:
436
                //
437
                // 1. SIGINT is not meant to kill interactive processes, whereas SIGTERM is.
438
                // 2. SIGKILL is required in order to really force, since SIGTERM can be ignored by
439
                //    the process.
440
                //
441
                // If tiny-process-library is fixed, then this can be restored and the part below
442
                // removed.
443
                // proc_->kill(false);
444

445
                ::kill(proc_->get_id(), SIGTERM);
90✔
446
                ::kill(-proc_->get_id(), SIGTERM);
90✔
447
        }
448
}
90✔
449

450
void Process::Kill() {
2✔
451
        if (proc_) {
2✔
452
                // See comment in Terminate().
453
                // proc_->kill(true);
454

455
                ::kill(proc_->get_id(), SIGKILL);
2✔
456
                ::kill(-proc_->get_id(), SIGKILL);
2✔
457
        }
458
}
2✔
459

460
void ProcessReaderFunctor::operator()(const char *bytes, size_t n) {
1,044✔
461
        if (callback_) {
1,044✔
462
                callback_(bytes, n);
1,002✔
463
        }
464

465
        if (fd_ < 0) {
1,044✔
466
                return;
467
        }
468

469
        size_t written = 0;
470
        while (written < n) {
14✔
471
                auto ret = write(fd_, bytes, n);
8✔
472
                if (ret < 0) {
8✔
473
                        int err = errno;
2✔
474
                        log::Error(
2✔
475
                                string {"Error while writing process output to main thread: "} + strerror(err));
2✔
476
                        fd_ = -1;
2✔
477
                        return;
2✔
478
                }
479

480
                written += ret;
6✔
481
        }
482
}
483

484
} // namespace processes
485
} // namespace common
486
} // namespace mender
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