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

mendersoftware / mender / 1070743650

13 Nov 2023 06:51PM UTC coverage: 80.107% (-0.05%) from 80.157%
1070743650

push

gitlab-ci

oleorhagen
chore: Error when trying to update with empty deployment ID

Signed-off-by: Ole Petter <ole.orhagen@northern.tech>

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

65 existing lines in 3 files now uncovered.

7011 of 8752 relevant lines covered (80.11%)

9197.49 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 int state_script_retry_exit_code {21};
43

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

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

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

70
        ifstream vf {path};
1,998✔
71

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

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

87
        if (not common::VectorContainsString(supported_state_script_versions, version)) {
999✔
88
                return executor::MakeError(
89
                        executor::VersionFileError, "Unexpected Artifact script version found: " + version);
2✔
90
        }
91
        return error::NoError;
998✔
92
}
93

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

334

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

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