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

mendersoftware / mender / 1071875915

14 Nov 2023 11:46AM UTC coverage: 80.182% (+0.08%) from 80.107%
1071875915

push

gitlab-ci

kacf
chore: Get rid of direct comparisons with `Error::code`.

Such comparisons are dangerous, because if you don't also compare the
category, you can get false positives.

Signed-off-by: Kristian Amlie <kristian.amlie@northern.tech>

12 of 12 new or added lines in 3 files covered. (100.0%)

79 existing lines in 9 files now uncovered.

6967 of 8689 relevant lines covered (80.18%)

9263.44 hits per line

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

85.93
/src/artifact/v3/scripts/executor.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 <artifact/v3/scripts/executor.hpp>
16

17
#include <algorithm>
18
#include <chrono>
19
#include <regex>
20
#include <string>
21

22
#include <common/common.hpp>
23
#include <common/expected.hpp>
24
#include <common/path.hpp>
25

26

27
namespace mender {
28
namespace artifact {
29
namespace scripts {
30
namespace executor {
31

32
namespace expected = mender::common::expected;
33

34

35
using expected::ExpectedBool;
36

37
namespace processes = mender::common::processes;
38
namespace error = mender::common::error;
39
namespace path = mender::common::path;
40

41

42
const string expected_state_script_version {"3"};
43
const int state_script_retry_exit_code {21};
44

45
unordered_map<const State, string> state_map {
46
        {State::Idle, "Idle"},
47
        {State::Sync, "Sync"},
48
        {State::Download, "Download"},
49
        {State::ArtifactInstall, "ArtifactInstall"},
50
        {State::ArtifactReboot, "ArtifactReboot"},
51
        {State::ArtifactCommit, "ArtifactCommit"},
52
        {State::ArtifactRollback, "ArtifactRollback"},
53
        {State::ArtifactRollbackReboot, "ArtifactRollbackReboot"},
54
        {State::ArtifactFailure, "ArtifactFailure"},
55
};
56

57
unordered_map<const Action, string> action_map {
58
        {Action::Enter, "Enter"},
59
        {Action::Leave, "Leave"},
60
        {Action::Error, "Error"},
61
};
62

63
error::Error CorrectVersionFile(const string &path) {
1,295✔
64
        // Missing file is OK
65
        // This is because previous versions of the client wrote no
66
        // version file, so no-file=v3
67
        if (!path::FileExists(path)) {
1,295✔
68
                return error::NoError;
297✔
69
        }
70

71
        ifstream vf {path};
1,996✔
72

73
        if (!vf) {
998✔
UNCOV
74
                auto errnum {errno};
×
75
                return error::Error(
UNCOV
76
                        generic_category().default_error_condition(errnum), "Failed to open the version file");
×
77
        }
78

79
        string version;
80
        vf >> version;
998✔
81
        if (!vf) {
998✔
UNCOV
82
                auto errnum {errno};
×
83
                return error::Error(
84
                        generic_category().default_error_condition(errnum),
×
UNCOV
85
                        "Error reading the version number from the version file");
×
86
        }
87

88
        if (version != expected_state_script_version) {
998✔
89
                return executor::MakeError(
90
                        executor::VersionFileError, "Unexpected Artifact script version found: " + version);
2✔
91
        }
92
        return error::NoError;
997✔
93
}
94

95
bool isValidStateScript(const string &file, State state, Action action) {
27,521✔
96
        string expression {
97
                "(" + state_map.at(state) + ")" + "_(" + action_map.at(action) + ")_[0-9][0-9](_\\S+)?"};
55,042✔
98
        const regex artifact_script_regexp {expression, std::regex_constants::ECMAScript};
27,521✔
99
        return regex_match(path::BaseName(file), artifact_script_regexp);
82,563✔
100
}
101

UNCOV
102
function<bool(const string &)> Matcher(State state, Action action) {
×
103
        return [state, action](const string &file) {
27,521✔
104
                const bool is_valid {isValidStateScript(file, state, action)};
27,521✔
105
                if (!is_valid) {
27,521✔
106
                        return false;
107
                }
108
                auto exp_executable = path::IsExecutable(file, true);
709✔
109
                if (!exp_executable) {
709✔
110
                        log::Debug("Issue figuring the executable bits of: " + exp_executable.error().String());
×
UNCOV
111
                        return false;
×
112
                }
113
                return is_valid and exp_executable.value();
709✔
UNCOV
114
        };
×
115
}
116

117
bool IsArtifactScript(State state) {
×
UNCOV
118
        switch (state) {
×
119
        case State::Idle:
120
        case State::Sync:
121
        case State::Download:
122
                return false;
UNCOV
123
        case State::ArtifactInstall:
×
124
        case State::ArtifactReboot:
125
        case State::ArtifactCommit:
126
        case State::ArtifactRollback:
127
        case State::ArtifactRollbackReboot:
128
        case State::ArtifactFailure:
UNCOV
129
                return true;
×
130
        }
131
        assert(false);
132
        return false;
133
}
134

135
string ScriptRunner::ScriptPath(State state) {
1,294✔
136
        if (IsArtifactScript(state)) {
137
                return this->artifact_script_path_;
637✔
138
        }
139
        return this->rootfs_script_path_;
657✔
140
}
141

142
string Name(const State state, const Action action) {
1,036✔
143
        return state_map.at(state) + action_map.at(action);
1,036✔
144
}
145

146
ScriptRunner::ScriptRunner(
3,111✔
147
        events::EventLoop &loop,
148
        chrono::milliseconds script_timeout,
149
        chrono::milliseconds retry_interval,
150
        chrono::milliseconds retry_timeout,
151
        const string &artifact_script_path,
152
        const string &rootfs_script_path,
153
        processes::OutputCallback stdout_callback,
154
        processes::OutputCallback stderr_callback) :
3,111✔
155
        loop_ {loop},
156
        script_timeout_ {script_timeout},
157
        retry_interval_ {retry_interval},
158
        retry_timeout_ {retry_timeout},
159
        artifact_script_path_ {artifact_script_path},
160
        rootfs_script_path_ {rootfs_script_path},
161
        stdout_callback_ {stdout_callback},
162
        stderr_callback_ {stderr_callback},
163
        error_script_error_ {error::NoError},
164
        retry_interval_timer_ {new events::Timer(loop_)},
3,111✔
165
        retry_timeout_timer_ {new events::Timer(loop_)} {};
6,222✔
166

167
void ScriptRunner::LogErrAndExecuteNext(
13✔
168
        Error err,
169
        vector<string>::iterator current_script,
170
        vector<string>::iterator end,
171
        bool ignore_error,
172
        HandlerFunction handler) {
173
        // Collect the error and carry on
174
        if (err.code == processes::MakeError(processes::NonZeroExitStatusError, "").code) {
13✔
175
                this->error_script_error_ = this->error_script_error_.FollowedBy(executor::MakeError(
13✔
176
                        executor::NonZeroExitStatusError,
177
                        "Got non zero exit code from script: " + *current_script));
26✔
178
        } else {
UNCOV
179
                this->error_script_error_ = this->error_script_error_.FollowedBy(err);
×
180
        }
181

182
        HandleScriptNext(current_script, end, ignore_error, handler);
13✔
183
}
13✔
184

185
void ScriptRunner::HandleScriptNext(
669✔
186
        vector<string>::iterator current_script,
187
        vector<string>::iterator end,
188
        bool ignore_error,
189
        HandlerFunction handler) {
190
        // Stop retry timer and start the next script execution
191
        if (this->retry_timeout_timer_->GetActive()) {
669✔
192
                this->retry_timeout_timer_->Cancel();
2✔
193
        }
194

195
        auto local_err = Execute(std::next(current_script), end, ignore_error, handler);
669✔
196
        if (local_err != error::NoError) {
669✔
UNCOV
197
                return handler(local_err);
×
198
        }
199
}
200

201
void ScriptRunner::HandleScriptError(Error err, HandlerFunction handler) {
1,236✔
202
        // Stop retry timer
203
        if (this->retry_timeout_timer_->GetActive()) {
1,236✔
204
                this->retry_timeout_timer_->Cancel();
1✔
205
        }
206
        if (err.code == processes::MakeError(processes::NonZeroExitStatusError, "").code) {
1,236✔
207
                return handler(executor::MakeError(
64✔
208
                        executor::NonZeroExitStatusError,
209
                        "Received error code: " + to_string(this->script_.get()->GetExitStatus())));
64✔
210
        }
211
        return handler(err);
2,408✔
212
}
213

214
void ScriptRunner::HandleScriptRetry(
30✔
215
        vector<string>::iterator current_script,
216
        vector<string>::iterator end,
217
        bool ignore_error,
218
        HandlerFunction handler) {
219
        log::Info(
30✔
220
                "Script returned Retry Later exit code, re-retrying in "
221
                + to_string(chrono::duration_cast<chrono::seconds>(this->retry_interval_).count()) + "s");
60✔
222

223
        this->retry_interval_timer_->AsyncWait(
30✔
224
                this->retry_interval_,
225
                [this, current_script, end, ignore_error, handler](error::Error err) {
60✔
226
                        if (err != error::NoError) {
30✔
227
                                return handler(this->error_script_error_.FollowedBy(err));
12✔
228
                        }
229

230
                        auto local_err = Execute(current_script, end, ignore_error, handler);
24✔
231
                        if (local_err != error::NoError) {
24✔
UNCOV
232
                                handler(local_err);
×
233
                        }
234
                });
60✔
235
}
30✔
236

237
void ScriptRunner::MaybeSetupRetryTimeoutTimer() {
30✔
238
        if (!this->retry_timeout_timer_->GetActive()) {
30✔
239
                log::Debug("Setting retry timer for " + to_string(this->retry_timeout_.count()) + "ms");
18✔
240
                // First run on this script
241
                this->retry_timeout_timer_->AsyncWait(this->retry_timeout_, [this](error::Error err) {
15✔
242
                        if (err.code == make_error_condition(errc::operation_canceled)) {
7✔
243
                                // The timer did not fire up. Do nothing
244
                        } else {
245
                                log::Error("Script Retry Later timeout out, cancelling and returning");
12✔
246
                                this->retry_interval_timer_->Cancel();
6✔
247
                                this->script_->Cancel();
6✔
248
                        }
249
                });
16✔
250
        }
251
}
30✔
252

253
Error ScriptRunner::Execute(
1,935✔
254
        vector<string>::iterator current_script,
255
        vector<string>::iterator end,
256
        bool ignore_error,
257
        HandlerFunction handler) {
258
        // No more scripts to execute
259
        if (current_script == end) {
1,935✔
260
                HandleScriptError(this->error_script_error_, handler); // Success
2,406✔
261
                return error::NoError;
1,203✔
262
        }
263

264
        log::Info("Running State Script: " + *current_script);
732✔
265

266
        this->script_.reset(new processes::Process({*current_script}));
2,196✔
267
        auto err {this->script_->Start(stdout_callback_, stderr_callback_)};
1,464✔
268
        if (err != error::NoError) {
732✔
UNCOV
269
                return err;
×
270
        }
271

272
        return this->script_.get()->AsyncWait(
273
                this->loop_,
274
                [this, current_script, end, ignore_error, handler](Error err) {
4,466✔
275
                        if (err != error::NoError) {
732✔
276
                                const bool is_script_retry_error =
277
                                        err.code == processes::MakeError(processes::NonZeroExitStatusError, "").code
152✔
278
                                        && this->script_->GetExitStatus() == state_script_retry_exit_code;
76✔
279
                                if (is_script_retry_error) {
76✔
280
                                        MaybeSetupRetryTimeoutTimer();
30✔
281
                                        return HandleScriptRetry(current_script, end, ignore_error, handler);
60✔
282
                                } else if (ignore_error) {
46✔
283
                                        return LogErrAndExecuteNext(err, current_script, end, ignore_error, handler);
26✔
284
                                }
285
                                return HandleScriptError(err, handler);
66✔
286
                        }
287
                        return HandleScriptNext(current_script, end, ignore_error, handler);
1,312✔
288
                },
289
                this->script_timeout_);
1,464✔
290
}
291

292
Error ScriptRunner::AsyncRunScripts(
1,295✔
293
        State state, Action action, HandlerFunction handler, OnError on_error) {
294
        // Verify the version in the version file (OK if no version file present)
295
        auto version_file_error {CorrectVersionFile(path::Join(
1,295✔
296
                IsArtifactScript(state) ? this->artifact_script_path_ : this->rootfs_script_path_,
297
                "version"))};
1,295✔
298
        if (version_file_error != error::NoError) {
1,295✔
299
                return version_file_error;
1✔
300
        }
301

302
        // Collect
303
        const auto script_path {ScriptPath(state)};
1,294✔
304
        auto exp_scripts {path::ListFiles(script_path, Matcher(state, action))};
2,588✔
305
        if (!exp_scripts) {
1,294✔
306
                // Missing directory is OK
307
                if (exp_scripts.error().IsErrno(ENOENT)) {
52✔
308
                        log::Debug("Found no state script directory (" + script_path + "). Continuing on");
104✔
309
                        handler(error::NoError);
52✔
310
                        return error::NoError;
52✔
311
                }
312
                return executor::MakeError(
313
                        executor::Code::CollectionError,
UNCOV
314
                        "Failed to get the scripts, error: " + exp_scripts.error().String());
×
315
        }
316

317
        // Sort
318
        {
319
                auto &unsorted_scripts {exp_scripts.value()};
1,242✔
320

321
                vector<string> sorted_scripts(unsorted_scripts.begin(), unsorted_scripts.end());
2,484✔
322

323
                sort(sorted_scripts.begin(), sorted_scripts.end());
1,242✔
324
                this->collected_scripts_ = std::move(sorted_scripts);
1,242✔
325
        }
326

327
        bool ignore_error = on_error == OnError::Ignore || action == Action::Error;
1,242✔
328

329
        // Execute
330
        auto scripts_iterator {this->collected_scripts_.begin()};
331
        auto scripts_iterator_end {this->collected_scripts_.end()};
332
        return Execute(scripts_iterator, scripts_iterator_end, ignore_error, handler);
2,484✔
333
}
334

335

336
Error ScriptRunner::RunScripts(State state, Action action, OnError on_error) {
251✔
337
        auto run_err {error::NoError};
251✔
338
        auto err = AsyncRunScripts(
339
                state,
340
                action,
341
                [this, &run_err](Error error) {
502✔
342
                        run_err = error;
251✔
343
                        this->loop_.Stop();
251✔
344
                },
251✔
345
                on_error);
251✔
346
        if (err != error::NoError) {
251✔
UNCOV
347
                return err;
×
348
        }
349
        this->loop_.Run();
251✔
350
        return run_err;
251✔
351
}
352

353
} // namespace executor
354
} // namespace scripts
355
} // namespace artifact
356
} // 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

© 2025 Coveralls, Inc