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

mendersoftware / mender / 2281564137

23 Jan 2026 10:59AM UTC coverage: 81.48% (+1.7%) from 79.764%
2281564137

push

gitlab-ci

michalkopczan
fix: Schedule next deployment poll if current one failed early causing no handler to be called

Ticket: MEN-9144
Changelog: Fix a hang when polling for deployment failed early causing no handler of API response
to be called. Added handler call for this case, causing the deployment polling
to continue.

Signed-off-by: Michal Kopczan <michal.kopczan@northern.tech>

1 of 1 new or added line in 1 file covered. (100.0%)

327 existing lines in 44 files now uncovered.

8839 of 10848 relevant lines covered (81.48%)

20226.53 hits per line

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

77.44
/src/mender-update/update_module/v3/platform/c++17/fs_operations.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 <mender-update/update_module/v3/update_module.hpp>
16

17
#include <cerrno>
18
#include <fstream>
19

20
#include <unistd.h>
21

22
#include <filesystem>
23

24
#include <client_shared/conf.hpp>
25
#include <common/events_io.hpp>
26
#include <common/io.hpp>
27
#include <common/log.hpp>
28
#include <common/path.hpp>
29
#include <mender-update/context.hpp>
30

31
namespace mender {
32
namespace update {
33
namespace update_module {
34
namespace v3 {
35

36
using namespace std;
37

38
namespace error = mender::common::error;
39
namespace events = mender::common::events;
40
namespace expected = mender::common::expected;
41
namespace conf = mender::client_shared::conf;
42
namespace context = mender::update::context;
43
namespace io = mender::common::io;
44
namespace log = mender::common::log;
45
namespace path = mender::common::path;
46

47
namespace fs = std::filesystem;
48

49
error::Error CreateDataFile(
1,296✔
50
        const fs::path &file_tree_path, const string &file_name, const string &data) {
51
        string fpath = (file_tree_path / file_name).string();
1,296✔
52
        auto ex_os = io::OpenOfstream(fpath);
1,296✔
53
        if (!ex_os) {
1,296✔
54
                return ex_os.error();
×
55
        }
56
        ofstream &os = ex_os.value();
1,296✔
57
        if (data != "") {
1,296✔
58
                auto err = io::WriteStringIntoOfstream(os, data);
1,027✔
59
                return err;
1,027✔
60
        }
61
        return error::NoError;
269✔
62
}
63

64
error::Error UpdateModule::PrepareFileTreeDeviceParts(const string &path) {
162✔
65
        // make sure all the required data can be gathered first before creating
66
        // directories and files
67
        auto ex_provides = ctx_.LoadProvides();
162✔
68
        if (!ex_provides) {
162✔
69
                return ex_provides.error();
×
70
        }
71

72
        auto ex_device_type = ctx_.GetDeviceType();
162✔
73
        if (!ex_device_type) {
162✔
74
                return ex_device_type.error();
×
75
        }
76

77
        const fs::path file_tree_path {path};
162✔
78

79
        const fs::path tmp_subdir_path = file_tree_path / "tmp";
162✔
80
        auto err = path::CreateDirectories(tmp_subdir_path.string());
162✔
81
        if (err != error::NoError) {
162✔
82
                return err;
×
83
        }
84

85
        auto provides = ex_provides.value();
162✔
86
        auto write_provides_into_file = [&](const string &key) {
324✔
87
                return CreateDataFile(
88
                        file_tree_path,
89
                        "current_" + key,
648✔
90
                        (provides.count(key) != 0) ? provides[key] + "\n" : "");
972✔
91
        };
162✔
92

93
        err = CreateDataFile(file_tree_path, "version", "3\n");
324✔
94
        if (err != error::NoError) {
162✔
95
                return err;
×
96
        }
97

98
        err = write_provides_into_file("artifact_name");
162✔
99
        if (err != error::NoError) {
162✔
100
                return err;
×
101
        }
102
        err = write_provides_into_file("artifact_group");
162✔
103
        if (err != error::NoError) {
162✔
104
                return err;
×
105
        }
106

107
        auto device_type = ex_device_type.value();
162✔
108
        err = CreateDataFile(file_tree_path, "current_device_type", device_type + "\n");
324✔
109
        if (err != error::NoError) {
162✔
110
                return err;
×
111
        }
112

113
        return error::NoError;
162✔
114
}
162✔
115

116
error::Error UpdateModule::CleanAndPrepareFileTree(
108✔
117
        const string &path, artifact::PayloadHeaderView &payload_meta_data) {
118
        const fs::path file_tree_path {path};
108✔
119

120
        std::error_code ec;
108✔
121
        fs::remove_all(file_tree_path, ec);
108✔
122
        if (ec) {
108✔
123
                return error::Error(
124
                        ec.default_error_condition(), "Could not clean File Tree for Update Module");
×
125
        }
126

127
        auto err = PrepareFileTreeDeviceParts(path);
108✔
128
        if (err != error::NoError) {
108✔
129
                return err;
×
130
        }
131

132
        //
133
        // Header
134
        //
135

136
        const fs::path header_subdir_path = file_tree_path / "header";
108✔
137
        err = path::CreateDirectories(header_subdir_path.string());
108✔
138
        if (err != error::NoError) {
108✔
139
                return err;
×
140
        }
141

142
        err = CreateDataFile(
108✔
143
                header_subdir_path, "artifact_group", payload_meta_data.header.artifact_group);
216✔
144
        if (err != error::NoError) {
108✔
145
                return err;
×
146
        }
147

148
        err =
149
                CreateDataFile(header_subdir_path, "artifact_name", payload_meta_data.header.artifact_name);
108✔
150
        if (err != error::NoError) {
108✔
151
                return err;
×
152
        }
153

154
        err = CreateDataFile(header_subdir_path, "payload_type", payload_meta_data.header.payload_type);
108✔
155
        if (err != error::NoError) {
108✔
156
                return err;
×
157
        }
158

159
        err = CreateDataFile(
216✔
160
                header_subdir_path, "header-info", payload_meta_data.header.header_info.verbatim.Dump());
216✔
161
        if (err != error::NoError) {
108✔
162
                return err;
×
163
        }
164

165
        err = CreateDataFile(
216✔
166
                header_subdir_path, "type-info", payload_meta_data.header.type_info.verbatim.Dump());
216✔
167
        if (err != error::NoError) {
108✔
168
                return err;
×
169
        }
170

171
        err =
172
                CreateDataFile(header_subdir_path, "meta-data", payload_meta_data.header.meta_data.Dump());
216✔
173
        if (err != error::NoError) {
108✔
174
                return err;
×
175
        }
176

177
        // Make sure all changes are permanent, even across spontaneous reboots. We don't want to
178
        // have half a tree when trying to recover from that.
179
        return path::DataSyncRecursively(path);
108✔
180
}
216✔
181

182
error::Error UpdateModule::EnsureRootfsImageFileTree(const string &path) {
54✔
183
        // Historical note: Versions of the client prior to 4.0 had the rootfs-image module built
184
        // in. Because of this it has no Update Module File Tree. So if we are upgrading, we might
185
        // hit an on-going upgrade without a File Tree. It's too late to create a complete one with
186
        // all the artifact content by the time we get here, but at least we can create one which
187
        // has the current Provides information, as well as a folder for the Update Module to run
188
        // in.
189
        ifstream payload_type(path::Join(path, "header", "payload_type"));
108✔
190
        if (payload_type.good()) {
54✔
191
                string type;
192
                payload_type >> type;
52✔
193
                if (payload_type.good() && type == "rootfs-image") {
52✔
194
                        // If we have a File Tree with the rootfs-image type, we assume we are
195
                        // fine. This is actually not completely safe in an upgrade situation,
196
                        // because the old <4.0 client will not have cleaned the tree, and it could
197
                        // be old. However, this will *only* happen in an upgrade situation from
198
                        // <4.0 to >=4.0, and I can't think of a way it could be exploited. Also,
199
                        // the rootfs-image module does not use any of the information ATM.
200
                        return error::NoError;
×
201
                }
202
        }
203

204
        return PrepareFileTreeDeviceParts(path);
54✔
205
}
54✔
206

207
error::Error UpdateModule::DeleteFileTree(const string &path) {
1✔
208
        try {
209
                fs::remove_all(fs::path {path});
1✔
210
        } catch (const fs::filesystem_error &e) {
×
211
                return error::Error(
212
                        e.code().default_error_condition(),
×
213
                        "Failed to recursively remove directory '" + path + "': " + e.what());
×
UNCOV
214
        }
×
215

216
        return error::NoError;
1✔
217
}
218

219
expected::ExpectedStringVector DiscoverUpdateModules(const conf::MenderConfig &config) {
4✔
220
        vector<string> ret {};
221
        fs::path file_tree_path = fs::path(config.paths.GetDataStore()) / "modules/v3";
12✔
222

223
        try {
224
                for (auto entry : fs::directory_iterator(file_tree_path)) {
18✔
225
                        const fs::path file_path = entry.path();
6✔
226
                        const string file_path_str = file_path.string();
6✔
227
                        if (!fs::is_regular_file(file_path)) {
6✔
228
                                log::Warning("'" + file_path_str + "' is not a regular file");
×
229
                                continue;
×
230
                        }
231

232
                        const fs::perms perms = entry.status().permissions();
233
                        if ((perms & (fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec))
6✔
234
                                == fs::perms::none) {
235
                                log::Warning("'" + file_path_str + "' is not executable");
4✔
236
                                continue;
4✔
237
                        }
238

239
                        ret.push_back(file_path_str);
2✔
240
                }
6✔
241
        } catch (const fs::filesystem_error &e) {
×
242
                auto code = e.code();
×
243
                if (code.value() == ENOENT) {
×
244
                        // directory not found is not an error, just return an empty vector
245
                        return ret;
×
246
                }
247
                // everything (?) else is an error
248
                return expected::unexpected(error::Error(
×
249
                        code.default_error_condition(),
×
250
                        "Failed to discover update modules in '" + file_tree_path.string() + "': " + e.what()));
×
UNCOV
251
        }
×
252

253
        return ret;
4✔
254
}
4✔
255

256
error::Error UpdateModule::PrepareStreamNextPipe() {
136✔
257
        download_->stream_next_path_ = path::Join(update_module_workdir_, "stream-next");
272✔
258

259
        if (::mkfifo(download_->stream_next_path_.c_str(), 0600) != 0) {
136✔
260
                int err = errno;
×
261
                return error::Error(
262
                        generic_category().default_error_condition(err),
×
263
                        "Unable to create `stream-next` at " + download_->stream_next_path_);
×
264
        }
265
        return error::NoError;
136✔
266
}
267

268
error::Error UpdateModule::OpenStreamNextPipe(ExpectedWriterHandler open_handler) {
141✔
269
        auto opener = make_shared<AsyncFifoOpener>(download_->event_loop_);
141✔
270
        download_->stream_next_opener_ = opener;
271
        return opener->AsyncOpen(download_->stream_next_path_, open_handler);
282✔
272
}
273

274
error::Error UpdateModule::PrepareAndOpenStreamPipe(
10✔
275
        const string &path, ExpectedWriterHandler open_handler) {
276
        auto fs_path = fs::path(path);
10✔
277
        std::error_code ec;
10✔
278
        if (!fs::create_directories(fs_path.parent_path(), ec) && ec) {
20✔
279
                return error::Error(
280
                        ec.default_error_condition(),
×
281
                        "Could not create stream directory at " + fs_path.parent_path().string());
×
282
        }
283

284
        if (::mkfifo(path.c_str(), 0600) != 0) {
10✔
285
                int err = errno;
×
286
                return error::Error(
287
                        generic_category().default_error_condition(err),
×
288
                        "Could not create stream FIFO at " + path);
×
289
        }
290

291
        auto opener = make_shared<AsyncFifoOpener>(download_->event_loop_);
10✔
292
        download_->current_stream_opener_ = opener;
293
        return opener->AsyncOpen(path, open_handler);
20✔
294
}
10✔
295

296
error::Error UpdateModule::PrepareDownloadDirectory(const string &path) {
122✔
297
        auto fs_path = fs::path(path);
122✔
298
        std::error_code ec;
122✔
299
        if (!fs::create_directories(fs_path, ec) && ec) {
122✔
300
                return error::Error(
301
                        ec.default_error_condition(), "Could not create `files` directory at " + path);
×
302
        }
303

304
        return error::NoError;
122✔
305
}
122✔
306

307
error::Error UpdateModule::DeleteStreamsFiles() {
122✔
308
        try {
309
                fs::path p {download_->stream_next_path_};
122✔
310
                fs::remove_all(p);
122✔
311

312
                p = fs::path(update_module_workdir_) / "streams";
122✔
313
                fs::remove_all(p);
122✔
314
        } catch (fs::filesystem_error &e) {
122✔
315
                return error::Error(
316
                        e.code().default_error_condition(), "Could not remove " + download_->stream_next_path_);
×
UNCOV
317
        }
×
318

319
        return error::NoError;
122✔
320
}
321

322
AsyncFifoOpener::AsyncFifoOpener(events::EventLoop &loop) :
308✔
323
        event_loop_ {loop},
154✔
324
        cancelled_ {make_shared<bool>(true)},
154✔
325
        destroying_ {make_shared<bool>(false)} {
308✔
326
}
308✔
327

328
AsyncFifoOpener::~AsyncFifoOpener() {
308✔
329
        *destroying_ = true;
154✔
330
        Cancel();
154✔
331
}
462✔
332

333
error::Error AsyncFifoOpener::AsyncOpen(const string &path, ExpectedWriterHandler handler) {
154✔
334
        // Excerpt from fifo(7) man page:
335
        // ------------------------------
336
        // The FIFO must be opened on both ends (reading and writing) before data can be passed.
337
        // Normally, opening the FIFO blocks until the other end is opened also.
338
        //
339
        // A process can open a FIFO in nonblocking mode. In this case, opening for read-only
340
        // succeeds even if no one has opened on the write side yet and opening for write-only fails
341
        // with ENXIO (no such device or address) unless the other end has already been opened.
342
        //
343
        // Under Linux, opening a FIFO for read and write will succeed both in blocking and
344
        // nonblocking mode. POSIX leaves this behavior undefined.  This can be used to open a FIFO
345
        // for writing while there are no readers available.
346
        // ------------------------------
347
        //
348
        // We want to open the pipe to check if a process is going to read from it. But we cannot do
349
        // this in a blocking fashion, because we are also waiting for the process to terminate,
350
        // which happens for Update Modules that want the client to download the files for them. So
351
        // we need this AsyncFifoOpener class, which does the work in a thread.
352

353
        if (!*cancelled_) {
154✔
354
                return error::Error(
355
                        make_error_condition(errc::operation_in_progress), "Already running AsyncFifoOpener");
×
356
        }
357

358
        *cancelled_ = false;
154✔
359
        path_ = path;
154✔
360
        thread_ = thread([this, handler]() {
308✔
361
                auto writer = make_shared<events::io::AsyncFileDescriptorWriter>(event_loop_);
154✔
362
                // This will block for as long as there are no FIFO readers.
363
                auto err = writer->Open(path_);
154✔
364

365
                io::ExpectedAsyncWriterPtr exp_writer;
154✔
366
                if (err != error::NoError) {
154✔
367
                        exp_writer = expected::unexpected(err);
2✔
368
                } else {
369
                        exp_writer = writer;
306✔
370
                }
371

372
                auto &cancelled = cancelled_;
154✔
373
                auto &destroying = destroying_;
374
                event_loop_.Post([handler, exp_writer, cancelled, destroying]() {
605✔
375
                        if (*destroying) {
143✔
376
                                return;
377
                        }
378

379
                        if (*cancelled) {
22✔
380
                                handler(expected::unexpected(error::Error(
1✔
381
                                        make_error_condition(errc::operation_canceled), "AsyncFifoOpener cancelled")));
2✔
382
                                return;
1✔
383
                        }
384

385
                        handler(exp_writer);
42✔
386
                });
387
        });
308✔
388

389
        return error::NoError;
154✔
390
}
391

392
void AsyncFifoOpener::Cancel() {
155✔
393
        if (*cancelled_) {
155✔
394
                return;
395
        }
396

397
        *cancelled_ = true;
154✔
398

399
        // Open the other end of the pipe to jerk the first end loose.
400
        int fd = ::open(path_.c_str(), O_RDONLY | O_NONBLOCK);
154✔
401
        if (fd < 0) {
154✔
402
                int errnum = errno;
1✔
403
                log::Error(string("Cancel::open() returned error: ") + strerror(errnum));
2✔
404
        }
405

406
        thread_.join();
154✔
407

408
        ::close(fd);
154✔
409
}
410

411
} // namespace v3
412
} // namespace update_module
413
} // namespace update
414
} // 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