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

mendersoftware / mender / 2286202698

26 Jan 2026 10:37AM UTC coverage: 81.48% (+0.1%) from 81.368%
2286202698

push

gitlab-ci

lluiscampos
test: Add new DBus service file

See:
* https://github.com/mendersoftware/mender/pull/1874

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

8839 of 10848 relevant lines covered (81.48%)

20226.81 hits per line

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

81.06
/src/mender-update/daemon/states.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/daemon/states.hpp>
16

17
#include <client_shared/conf.hpp>
18
#include <common/events_io.hpp>
19
#include <common/log.hpp>
20
#include <common/path.hpp>
21

22
#include <mender-update/daemon/context.hpp>
23
#include <mender-update/inventory.hpp>
24

25
namespace mender {
26
namespace update {
27
namespace daemon {
28

29
namespace conf = mender::client_shared::conf;
30
namespace error = mender::common::error;
31
namespace events = mender::common::events;
32
namespace kv_db = mender::common::key_value_database;
33
namespace path = mender::common::path;
34
namespace log = mender::common::log;
35

36
namespace main_context = mender::update::context;
37
namespace inventory = mender::update::inventory;
38

39
class DefaultStateHandler {
40
public:
41
        void operator()(const error::Error &err) {
294✔
42
                if (err != error::NoError) {
294✔
43
                        log::Error(err.String());
23✔
44
                        poster.PostEvent(StateEvent::Failure);
23✔
45
                        return;
23✔
46
                }
47
                poster.PostEvent(StateEvent::Success);
271✔
48
        }
49

50
        sm::EventPoster<StateEvent> &poster;
51
};
52

53
static void DefaultAsyncErrorHandler(sm::EventPoster<StateEvent> &poster, const error::Error &err) {
412✔
54
        if (err != error::NoError) {
412✔
55
                log::Error(err.String());
×
56
                poster.PostEvent(StateEvent::Failure);
×
57
        }
58
}
412✔
59

60
void EmptyState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
152✔
61
        // Keep this state truly empty.
62
}
152✔
63

64
void InitState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
59✔
65
        // I will never run - just a placeholder to start the state-machine at
66
        poster.PostEvent(StateEvent::Started); // Start the state machine
59✔
67
}
59✔
68

69
void StateScriptState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
1,064✔
70
        string state_name {script_executor::Name(this->state_, this->action_)};
1,064✔
71
        log::Debug("Executing the  " + state_name + " State Scripts...");
1,064✔
72
        auto err = this->script_.AsyncRunScripts(
2,128✔
73
                this->state_,
74
                this->action_,
75
                [state_name, &poster](error::Error err) {
8,945✔
76
                        if (err != error::NoError) {
1,064✔
77
                                log::Error(
21✔
78
                                        "Received error: (" + err.String() + ") when running the State Script scripts "
21✔
79
                                        + state_name);
42✔
80
                                poster.PostEvent(StateEvent::Failure);
21✔
81
                                return;
21✔
82
                        }
83
                        log::Debug("Successfully ran the " + state_name + " State Scripts...");
1,043✔
84
                        poster.PostEvent(StateEvent::Success);
1,043✔
85
                },
86
                this->on_error_);
2,128✔
87

88
        if (err != error::NoError) {
1,064✔
89
                log::Error(
×
90
                        "Failed to schedule the state script execution for: " + state_name
×
91
                        + " got error: " + err.String());
×
92
                poster.PostEvent(StateEvent::Failure);
×
93
                return;
94
        }
95
}
96

97

98
void SaveStateScriptState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
278✔
99
        return state_script_state_.OnEnter(ctx, poster);
278✔
100
}
101

102
void IdleState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
123✔
103
        log::Debug("Entering Idle state");
123✔
104
}
123✔
105

106
ScheduleNextPollState::ScheduleNextPollState(
192✔
107
        events::Timer &timer, const string &poll_action, const StateEvent event, int interval) :
192✔
108
        timer_ {timer},
192✔
109
        poll_action_ {poll_action},
192✔
110
        event_ {event},
192✔
111
        interval_ {interval} {
192✔
112
}
192✔
113

114
void ScheduleNextPollState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
122✔
115
        log::Debug("Scheduling the next " + poll_action_ + " in: " + to_string(interval_) + " seconds");
244✔
116
        timer_.AsyncWait(chrono::seconds(interval_), [this, &poster](error::Error err) {
122✔
117
                if (err != error::NoError) {
8✔
118
                        if (err.code != make_error_condition(errc::operation_canceled)) {
5✔
119
                                log::Error("Timer caused error: " + err.String());
×
120
                        }
121
                } else {
122
                        poster.PostEvent(event_);
3✔
123
                }
124
        });
8✔
125

126
        poster.PostEvent(StateEvent::Success);
122✔
127
}
122✔
128

129
SubmitInventoryState::SubmitInventoryState(int retry_interval_seconds, int retry_count) :
192✔
130
        backoff_ {chrono::seconds(retry_interval_seconds), retry_count} {
96✔
131
}
192✔
132

133
void SubmitInventoryState::HandlePollingError(Context &ctx, sm::EventPoster<StateEvent> &poster) {
×
134
        // When using short polling intervals, we should adjust the backoff to ensure
135
        // that the intervals do not exceed the maximum retry polling interval, which
136
        // converts the backoff to a fixed interval.
137
        chrono::milliseconds max_interval =
138
                chrono::seconds(ctx.mender_context.GetConfig().retry_poll_interval_seconds);
×
139
        if (max_interval < backoff_.SmallestInterval()) {
×
140
                backoff_.SetSmallestInterval(max_interval);
×
141
                backoff_.SetMaxInterval(max_interval);
×
142
        }
143
        auto exp_interval = backoff_.NextInterval();
×
144
        if (!exp_interval) {
×
145
                log::Debug(
×
146
                        "Not retrying with backoff, retrying InventoryPollIntervalSeconds: "
147
                        + exp_interval.error().String());
×
148
                return;
149
        }
150
        log::Info(
×
151
                "Retrying inventory polling in "
152
                + to_string(chrono::duration_cast<chrono::seconds>(*exp_interval).count()) + " seconds");
×
153

154
        ctx.inventory_timer.Cancel();
×
155
        ctx.inventory_timer.AsyncWait(*exp_interval, [&poster](error::Error err) {
×
156
                if (err != error::NoError) {
×
157
                        if (err.code != make_error_condition(errc::operation_canceled)) {
×
158
                                log::Error("Retry poll timer caused error: " + err.String());
×
159
                        }
160
                } else {
161
                        poster.PostEvent(StateEvent::InventoryPollingTriggered);
×
162
                }
163
        });
×
164
}
165

166
void SubmitInventoryState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
59✔
167
        log::Debug("Submitting inventory");
59✔
168

169
        auto handler = [this, &ctx, &poster](error::Error err) {
59✔
170
                if (err != error::NoError) {
59✔
171
                        log::Error("Failed to submit inventory: " + err.String());
×
172
                        // Replace the inventory poll timer with a backoff
173
                        HandlePollingError(ctx, poster);
×
174
                        poster.PostEvent(StateEvent::Failure);
×
175
                        return;
×
176
                }
177
                backoff_.Reset();
59✔
178
                ctx.inventory_client->has_submitted_inventory = true;
59✔
179
                poster.PostEvent(StateEvent::Success);
59✔
180
        };
59✔
181

182
        auto err = ctx.inventory_client->PushData(
59✔
183
                ctx.mender_context.GetConfig().paths.GetInventoryScriptsDir(),
59✔
184
                ctx.event_loop,
185
                ctx.http_client,
186
                handler);
59✔
187

188
        if (err != error::NoError) {
59✔
189
                // This is the only case the handler won't be called for us by
190
                // PushData() (see inventory::PushInventoryData()).
191
                handler(err);
×
192
        }
193
}
59✔
194

195
PollForDeploymentState::PollForDeploymentState(int retry_interval_seconds, int retry_count) :
192✔
196
        backoff_ {chrono::seconds(retry_interval_seconds), retry_count} {
96✔
197
}
192✔
198

199
void PollForDeploymentState::HandlePollingError(Context &ctx, sm::EventPoster<StateEvent> &poster) {
4✔
200
        // When using short polling intervals, we should adjust the backoff to ensure
201
        // that the intervals do not exceed the maximum retry polling interval, which
202
        // converts the backoff to a fixed interval.
203
        chrono::milliseconds max_interval =
204
                chrono::seconds(ctx.mender_context.GetConfig().retry_poll_interval_seconds);
4✔
205
        if (max_interval < backoff_.SmallestInterval()) {
4✔
206
                backoff_.SetSmallestInterval(max_interval);
1✔
207
                backoff_.SetMaxInterval(max_interval);
1✔
208
        }
209
        auto exp_interval = backoff_.NextInterval();
4✔
210
        if (!exp_interval) {
4✔
211
                log::Debug(
1✔
212
                        "Not retrying with backoff, retrying with UpdatePollIntervalSeconds: "
213
                        + exp_interval.error().String());
2✔
214
                return;
215
        }
216
        log::Info(
3✔
217
                "Retrying deployment polling in "
218
                + to_string(chrono::duration_cast<chrono::seconds>(*exp_interval).count()) + " seconds");
3✔
219

220
        ctx.deployment_timer.Cancel();
3✔
221
        ctx.deployment_timer.AsyncWait(*exp_interval, [&poster](error::Error err) {
6✔
222
                if (err != error::NoError) {
3✔
223
                        if (err.code != make_error_condition(errc::operation_canceled)) {
×
224
                                log::Error("Retry poll timer caused error: " + err.String());
×
225
                        }
226
                } else {
227
                        poster.PostEvent(StateEvent::DeploymentPollingTriggered);
3✔
228
                }
229
        });
3✔
230
}
231

232
void PollForDeploymentState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
62✔
233
        log::Debug("Polling for update");
124✔
234

235
        auto err = ctx.deployment_client->CheckNewDeployments(
124✔
236
                ctx.mender_context,
237
                ctx.http_client,
238
                [this, &ctx, &poster](mender::update::deployments::CheckUpdatesAPIResponse response) {
120✔
239
                        if (!response) {
58✔
240
                                log::Error("Error while polling for deployment: " + response.error().String());
×
241
                                // Replace the update poll timer with a backoff
242
                                HandlePollingError(ctx, poster);
×
243
                                poster.PostEvent(StateEvent::Failure);
×
244
                                return;
1✔
245
                        } else if (!response.value()) {
58✔
246
                                log::Info("No update available");
1✔
247
                                poster.PostEvent(StateEvent::NothingToDo);
1✔
248
                                if (not ctx.inventory_client->has_submitted_inventory) {
1✔
249
                                        // If we have not submitted inventory successfully at least
250
                                        // once, schedule this after receiving a successful response
251
                                        // with no update. This enables inventory to be submitted
252
                                        // immediately after the device has been accepted. If there
253
                                        // is an update available, an inventory update will be
254
                                        // scheduled at the end of it unconditionally.
255
                                        poster.PostEvent(StateEvent::InventoryPollingTriggered);
×
256
                                }
257

258
                                backoff_.Reset();
1✔
259
                                return;
1✔
260
                        }
261
                        backoff_.Reset();
57✔
262

263
                        auto exp_data = ApiResponseJsonToStateData(response.value().value());
57✔
264
                        if (!exp_data) {
57✔
265
                                log::Error("Error in API response: " + exp_data.error().String());
×
266
                                poster.PostEvent(StateEvent::Failure);
×
267
                                return;
268
                        }
269

270
                        // Make a new set of update data.
271
                        ctx.deployment.state_data.reset(new StateData(std::move(exp_data.value())));
57✔
272

273
                        ctx.BeginDeploymentLogging();
57✔
274

275
                        // This is a duplicate message to one logged when mender-update
276
                        // starts, but this one goes into the deployment log.
277
                        log::Info("Running mender-update " + conf::kMenderVersion);
57✔
278
                        log::Info(
57✔
279
                                "Deployment with ID " + ctx.deployment.state_data->update_info.id + " started.");
57✔
280

281
                        poster.PostEvent(StateEvent::DeploymentStarted);
57✔
282
                        poster.PostEvent(StateEvent::Success);
57✔
283
                });
62✔
284

285
        if (err != error::NoError) {
62✔
286
                log::Error("Error when trying to poll for deployment: " + err.String());
4✔
287
                // If we're here, no handler will be called, so we need to manually schedule the next
288
                // deployment poll.
289
                // Posting Failure correctly exits the PollForDeploymentState, but does not schedule the
290
                // next poll. Thus, we need to also call HandlePollingError that adds a timer that will
291
                // cause DeploymentPollingTriggered to be posted to the state machine after a defined time.
292
                HandlePollingError(ctx, poster);
4✔
293
                poster.PostEvent(StateEvent::Failure);
4✔
294
        }
295
}
62✔
296

297
void SaveState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
535✔
298
        assert(ctx.deployment.state_data);
299

300
        ctx.deployment.state_data->state = DatabaseStateString();
535✔
301

302
        log::Trace("Storing deployment state in the DB (database-string): " + DatabaseStateString());
1,070✔
303

304
        auto err = ctx.SaveDeploymentStateData(*ctx.deployment.state_data);
535✔
305
        if (err != error::NoError) {
535✔
306
                log::Error(err.String());
10✔
307
                if (err.code
10✔
308
                        == main_context::MakeError(main_context::StateDataStoreCountExceededError, "").code) {
20✔
309
                        poster.PostEvent(StateEvent::StateLoopDetected);
1✔
310
                        return;
311
                } else if (!IsFailureState()) {
9✔
312
                        // Non-failure states should be interrupted, but failure states should be
313
                        // allowed to do their work, even if a database error was detected.
314
                        poster.PostEvent(StateEvent::Failure);
2✔
315
                        return;
316
                }
317
        }
318

319
        OnEnterSaveState(ctx, poster);
532✔
320
}
321

322
void UpdateDownloadState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
54✔
323
        log::Debug("Entering Download state");
54✔
324

325
        auto req = make_shared<http::OutgoingRequest>();
54✔
326
        req->SetMethod(http::Method::GET);
54✔
327
        auto err = req->SetAddress(ctx.deployment.state_data->update_info.artifact.source.uri);
54✔
328
        if (err != error::NoError) {
54✔
329
                log::Error(err.String());
×
330
                poster.PostEvent(StateEvent::Failure);
×
331
                return;
332
        }
333

334
        err = ctx.download_client->AsyncCall(
162✔
335
                req,
336
                [&ctx, &poster](http::ExpectedIncomingResponsePtr exp_resp) {
54✔
337
                        if (!exp_resp) {
54✔
338
                                log::Error("Unexpected error during download: " + exp_resp.error().String());
×
339
                                poster.PostEvent(StateEvent::Failure);
×
340
                                return;
1✔
341
                        }
342

343
                        auto &resp = exp_resp.value();
54✔
344
                        if (resp->GetStatusCode() != http::StatusOK) {
54✔
345
                                log::Error(
1✔
346
                                        "Unexpected status code while fetching artifact: " + resp->GetStatusMessage());
1✔
347
                                poster.PostEvent(StateEvent::Failure);
1✔
348
                                return;
1✔
349
                        }
350

351
                        auto http_reader = resp->MakeBodyAsyncReader();
53✔
352
                        if (!http_reader) {
53✔
353
                                log::Error(http_reader.error().String());
×
354
                                poster.PostEvent(StateEvent::Failure);
×
355
                                return;
356
                        }
357
                        ctx.deployment.artifact_reader =
53✔
358
                                make_shared<events::io::ReaderFromAsyncReader>(ctx.event_loop, http_reader.value());
53✔
359
                        ParseArtifact(ctx, poster);
53✔
360
                },
361
                [](http::ExpectedIncomingResponsePtr exp_resp) {
54✔
362
                        if (!exp_resp) {
54✔
363
                                log::Error(exp_resp.error().String());
6✔
364
                                // Cannot handle error here, because this handler is called at the
365
                                // end of the download, when we have already left this state. So
366
                                // rely on this error being propagated through the BodyAsyncReader
367
                                // above instead.
368
                                return;
6✔
369
                        }
370
                });
54✔
371

372
        if (err != error::NoError) {
54✔
373
                log::Error(err.String());
×
374
                poster.PostEvent(StateEvent::Failure);
×
375
                return;
376
        }
377
}
378

379
void UpdateDownloadState::ParseArtifact(Context &ctx, sm::EventPoster<StateEvent> &poster) {
53✔
380
        string art_scripts_path = ctx.mender_context.GetConfig().paths.GetArtScriptsPath();
53✔
381

382
        // Clear the artifact scripts directory so we don't risk old scripts lingering.
383
        auto err = path::DeleteRecursively(art_scripts_path);
53✔
384
        if (err != error::NoError) {
53✔
385
                log::Error("When preparing to parse artifact: " + err.String());
×
386
                poster.PostEvent(StateEvent::Failure);
×
387
                return;
388
        }
389

390
        artifact::config::ParserConfig config {
53✔
391
                .artifact_scripts_filesystem_path = art_scripts_path,
392
                .artifact_scripts_version = 3,
393
                .artifact_verify_keys = ctx.mender_context.GetConfig().artifact_verify_keys,
53✔
394
        };
53✔
395
        auto exp_parser = artifact::Parse(*ctx.deployment.artifact_reader, config);
53✔
396
        if (!exp_parser) {
53✔
397
                log::Error(exp_parser.error().String());
×
398
                poster.PostEvent(StateEvent::Failure);
×
399
                return;
400
        }
401
        ctx.deployment.artifact_parser.reset(new artifact::Artifact(std::move(exp_parser.value())));
53✔
402

403
        auto exp_header = artifact::View(*ctx.deployment.artifact_parser, 0);
53✔
404
        if (!exp_header) {
53✔
405
                log::Error(exp_header.error().String());
×
406
                poster.PostEvent(StateEvent::Failure);
×
407
                return;
408
        }
409
        auto &header = exp_header.value();
53✔
410

411
        auto exp_matches = ctx.mender_context.MatchesArtifactDepends(header.header);
53✔
412
        if (!exp_matches) {
53✔
413
                log::Error(exp_matches.error().String());
2✔
414
                poster.PostEvent(StateEvent::Failure);
2✔
415
                return;
416
        } else if (!exp_matches.value()) {
51✔
417
                // reasons already logged
418
                poster.PostEvent(StateEvent::Failure);
1✔
419
                return;
420
        }
421

422
        log::Info("Installing artifact...");
100✔
423

424
        ctx.deployment.state_data->FillUpdateDataFromArtifact(header);
50✔
425

426
        ctx.deployment.state_data->state = Context::kUpdateStateDownload;
50✔
427

428
        assert(ctx.deployment.state_data->update_info.artifact.payload_types.size() == 1);
429

430
        // Initial state data save, now that we have enough information from the artifact.
431
        err = ctx.SaveDeploymentStateData(*ctx.deployment.state_data);
50✔
432
        if (err != error::NoError) {
50✔
433
                log::Error(err.String());
×
434
                if (err.code
×
435
                        == main_context::MakeError(main_context::StateDataStoreCountExceededError, "").code) {
×
436
                        poster.PostEvent(StateEvent::StateLoopDetected);
×
437
                        return;
438
                } else {
439
                        poster.PostEvent(StateEvent::Failure);
×
440
                        return;
441
                }
442
        }
443

444
        if (header.header.payload_type == "") {
50✔
445
                // Empty-payload-artifact, aka "bootstrap artifact".
446
                poster.PostEvent(StateEvent::NothingToDo);
1✔
447
                return;
448
        }
449

450
        auto exp_update_module =
451
                update_module::UpdateModule::Create(ctx.mender_context, header.header.payload_type);
49✔
452
        if (!exp_update_module.has_value()) {
49✔
453
                log::Error(
×
454
                        "Error creating an Update Module when parsing artifact: "
455
                        + exp_update_module.error().String());
×
456
                poster.PostEvent(StateEvent::Failure);
×
457
                return;
458
        }
459
        ctx.deployment.update_module = std::move(exp_update_module.value());
49✔
460

461
        err = ctx.deployment.update_module->CleanAndPrepareFileTree(
49✔
462
                ctx.deployment.update_module->GetUpdateModuleWorkDir(), header);
49✔
463
        if (err != error::NoError) {
49✔
464
                log::Error(err.String());
×
465
                poster.PostEvent(StateEvent::Failure);
×
466
                return;
467
        }
468

469
        err = ctx.deployment.update_module->AsyncProvidePayloadFileSizes(
49✔
470
                ctx.event_loop, [&ctx, &poster](expected::ExpectedBool download_with_sizes) {
49✔
471
                        if (!download_with_sizes.has_value()) {
49✔
472
                                log::Error(download_with_sizes.error().String());
×
473
                                poster.PostEvent(StateEvent::Failure);
×
474
                                return;
×
475
                        }
476
                        ctx.deployment.download_with_sizes = download_with_sizes.value();
49✔
477
                        DoDownload(ctx, poster);
49✔
478
                });
49✔
479

480
        if (err != error::NoError) {
49✔
481
                log::Error(err.String());
×
482
                poster.PostEvent(StateEvent::Failure);
×
483
                return;
484
        }
485
}
53✔
486

487
void UpdateDownloadState::DoDownload(Context &ctx, sm::EventPoster<StateEvent> &poster) {
49✔
488
        auto exp_payload = ctx.deployment.artifact_parser->Next();
49✔
489
        if (!exp_payload) {
49✔
490
                log::Error(exp_payload.error().String());
×
491
                poster.PostEvent(StateEvent::Failure);
×
492
                return;
493
        }
494
        ctx.deployment.artifact_payload.reset(new artifact::Payload(std::move(exp_payload.value())));
49✔
495

496
        auto handler = [&poster, &ctx](error::Error err) {
49✔
497
                if (err != error::NoError) {
49✔
498
                        log::Error(err.String());
2✔
499
                        poster.PostEvent(StateEvent::Failure);
2✔
500
                        return;
2✔
501
                }
502

503
                auto exp_payload = ctx.deployment.artifact_parser->Next();
47✔
504
                if (exp_payload) {
47✔
505
                        log::Error("Multiple payloads are not yet supported in daemon mode.");
×
506
                        poster.PostEvent(StateEvent::Failure);
×
507
                        return;
508
                } else if (
47✔
509
                        exp_payload.error().code
510
                        != artifact::parser_error::MakeError(artifact::parser_error::EOFError, "").code) {
94✔
511
                        log::Error(exp_payload.error().String());
×
512
                        poster.PostEvent(StateEvent::Failure);
×
513
                        return;
514
                }
515

516
                poster.PostEvent(StateEvent::Success);
47✔
517
        };
518

519
        if (ctx.deployment.download_with_sizes) {
49✔
520
                ctx.deployment.update_module->AsyncDownloadWithFileSizes(
2✔
521
                        ctx.event_loop, *ctx.deployment.artifact_payload, handler);
522
        } else {
523
                ctx.deployment.update_module->AsyncDownload(
96✔
524
                        ctx.event_loop, *ctx.deployment.artifact_payload, handler);
525
        }
526
}
527

528
void UpdateDownloadCancelState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
6✔
529
        log::Debug("Entering DownloadCancel state");
12✔
530
        ctx.download_client->Cancel();
6✔
531
        poster.PostEvent(StateEvent::Success);
6✔
532
}
6✔
533

534
SendStatusUpdateState::SendStatusUpdateState(optional<deployments::DeploymentStatus> status) :
288✔
535
        status_(status),
288✔
536
        mode_(FailureMode::Ignore) {
288✔
537
}
288✔
538

539
SendStatusUpdateState::SendStatusUpdateState(
384✔
540
        optional<deployments::DeploymentStatus> status,
541
        events::EventLoop &event_loop,
542
        int retry_interval_seconds,
543
        int retry_count) :
192✔
544
        status_(status),
192✔
545
        mode_(FailureMode::RetryThenFail),
192✔
546
        retry_(Retry {
192✔
547
                http::ExponentialBackoff(chrono::seconds(retry_interval_seconds), retry_count),
548
                event_loop}) {
192✔
549
}
384✔
550

551
void SendStatusUpdateState::SetSmallestWaitInterval(chrono::milliseconds interval) {
182✔
552
        if (retry_) {
182✔
553
                retry_->backoff.SetSmallestInterval(interval);
182✔
554
        }
555
}
182✔
556

557
void SendStatusUpdateState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
246✔
558
        // Reset this every time we enter the state, which means a new round of retries.
559
        if (retry_) {
246✔
560
                retry_->backoff.Reset();
561
        }
562

563
        DoStatusUpdate(ctx, poster);
246✔
564
}
246✔
565

566
void SendStatusUpdateState::DoStatusUpdate(Context &ctx, sm::EventPoster<StateEvent> &poster) {
265✔
567
        assert(ctx.deployment_client);
568
        assert(ctx.deployment.state_data);
569

570
        log::Info("Sending status update to server");
265✔
571

572
        auto result_handler = [this, &ctx, &poster](const error::Error &err) {
265✔
573
                if (err != error::NoError) {
265✔
574
                        log::Error("Could not send deployment status: " + err.String());
25✔
575

576
                        if (err.code == deployments::MakeError(deployments::DeploymentAbortedError, "").code) {
50✔
577
                                // If the deployment was aborted upstream it is an immediate
578
                                // failure, even if retry is enabled.
579
                                poster.PostEvent(StateEvent::DeploymentAborted);
3✔
580
                                return;
3✔
581
                        }
582

583
                        switch (mode_) {
22✔
584
                        case FailureMode::Ignore:
585
                                break;
2✔
586
                        case FailureMode::RetryThenFail:
20✔
587

588
                                auto exp_interval = retry_->backoff.NextInterval();
20✔
589
                                if (!exp_interval) {
20✔
590
                                        log::Error(
1✔
591
                                                "Giving up on sending status updates to server: "
592
                                                + exp_interval.error().String());
1✔
593
                                        poster.PostEvent(StateEvent::Failure);
1✔
594
                                        return;
595
                                }
596

597
                                log::Info(
19✔
598
                                        "Retrying status update after "
599
                                        + to_string(chrono::duration_cast<chrono::seconds>(*exp_interval).count())
19✔
600
                                        + " seconds");
19✔
601
                                retry_->wait_timer.AsyncWait(
19✔
602
                                        *exp_interval, [this, &ctx, &poster](error::Error err) {
19✔
603
                                                // Error here is quite unexpected (from a timer), so treat
604
                                                // this as an immediate error, despite Retry flag.
605
                                                if (err != error::NoError) {
19✔
606
                                                        log::Error(
×
607
                                                                "Unexpected error in SendStatusUpdateState wait timer: "
608
                                                                + err.String());
×
609
                                                        poster.PostEvent(StateEvent::Failure);
×
610
                                                        return;
×
611
                                                }
612

613
                                                // Try again. Since both status and logs are sent
614
                                                // from here, there's a chance this might resubmit
615
                                                // the status, but there's no harm in it, and it
616
                                                // won't happen often.
617
                                                DoStatusUpdate(ctx, poster);
19✔
618
                                        });
619
                                return;
19✔
620
                        }
621
                }
622

623
                poster.PostEvent(StateEvent::Success);
242✔
624
        };
265✔
625

626
        deployments::DeploymentStatus status;
627
        if (status_) {
265✔
628
                status = status_.value();
172✔
629
        } else {
630
                // If nothing is specified, grab success/failure status from the deployment status.
631
                if (ctx.deployment.failed) {
93✔
632
                        status = deployments::DeploymentStatus::Failure;
633
                } else {
634
                        status = deployments::DeploymentStatus::Success;
635
                }
636
        }
637

638
        // Push status.
639
        log::Debug("Pushing deployment status: " + DeploymentStatusString(status));
530✔
640
        auto err = ctx.deployment_client->PushStatus(
265✔
641
                ctx.deployment.state_data->update_info.id,
265✔
642
                status,
643
                "",
644
                ctx.http_client,
645
                [result_handler, &ctx](error::Error err) {
265✔
646
                        // If there is an error, we don't submit logs now, but call the handler,
647
                        // which may schedule a retry later. If there is no error, and the
648
                        // deployment as a whole was successful, then also call the handler here,
649
                        // since we don't need to submit logs at all then.
650
                        if (err != error::NoError || !ctx.deployment.failed) {
265✔
651
                                result_handler(err);
190✔
652
                                return;
190✔
653
                        }
654

655
                        // Push logs.
656
                        err = ctx.deployment_client->PushLogs(
75✔
657
                                ctx.deployment.state_data->update_info.id,
75✔
658
                                ctx.deployment.logger->LogFilePath(),
150✔
659
                                ctx.http_client,
75✔
660
                                result_handler);
150✔
661

662
                        if (err != error::NoError) {
75✔
663
                                result_handler(err);
×
664
                        }
665
                });
265✔
666

667
        if (err != error::NoError) {
265✔
668
                result_handler(err);
×
669
        }
670

671
        // No action, wait for reply from status endpoint.
672
}
265✔
673

674
void UpdateInstallState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
42✔
675
        log::Debug("Entering ArtifactInstall state");
84✔
676

677
        DefaultAsyncErrorHandler(
42✔
678
                poster,
679
                ctx.deployment.update_module->AsyncArtifactInstall(
42✔
680
                        ctx.event_loop, DefaultStateHandler {poster}));
42✔
681
}
42✔
682

683
void UpdateCheckRebootState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
73✔
684
        DefaultAsyncErrorHandler(
73✔
685
                poster,
686
                ctx.deployment.update_module->AsyncNeedsReboot(
73✔
687
                        ctx.event_loop, [&ctx, &poster](update_module::ExpectedRebootAction reboot_action) {
73✔
688
                                if (!reboot_action.has_value()) {
73✔
689
                                        log::Error(reboot_action.error().String());
2✔
690
                                        poster.PostEvent(StateEvent::Failure);
2✔
691
                                        return;
2✔
692
                                }
693

694
                                ctx.deployment.state_data->update_info.reboot_requested.resize(1);
71✔
695
                                ctx.deployment.state_data->update_info.reboot_requested[0] =
71✔
696
                                        NeedsRebootToDbString(*reboot_action);
71✔
697
                                switch (*reboot_action) {
71✔
698
                                case update_module::RebootAction::No:
8✔
699
                                        poster.PostEvent(StateEvent::NothingToDo);
8✔
700
                                        break;
8✔
701
                                case update_module::RebootAction::Yes:
63✔
702
                                case update_module::RebootAction::Automatic:
703
                                        poster.PostEvent(StateEvent::Success);
63✔
704
                                        break;
63✔
705
                                }
706
                        }));
707
}
73✔
708

709
void UpdateRebootState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
26✔
710
        log::Debug("Entering ArtifactReboot state");
52✔
711

712
        assert(ctx.deployment.state_data->update_info.reboot_requested.size() == 1);
713
        auto exp_reboot_mode =
714
                DbStringToNeedsReboot(ctx.deployment.state_data->update_info.reboot_requested[0]);
26✔
715
        // Should always be true because we check it at load time.
716
        assert(exp_reboot_mode);
717

718
        switch (exp_reboot_mode.value()) {
26✔
719
        case update_module::RebootAction::No:
×
720
                // Should not happen because then we don't enter this state.
721
                assert(false);
722
                poster.PostEvent(StateEvent::Failure);
×
723
                break;
724
        case update_module::RebootAction::Yes:
26✔
725
                DefaultAsyncErrorHandler(
26✔
726
                        poster,
727
                        ctx.deployment.update_module->AsyncArtifactReboot(
26✔
728
                                ctx.event_loop, DefaultStateHandler {poster}));
26✔
729
                break;
26✔
730
        case update_module::RebootAction::Automatic:
×
731
                DefaultAsyncErrorHandler(
×
732
                        poster,
733
                        ctx.deployment.update_module->AsyncSystemReboot(
×
734
                                ctx.event_loop, DefaultStateHandler {poster}));
×
735
                break;
×
736
        }
737
}
26✔
738

739
void UpdateVerifyRebootState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
29✔
740
        log::Debug("Entering ArtifactVerifyReboot state");
58✔
741

742
        ctx.deployment.update_module->EnsureRootfsImageFileTree(
29✔
743
                ctx.deployment.update_module->GetUpdateModuleWorkDir());
29✔
744

745
        DefaultAsyncErrorHandler(
29✔
746
                poster,
747
                ctx.deployment.update_module->AsyncArtifactVerifyReboot(
29✔
748
                        ctx.event_loop, DefaultStateHandler {poster}));
29✔
749
}
29✔
750

751
void UpdateBeforeCommitState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
22✔
752
        // It's possible that the update we have done has changed our credentials. Therefore it's
753
        // important that we try to log in from scratch and do not use the token we already have.
754
        ctx.http_client.ExpireToken();
22✔
755

756
        poster.PostEvent(StateEvent::Success);
22✔
757
}
22✔
758

759
void UpdateCommitState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
19✔
760
        log::Debug("Entering ArtifactCommit state");
19✔
761

762
        // Explicitly check if state scripts version is supported
763
        auto err = script_executor::CheckScriptsCompatibility(
764
                ctx.mender_context.GetConfig().paths.GetRootfsScriptsPath());
19✔
765
        if (err != error::NoError) {
19✔
766
                log::Error("Failed script compatibility check: " + err.String());
×
767
                poster.PostEvent(StateEvent::Failure);
×
768
                return;
769
        }
770

771
        DefaultAsyncErrorHandler(
19✔
772
                poster,
773
                ctx.deployment.update_module->AsyncArtifactCommit(
19✔
774
                        ctx.event_loop, DefaultStateHandler {poster}));
38✔
775
}
776

777
void UpdateAfterCommitState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
19✔
778
        // Now we have committed. If we had a schema update, re-save state data with the new schema.
779
        assert(ctx.deployment.state_data);
780
        auto &state_data = *ctx.deployment.state_data;
781
        if (state_data.update_info.has_db_schema_update) {
19✔
782
                state_data.update_info.has_db_schema_update = false;
×
783
                auto err = ctx.SaveDeploymentStateData(state_data);
×
784
                if (err != error::NoError) {
×
785
                        log::Error("Not able to commit schema update: " + err.String());
×
786
                        poster.PostEvent(StateEvent::Failure);
×
787
                        return;
788
                }
789
        }
790

791
        poster.PostEvent(StateEvent::Success);
19✔
792
}
793

794
void UpdateCheckRollbackState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
45✔
795
        DefaultAsyncErrorHandler(
45✔
796
                poster,
797
                ctx.deployment.update_module->AsyncSupportsRollback(
45✔
798
                        ctx.event_loop, [&ctx, &poster](expected::ExpectedBool rollback_supported) {
45✔
799
                                if (!rollback_supported.has_value()) {
45✔
800
                                        log::Error(rollback_supported.error().String());
1✔
801
                                        poster.PostEvent(StateEvent::Failure);
1✔
802
                                        return;
1✔
803
                                }
804

805
                                ctx.deployment.state_data->update_info.supports_rollback =
44✔
806
                                        SupportsRollbackToDbString(*rollback_supported);
44✔
807
                                if (*rollback_supported) {
44✔
808
                                        poster.PostEvent(StateEvent::RollbackStarted);
38✔
809
                                        poster.PostEvent(StateEvent::Success);
38✔
810
                                } else {
811
                                        poster.PostEvent(StateEvent::NothingToDo);
6✔
812
                                }
813
                        }));
814
}
45✔
815

816
void UpdateRollbackState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
41✔
817
        log::Debug("Entering ArtifactRollback state");
82✔
818

819
        DefaultAsyncErrorHandler(
41✔
820
                poster,
821
                ctx.deployment.update_module->AsyncArtifactRollback(
41✔
822
                        ctx.event_loop, DefaultStateHandler {poster}));
41✔
823
}
41✔
824

825
void UpdateRollbackRebootState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
57✔
826
        log::Debug("Entering ArtifactRollbackReboot state");
114✔
827

828
        auto exp_reboot_mode =
829
                DbStringToNeedsReboot(ctx.deployment.state_data->update_info.reboot_requested[0]);
57✔
830
        // Should always be true because we check it at load time.
831
        assert(exp_reboot_mode);
832

833
        // We ignore errors in this state as long as the ArtifactVerifyRollbackReboot state
834
        // succeeds.
835
        auto handler = [&poster](error::Error err) {
57✔
836
                if (err != error::NoError) {
57✔
837
                        log::Error(err.String());
2✔
838
                }
839
                poster.PostEvent(StateEvent::Success);
57✔
840
        };
57✔
841

842
        error::Error err;
57✔
843
        switch (exp_reboot_mode.value()) {
57✔
844
        case update_module::RebootAction::No:
845
                // Should not happen because then we don't enter this state.
846
                assert(false);
847

848
                err = error::MakeError(
×
849
                        error::ProgrammingError, "Entered UpdateRollbackRebootState with RebootAction = No");
×
850
                break;
×
851

852
        case update_module::RebootAction::Yes:
57✔
853
                err = ctx.deployment.update_module->AsyncArtifactRollbackReboot(ctx.event_loop, handler);
57✔
854
                break;
57✔
855

856
        case update_module::RebootAction::Automatic:
×
857
                err = ctx.deployment.update_module->AsyncSystemReboot(ctx.event_loop, handler);
×
858
                break;
×
859
        }
860

861
        if (err != error::NoError) {
57✔
862
                log::Error(err.String());
×
863
                poster.PostEvent(StateEvent::Success);
×
864
        }
865
}
57✔
866

867
void UpdateVerifyRollbackRebootState::OnEnterSaveState(
60✔
868
        Context &ctx, sm::EventPoster<StateEvent> &poster) {
869
        log::Debug("Entering ArtifactVerifyRollbackReboot state");
120✔
870

871
        // In this state we only retry, we don't fail. If this keeps on going forever, then the
872
        // state loop detection will eventually kick in.
873
        auto err = ctx.deployment.update_module->AsyncArtifactVerifyRollbackReboot(
120✔
874
                ctx.event_loop, [&poster](error::Error err) {
60✔
875
                        if (err != error::NoError) {
60✔
876
                                log::Error(err.String());
22✔
877
                                poster.PostEvent(StateEvent::Retry);
22✔
878
                                return;
22✔
879
                        }
880
                        poster.PostEvent(StateEvent::Success);
38✔
881
                });
60✔
882
        if (err != error::NoError) {
60✔
883
                log::Error(err.String());
×
884
                poster.PostEvent(StateEvent::Retry);
×
885
        }
886
}
60✔
887

888
void UpdateRollbackSuccessfulState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
50✔
889
        ctx.deployment.state_data->update_info.all_rollbacks_successful = true;
50✔
890
        poster.PostEvent(StateEvent::Success);
50✔
891
}
50✔
892

893
void UpdateFailureState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
55✔
894
        log::Debug("Entering ArtifactFailure state");
110✔
895

896
        DefaultAsyncErrorHandler(
55✔
897
                poster,
898
                ctx.deployment.update_module->AsyncArtifactFailure(
55✔
899
                        ctx.event_loop, DefaultStateHandler {poster}));
55✔
900
}
55✔
901

902
static string AddInconsistentSuffix(const string &str) {
21✔
903
        const auto &suffix = main_context::MenderContext::broken_artifact_name_suffix;
904
        // `string::ends_with` is C++20... grumble
905
        string ret {str};
21✔
906
        if (!common::EndsWith(ret, suffix)) {
21✔
907
                ret.append(suffix);
21✔
908
        }
909
        return ret;
21✔
910
}
911

912
void UpdateSaveProvidesState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
75✔
913
        if (ctx.deployment.failed && !ctx.deployment.rollback_failed) {
75✔
914
                // If the update failed, but we rolled back successfully, then we don't need to do
915
                // anything, just keep the old data.
916
                poster.PostEvent(StateEvent::Success);
38✔
917
                return;
38✔
918
        }
919

920
        assert(ctx.deployment.state_data);
921
        // This state should never happen: rollback failed, but update not failed??
922
        assert(!(!ctx.deployment.failed && ctx.deployment.rollback_failed));
923

924
        // We expect Cleanup to be the next state after this.
925
        ctx.deployment.state_data->state = ctx.kUpdateStateCleanup;
37✔
926

927
        auto &artifact = ctx.deployment.state_data->update_info.artifact;
928

929
        string artifact_name;
930
        if (ctx.deployment.rollback_failed) {
37✔
931
                artifact_name = AddInconsistentSuffix(artifact.artifact_name);
38✔
932
        } else {
933
                artifact_name = artifact.artifact_name;
18✔
934
        }
935

936
        bool deploy_failed = ctx.deployment.failed;
37✔
937

938
        // Only the artifact_name and group should be committed in the case of a
939
        // failing update in order to make this consistent with the old client
940
        // behaviour.
941
        auto err = ctx.mender_context.CommitArtifactData(
74✔
942
                artifact_name,
943
                artifact.artifact_group,
37✔
944
                deploy_failed ? nullopt : optional<context::ProvidesData>(artifact.type_info_provides),
74✔
945
                /* Special case: Keep existing provides */
946
                deploy_failed ? context::ClearsProvidesData {}
74✔
947
                                          : optional<context::ClearsProvidesData>(artifact.clears_artifact_provides),
18✔
948
                [&ctx](kv_db::Transaction &txn) {
37✔
949
                        // Save the Cleanup state together with the artifact data, atomically.
950
                        return ctx.SaveDeploymentStateData(txn, *ctx.deployment.state_data);
37✔
951
                });
37✔
952
        if (err != error::NoError) {
37✔
953
                log::Error("Error saving artifact data: " + err.String());
×
954
                if (err.code
×
955
                        == main_context::MakeError(main_context::StateDataStoreCountExceededError, "").code) {
×
956
                        poster.PostEvent(StateEvent::StateLoopDetected);
×
957
                        return;
958
                }
959
                poster.PostEvent(StateEvent::Failure);
×
960
                return;
961
        }
962

963
        poster.PostEvent(StateEvent::Success);
37✔
964
}
965

966
void UpdateCleanupState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
91✔
967
        log::Debug("Entering ArtifactCleanup state");
182✔
968

969
        // It's possible for there not to be an initialized update_module structure, if the
970
        // deployment failed before we could successfully parse the artifact. If so, cleanup is a
971
        // no-op.
972
        if (!ctx.deployment.update_module) {
91✔
973
                poster.PostEvent(StateEvent::Success);
9✔
974
                return;
9✔
975
        }
976

977
        DefaultAsyncErrorHandler(
82✔
978
                poster,
979
                ctx.deployment.update_module->AsyncCleanup(ctx.event_loop, DefaultStateHandler {poster}));
164✔
980
}
981

982
void ClearArtifactDataState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
93✔
983
        auto err = ctx.mender_context.GetMenderStoreDB().WriteTransaction([](kv_db::Transaction &txn) {
93✔
984
                // Remove state data, since we're done now.
985
                auto err = txn.Remove(main_context::MenderContext::state_data_key);
91✔
986
                if (err != error::NoError) {
91✔
987
                        return err;
×
988
                }
989
                return txn.Remove(main_context::MenderContext::state_data_key_uncommitted);
91✔
990
        });
93✔
991
        if (err != error::NoError) {
93✔
992
                log::Error("Error removing artifact data: " + err.String());
2✔
993
                poster.PostEvent(StateEvent::Failure);
2✔
994
                return;
995
        }
996

997
        poster.PostEvent(StateEvent::Success);
91✔
998
}
999

1000
void StateLoopState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
2✔
1001
        assert(ctx.deployment.state_data);
1002
        auto &artifact = ctx.deployment.state_data->update_info.artifact;
1003

1004
        // Mark update as inconsistent.
1005
        string artifact_name = AddInconsistentSuffix(artifact.artifact_name);
2✔
1006

1007
        auto err = ctx.mender_context.CommitArtifactData(
6✔
1008
                artifact_name,
1009
                artifact.artifact_group,
2✔
1010
                artifact.type_info_provides,
2✔
1011
                artifact.clears_artifact_provides,
2✔
1012
                [](kv_db::Transaction &txn) { return error::NoError; });
4✔
1013
        if (err != error::NoError) {
2✔
1014
                log::Error("Error saving inconsistent artifact data: " + err.String());
×
1015
                poster.PostEvent(StateEvent::Failure);
×
1016
                return;
1017
        }
1018

1019
        poster.PostEvent(StateEvent::Success);
2✔
1020
}
1021

1022
void EndOfDeploymentState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
93✔
1023
        log::Info(
93✔
1024
                "Deployment with ID " + ctx.deployment.state_data->update_info.id
93✔
1025
                + " finished with status: " + string(ctx.deployment.failed ? "Failure" : "Success"));
297✔
1026

1027
        ctx.FinishDeploymentLogging();
93✔
1028

1029
        ctx.deployment = {};
93✔
1030
        poster.PostEvent(
93✔
1031
                StateEvent::InventoryPollingTriggered); // Submit the inventory right after an update
1032
        poster.PostEvent(StateEvent::DeploymentEnded);
93✔
1033
        poster.PostEvent(StateEvent::Success);
93✔
1034
}
93✔
1035

1036
ExitState::ExitState(events::EventLoop &event_loop) :
96✔
1037
        event_loop_(event_loop) {
96✔
1038
}
96✔
1039

1040
void ExitState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
93✔
1041
#ifndef NDEBUG
1042
        if (--iterations_left_ <= 0) {
1043
                event_loop_.Stop();
1044
        } else {
1045
                poster.PostEvent(StateEvent::Success);
1046
        }
1047
#else
1048
        event_loop_.Stop();
93✔
1049
#endif
1050
}
93✔
1051

1052
namespace deployment_tracking {
1053

1054
void NoFailuresState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
63✔
1055
        ctx.deployment.failed = false;
63✔
1056
        ctx.deployment.rollback_failed = false;
63✔
1057
}
63✔
1058

1059
void FailureState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
60✔
1060
        ctx.deployment.failed = true;
60✔
1061
        ctx.deployment.rollback_failed = true;
60✔
1062
}
60✔
1063

1064
void RollbackAttemptedState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
52✔
1065
        ctx.deployment.failed = true;
52✔
1066
        ctx.deployment.rollback_failed = false;
52✔
1067
}
52✔
1068

1069
void RollbackFailedState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
12✔
1070
        ctx.deployment.failed = true;
12✔
1071
        ctx.deployment.rollback_failed = true;
12✔
1072
}
12✔
1073

1074
} // namespace deployment_tracking
1075

1076
} // namespace daemon
1077
} // namespace update
1078
} // 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