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

suculent / thinx-device-api / #252646768

01 Nov 2025 03:31PM UTC coverage: 46.298% (-25.7%) from 71.971%
#252646768

push

suculent
testing upgraded mqtt package

1123 of 3470 branches covered (32.36%)

Branch coverage included in aggregate %.

5324 of 10455 relevant lines covered (50.92%)

4.07 hits per line

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

4.26
/lib/thinx/builder.js
1
/** This THiNX Device Management API module is responsible for managing builds and should be offloadable to another server. */
2

3
const Globals = require("./globals.js");
1✔
4
const app_config = Globals.app_config();
1✔
5
const prefix = Globals.prefix();
1✔
6

7
const Filez = require("./files.js");
1✔
8
let ROOT = Filez.appRoot();
1✔
9

10
const { v1: uuidV1 } = require('uuid');
1✔
11
const { readdirSync } = require('fs');
1✔
12

13
const mkdirp = require("mkdirp");
1✔
14
const exec = require("child_process");
1✔
15
const fs = require("fs-extra");
1✔
16
const path = require("path");
1✔
17
const finder = require("fs-finder");
1✔
18
const YAML = require('yaml');
1✔
19
const chmodr = require('chmodr');
1✔
20
const CryptoJS = require("crypto-js");
1✔
21

22
const Git = require("./git"); const git = new Git();
1✔
23

24
const Database = require("./database");
1✔
25
let db_uri = new Database().uri();
1✔
26
let devicelib = require("nano")(db_uri).use(prefix + "managed_devices");
1✔
27
let userlib = require("nano")(db_uri).use(prefix + "managed_users");
1✔
28

29
const APIEnv = require("./apienv");
1✔
30
const ApiKey = require("./apikey");
1✔
31
const Platform = require("./platform");
1✔
32
const JSON2H = require("./json2h");
1✔
33

34
const BuildLog = require("./buildlog"); let blog = new BuildLog();
1✔
35
const Sanitka = require("./sanitka"); let sanitka = new Sanitka();
1✔
36
const Sources = require("./sources");
1✔
37

38
const InfluxConnector = require('./influx');
1✔
39
const Util = require("./util.js");
1✔
40

41
module.exports = class Builder {
1✔
42

43
        constructor(redis) {
44
                this.io = null;
4✔
45
                this.apienv = new APIEnv(redis);
4✔
46
                this.apikey = new ApiKey(redis);
4✔
47
                this.sources = new Sources();
4✔
48
        }
49

50
        setIo(io) {
51
                this.io = io;
×
52
        }
53

54
        /* Used to decrypt secure WiFi credentials */
55
        decrypt(cryptokey, inputHex) {
56

57
                let plain;
58

59
                if ((typeof (inputHex) === "undefined") || (inputHex === null) ||
×
60
                        (typeof (cryptokey) === "undefined") || (cryptokey === null)) {
61
                        console.log("Fatal Error - Invalid decryption data: [" + inputHex + "] [" + "***" + cryptokey.slice(cryptokey.length - 3) + "]");
×
62
                }
63

64
                try {
×
65
                        let reb64 = CryptoJS.enc.Base64.parse(inputHex);
×
66
                        let bytes = reb64.toString(CryptoJS.enc.Base64);
×
67
                        let decrypt = CryptoJS.AES.decrypt(bytes, cryptokey);
×
68
                        plain = decrypt.toString(CryptoJS.enc.Utf8);
×
69
                } catch (de) {
70
                        console.log("Decryption error", de);
×
71
                }
72

73
                return plain;
×
74
        }
75

76
        wsOK(websocket, _message, udid) {
77
                if (typeof (websocket) !== "undefined" && websocket !== null) {
×
78
                        try {
×
79
                                websocket.send(JSON.stringify({
×
80
                                        notification: {
81
                                                title: "Build Status",
82
                                                body: "Completed",
83
                                                type: "success",
84
                                                udid: udid,
85
                                                message: _message
86
                                        }
87
                                }));
88
                        } catch (e) {
89
                                console.log(e);
×
90
                        }
91
                }
92
        }
93

94
        buildGuards(callback, owner, git, branch) {
95
                if ((typeof (owner) === "undefined") || (owner === null)) {
×
96
                        console.log("owner is undefined, exiting!");
×
97
                        if (typeof (callback) !== "undefined") callback(false, "owner undefined");
×
98
                        return false;
×
99
                }
100

101
                if ((typeof (git) === "undefined") || (git === null)) {
×
102
                        console.log("git is undefined, exiting!");
×
103
                        if ((typeof (callback) !== "undefined") && (callback !== null)) callback(false, "git undefined");
×
104
                        return false;
×
105
                }
106

107
                if ((typeof (branch) === "undefined") || (branch === null)) {
×
108
                        console.log("branch is undefined, exiting!");
×
109
                        if ((typeof (callback) !== "undefined") && (callback !== null)) callback(false, "branch undefined");
×
110
                        return false;
×
111
                }
112
                return true;
×
113
        }
114

115
        sendNotificationIfSocketAlive(websocket, notification) {
116

117
                if (typeof (websocket) === "undefined" || websocket === null) {
×
118
                        return;
×
119
                }
120

121
                try {
×
122
                        if (websocket.isAlive) {
×
123
                                websocket.send(notification, function ack(/* error */) {
×
124
                                        /* We ignore errors here, the socket may have been closed anytime. */
125
                                });
126
                        } else {
127
                                console.log("Skipping dead websocket notification.");
×
128
                        }
129
                } catch (e) {
130
                        console.log("[builder] ws_send_exception" + e);
×
131
                }
132
        }
133

134
        successStringFromBool(success) {
135
                let successString;
136
                if (success) {
×
137
                        successString = "success"; // green
×
138
                } else {
139
                        successString = "error"; // orange
×
140
                }
141
                return successString;
×
142
        }
143

144
        buildStatusNotification(message, messageType, udid, build_id) {
145
                return JSON.stringify({
×
146
                        notification: this.buildNotification(message, messageType, udid, build_id)
147
                });
148
        }
149

150
        buildNotification(message, messageType, udid, build_id) {
151
                return {
×
152
                        title: "Build Status",
153
                        body: message.toString(),
154
                        type: messageType,
155
                        udid: udid,
156
                        build_id: build_id
157
                };
158
        }
159

160
        // Used to sends a build status notification using websocket
161
        notify(udid, build_id, notifiers, message, success_status) {
162

163
                if ((typeof (message) === "undefined") || (message === null)) {
×
164
                        console.log("[error] builder:notify - No message given in notify()");
×
165
                        return;
×
166
                }
167

168
                let status = this.successStringFromBool(success_status);
×
169

170
                if (message.indexOf("build_running") !== -1) {
×
171
                        status = "info"; // blue
×
172
                }
173

174
                if (message == "OK") {
×
175
                        status = "success";
×
176
                }
177

178
                this.sendNotificationIfSocketAlive(
×
179
                        notifiers.websocket,
180
                        this.buildStatusNotification(message, status, udid, build_id)
181
                );
182
        }
183

184
        getLastAPIKey(owner, callback) {
185
                console.log("[builder] Fetching LAST API Key for owner " + owner);
×
186
                this.apikey.get_last_apikey(owner, callback);
×
187
        }
188

189
        runRemoteShell(worker, CMD, owner, build_id, udid, notifiers, source_id) {
190

191
                if ((typeof (worker) === "undefined") || (typeof (worker.socket) === "undefined")) {
×
192
                        let message = "ERROR: worker needs socket for remote builds";
×
193
                        console.log(message);
×
194
                        this.notify(
×
195
                                udid,
196
                                build_id,
197
                                notifiers,
198
                                message,
199
                                this.successStringFromBool(false)
200
                        );
201
                        return;
×
202
                }
203

204
                const BUILD_PATH = app_config.data_root + app_config.build_root + "/" + owner + "/" + udid + "/" + build_id;
×
205

206
                let job = {
×
207
                        mock: false,
208
                        build_id: build_id,
209
                        source_id: source_id,
210
                        owner: owner,
211
                        udid: udid,
212
                        path: BUILD_PATH,
213
                        cmd: CMD,
214
                        secret: process.env.WORKER_SECRET || null
×
215
                };
216

217
                let copy = JSON.parse(JSON.stringify(job));
×
218
                if ((typeof (copy.secret) !== "undefined") && (copy.secret !== null)) {
×
219
                        copy.secret = "****"; // mask secrets in log
×
220
                }
221

222
                if (this.io !== null) {
×
223
                        this.io.emit('job', job);
×
224
                } else {
225
                        console.log("☣️ [error] [build] io socket null");
×
226
                        return;
×
227
                }
228

229
                let options = {
×
230
                        owner: owner,
231
                        build_id: build_id,
232
                        source_id: source_id,
233
                        udid: udid,
234
                        notifiers: notifiers
235
                };
236

237
                if (typeof (worker.socket) === "undefined") {
×
238
                        console.log(`[OID:${owner}] [BUILD_FAILED] REMOTE execution failed; no socket.`);
×
239
                        return InfluxConnector.statsLog(owner, "BUILD_FAILED", build_id);
×
240
                }
241

242
                worker.socket.on('log', (data) => {
×
243
                        this.processShellData(options, data);
×
244
                });
245

246
                worker.socket.on('job-status', (data) => {
×
247
                        // Worker sends 'job-status' on exit event, not a build-id event
248
                        this.processExitData(owner, build_id, udid, notifiers, data);
×
249

250
                        if (data.status == "OK") {
×
251
                                this.cleanupDeviceRepositories(owner, udid, build_id);
×
252
                        }
253
                });
254
                console.log(`[OID:${owner}] [BUILD_STARTED]`);
×
255
                InfluxConnector.statsLog(owner, "BUILD_STARTED", build_id);
×
256
        }
257

258
        getDirectories(source) {
259
                return readdirSync(source, { withFileTypes: true }) // lgtm [js/path-injection]
×
260
                        .filter(dirent => dirent.isDirectory())
×
261
                        .map(dirent => dirent.name); // lgtm [js/path-injection]
×
262
        }
263

264
        // Should delete all previous repositories except for the latest build (on success only)
265
        cleanupDeviceRepositories(owner, udid, build_id) {
266

267
                let s_owner = sanitka.owner(owner);
×
268
                let s_udid = sanitka.udid(udid);
×
269
                let keep_build_id = sanitka.udid(build_id);
×
270

271
                if ((s_owner == null) || (s_udid == null)) {
×
272
                        console.log("☣️ [error] cleanup failed for udid/owner and keep_build_id", udid, owner, build_id);
×
273
                        return;
×
274
                }
275

276
                const device_path = app_config.data_root + "/repos" + "/" + s_owner + "/" + s_udid;
×
277
                const keep_path = device_path + "/" + keep_build_id;
×
278
                let all = this.getDirectories(keep_path);
×
279
                for (let index in all) {
×
280
                        let directory = all[index];
×
281
                        if (directory.indexOf(keep_build_id) == -1) {
×
282
                                let delete_path = device_path + "/" + directory;
×
283
                                fs.remove(delete_path); // lgtm [js/path-injection]
×
284
                        }
285
                }
286
        }
287

288
        processExitData(owner, build_id, udid, notifiers, data) {
289
                if ((typeof (data.status) !== "undefined") &&
×
290
                        (data.status != null) &&
291
                        (data.status.indexOf("OK") == 0)) {
292
                        this.cleanupDeviceRepositories(owner, udid, build_id);
×
293
                }
294
                this.notify(udid, build_id, notifiers, data.status, false);
×
295
                if (data.status !== "OK") {
×
296
                        blog.state(build_id, owner, udid, data.status);
×
297
                }
298
                this.wsOK(notifiers.websocket, data.status, udid);
×
299
        }
300

301
        processShellError(owner, build_id, udid, data) {
302
                let dstring = data.toString();
×
303
                console.log("[STDERR] " + data);
×
304
                if (dstring.indexOf("fatal:") !== -1) {
×
305
                        blog.state(build_id, owner, udid, "FAILED");
×
306
                }
307
        }
308

309
        processShellData(opts, data) {
310

311
                if (typeof (data) === "object") return;
×
312
                let logline = data;
×
313
                if (logline.length > 1) { // skip empty lines in log
×
314
                        logline = logline.replace("\n\n", "\n"); // strip duplicate newlines in log, ignore lint warnings here
×
315

316
                        // Strip only first line for console logging
317
                        console.log("[" + opts.build_id + "] »»", logline.replace("\n", "")); /* lgtm [js/incomplete-sanitization] */
×
318

319
                        // just a hack while shell.exit does not work or fails with another error
320
                        if ((logline.indexOf("STATUS OK") !== -1) || // old
×
321
                                (logline.indexOf("status: OK") !== -1)) // new
322
                        {
323
                                this.notify(opts.udid, opts.build_id, opts.notifiers, "Completed", true);
×
324
                                blog.state(opts.build_id, opts.owner, opts.udid, "Success");
×
325
                                this.wsOK(opts.notifiers.websocket, "Build successful.", opts.udid);
×
326

327
                                this.sources.update(opts.owner, opts.source_id, "last_build", opts.version, (result) => {
×
328
                                        console.log("🔨 [debug] updateLastBuild result:", result);
×
329
                                });
330
                        }
331
                }
332

333
                // Tries to send but socket will be probably closed.
334
                this.sendNotificationIfSocketAlive(
×
335
                        opts.notifiers.websocket,
336
                        logline
337
                );
338
        }
339

340
        runShell(XBUILD_PATH, CMD, owner, build_id, udid, notifiers) {
341
                let shell = exec.spawn(CMD, { shell: true }); // lgtm [js/command-line-injection]
×
342
                shell.stdout.on("data", (data) => {
×
343
                        this.processShellData(owner, build_id, udid, notifiers, data);
×
344
                });
345
                shell.stderr.on("data", (data) => {
×
346
                        this.processShellError(owner, build_id, udid, data);
×
347
                });
348
                shell.on("exit", (code) => {
×
349
                        console.log(`[OID:${owner}] [BUILD_COMPLETED] LOCAL [builder] with code ${code}`);
×
350
                        // success error code is processed using job-status parser
351
                        if (code !== 0) {
×
352
                                this.processExitData(owner, build_id, udid, notifiers, code);
×
353
                        }
354
                        this.cleanupSecrets(XBUILD_PATH);
×
355
                });
356
        }
357

358
        containsNullOrUndefined(array) {
359
                for (let index in array) {
×
360
                        const item = array[index];
×
361
                        if (typeof (item) === "undefined") return false;
×
362
                        if (item === null) return false;
×
363
                        if (item.length === 0) return false;
×
364
                }
365
                return true;
×
366
        }
367

368
        // DevSec uses only 2nd part of MAC, because beginning
369
        // is always same for ESPs and thus easily visible.
370
        formatMacForDevSec(incoming) {
371
                let outgoing;
372
                let no_colons = incoming.replace(/:/g, "");
×
373
                if (no_colons.length == 12) {
×
374
                        outgoing = no_colons.substr(6, 12);
×
375
                } else {
376
                        outgoing = no_colons;
×
377
                }
378
                return outgoing;
×
379
        }
380

381
        // fetch last git tag in repository or return 1.0
382
        getTag(rpath) {
383
                let git_tag = null;
×
384
                try {
×
385
                        git_tag = exec.execSync(`cd ${rpath}; git describe --abbrev=0 --tags`).toString();
×
386
                } catch (e) {
387
                        git_tag = "1.0";
×
388
                }
389
                if (git_tag === null) {
×
390
                        git_tag = "1.0";
×
391
                }
392
                console.log(`ℹ️ [info] [builder] Tried to fetch GIT tag at ${rpath} with result ${git_tag}`);
×
393
                return git_tag;
×
394
        }
395

396
        gitCloneAndPullCommand(BUILD_PATH, sanitized_url, sanitized_branch) {
397
                return (
×
398
                        `cd ${BUILD_PATH}; rm -rf ./*; ` +
399
                        `if $(git clone "${sanitized_url}" -b "${sanitized_branch}");` +
400
                        `then cd *; ` +
401
                        `git pull origin ${sanitized_branch} --recurse-submodules --rebase; ` +
402
                        `chmod -R 666 *; ` + // writeable, but not executable
403
                        `echo { "basename":"$(basename $(pwd))", "branch":"${sanitized_branch}" } > ../basename.json;` +
404
                        `fi`
405
                );
406
        }
407

408
        prefetchPublic(SHELL_FETCH) {
409
                try {
×
410
                        let git_result = null;
×
411
                        console.log("[builder] Attempting public git fetch...");
×
412
                        git_result = exec.execSync(SHELL_FETCH).toString().replace("\n", "");
×
413
                        console.log(`ℹ️ [info] [build] public prefetch git resut ${git_result}`);
×
414
                } catch (e) {
415
                        console.log(`[builder] git_fetch_exception ${e}`);
×
416
                        // will try again with private keys...
417
                }
418
        }
419

420
        prefetchPrivate(br, SHELL_FETCH, BUILD_PATH) {
421

422
                let build_id = br.build_id;
×
423
                let owner = br.owner;
×
424
                let udid = br.udid;
×
425
                let source_id = br.source_id;
×
426

427
                // this checks whether the previous (public) prefetch did succeed. if yes; skips...
428
                let ptemplate = BUILD_PATH + "/basename.json";
×
429
                let exists = fs.existsSync(ptemplate); // must be file                                                
×
430
                if (exists) return true;
×
431

432
                // fetch using owner keys...
433
                console.log("builder] Fetching using SSH keys...");
×
434
                let success = git.fetch(owner, SHELL_FETCH, BUILD_PATH);
×
435
                if (!success) {
×
436
                        console.log("☣️ [error] Git prefetchPrivate FAILED for build_id", build_id, "owner", owner, "udid", udid);
×
437
                        blog.state(build_id, owner, udid, "error");
×
438
                        return false;
×
439
                }
440

441
                // update repository privacy status and return
442
                this.sources.update(owner, source_id, "is_private", true, (xuccess, error) => {
×
443
                        if (xuccess) {
×
444
                                console.log(`ℹ️ [info] repo privacy status updated to is_private=true; should prevent future public fetches`);
×
445
                        } else {
446
                                console.log(`[critical] updating repo privacy status failed with error ${error}`);
×
447
                        }
448
                        return xuccess;
×
449
                });
450
        }
451

452
        generate_thinx_json(api_envs, device, api_key, commit_id, git_tag, XBUILD_PATH) {
453

454
                // Load template
455
                let json = JSON.parse(
×
456
                        fs.readFileSync(
457
                                __dirname + "/../../builder.thinx.dist.json"
458
                        )
459
                );
460

461
                if (typeof (api_envs) === "undefined" || api_envs === null) {
×
462
                        console.log("[builder] No env vars to apply...");
×
463
                        api_envs = [];
×
464
                }
465

466
                if (api_envs.count > 0) {
×
467
                        console.log("[builder] Applying environment vars...");
×
468
                        for (let object in api_envs) {
×
469
                                let key = Object.keys(object)[0];
×
470
                                console.log("Setting " + key + " to " + object[key]);
×
471
                                json[key] = object[key];
×
472
                        }
473
                } else {
474
                        console.log("[builder] No environment vars to apply...");
×
475
                }
476

477
                // Attach/replace with important data
478
                json.THINX_ALIAS = device.alias;
×
479
                json.THINX_API_KEY = api_key; // inferred from last_key_hash
×
480

481
                // Replace important data...
482
                json.THINX_COMMIT_ID = commit_id.replace("\n", "");
×
483
                json.THINX_FIRMWARE_VERSION_SHORT = git_tag.replace("\n", "");
×
484

485
                let REPO_NAME = XBUILD_PATH.replace(/^.*[\\\/]/, '').replace(".git", "");
×
486

487
                json.THINX_FIRMWARE_VERSION = REPO_NAME + ":" + git_tag.replace("\n", "");
×
488
                json.THINX_APP_VERSION = json.THINX_FIRMWARE_VERSION;
×
489

490
                json.THINX_OWNER = device.owner;
×
491
                json.THINX_PLATFORM = device.platform;
×
492
                json.LANGUAGE_NAME = JSON2H.languageNameForPlatform(device.platform);
×
493
                json.THINX_UDID = device.udid;
×
494

495
                // Attach/replace with more specific data...");
496
                json.THINX_CLOUD_URL = app_config.api_url.replace("https://", "").replace("http://", "");
×
497
                json.THINX_MQTT_URL = app_config.mqtt.server.replace("mqtt://", ""); // due to problem with slashes in json and some libs on platforms
×
498
                json.THINX_AUTO_UPDATE = true; // device.autoUpdate
×
499
                json.THINX_MQTT_PORT = app_config.mqtt.port;
×
500
                json.THINX_API_PORT = app_config.port;
×
501
                json.THINX_ENV_SSID = "";
×
502
                json.THINX_ENV_PASS = "";
×
503

504
                if (typeof (app_config.secure_port) !== "undefined") {
×
505
                        json.THINX_API_PORT_SECURE = app_config.secure_port;
×
506
                }
507

508
                json.THINX_AUTO_UPDATE = device.auto_update;
×
509
                json.THINX_FORCED_UPDATE = false;
×
510

511
                return json;
×
512
        }
513

514
        createBuildPath(BUILD_PATH) {
515
                let mkresult = mkdirp.sync(BUILD_PATH);
×
516
                if (!mkresult) {
×
517
                        console.log("[ERROR] mkdirp.sync ended with with:", mkresult);
×
518
                        return;
×
519
                }
520
                chmodr(BUILD_PATH, 0o766, (cherr) => {
×
521
                        if (cherr) {
×
522
                                console.log('Failed to execute chmodr', cherr);
×
523
                        } else {
524
                                console.log("[builder] BUILD_PATH permission change successful.");
×
525
                        }
526
                });
527
        }
528

529
        run_build(br, notifiers, callback, transmit_key) {
530

531
                let start_timestamp = new Date().getTime();
×
532

533
                console.log("[builder] [BUILD_STARTED] at", start_timestamp);
×
534

535
                let build_id = br.build_id;
×
536
                let owner = br.owner;
×
537
                let git = br.git;
×
538
                let branch = br.branch;
×
539
                let udid = br.udid;
×
540
                let source_id = br.source_id;
×
541

542
                if ((typeof (br.worker) === "undefined") || (typeof (br.worker.socket) === "undefined")) {
×
543
                        InfluxConnector.statsLog(br.owner, "BUILD_FAILED", br.build_id);
×
544
                        if (process.env.ENVIRONMENT !== "test") {
×
545
                                return callback(false, "workers_not_ready");
×
546
                        }
547
                }
548

549
                if (!this.buildGuards(callback, owner, git, branch)) {
×
550
                        InfluxConnector.statsLog(owner, "BUILD_FAILED", build_id);
×
551
                }
552

553
                blog.log(build_id, owner, udid, "started"); // may take time to save, initial record to be edited using blog.state
×
554

555
                if ((build_id.length > 64)) return callback(false, "invalid_build_id");
×
556

557
                // Fetch device info to validate owner and udid
558
                console.log("[builder] Fetching device " + udid + " for owner " + owner);
×
559

560
                devicelib.get(udid, (err, device) => {
×
561

562
                        if (err) return callback(false, "no_such_udid");
×
563

564
                        const BUILD_PATH = app_config.data_root + app_config.build_root + "/" + device.owner + "/" + device.udid + "/" + sanitka.udid(build_id);
×
565

566
                        // Embed Authentication
567
                        this.getLastAPIKey(owner, (success, api_key) => {
×
568

569
                                if (!success || (api_key === null)) {
×
570
                                        console.log("Build requires API Key.");
×
571
                                        blog.state(build_id, owner, udid, "error");
×
572
                                        return callback(false, "build_requires_api_key");
×
573
                                }
574

575
                                this.createBuildPath(BUILD_PATH);
×
576

577
                                this.notify(udid, build_id, notifiers, "Pulling repository", true);
×
578

579
                                // Error may be emutted if document does not exist here.
580
                                // blog.state(build_id, owner, udid, "started"); // initial state from "created", seems to work...
581

582
                                console.log("[builder] Build path created:", BUILD_PATH);
×
583

584
                                //
585
                                // Fetch GIT repository
586
                                //
587

588
                                if (!Util.isDefined(branch)) branch = "origin/main";
×
589
                                let sanitized_branch = sanitka.branch(branch);
×
590
                                if (branch === null) sanitized_branch = "main";
×
591
                                let sanitized_url = sanitka.url(git);
×
592

593
                                // may fail if path already exists (because it is not pull)
594
                                const SHELL_FETCH = this.gitCloneAndPullCommand(BUILD_PATH, sanitized_url, sanitized_branch);
×
595

596
                                // Attempts to fetch GIT repo, if not marked as private                                
597
                                if (!br.is_private) this.prefetchPublic(SHELL_FETCH, BUILD_PATH);
×
598

599
                                // Attempts to fetch git repo as private using SSH keys, otherwise fails
600
                                if (!this.prefetchPrivate(br, SHELL_FETCH, BUILD_PATH)) return callback(false, "git_fetch_failed");
×
601

602
                                //
603
                                // Cound files
604
                                //
605

606
                                let files = fs.readdirSync(BUILD_PATH);
×
607
                                let directories = fs.readdirSync(BUILD_PATH).filter(
×
608
                                        file => fs.lstatSync(path.join(BUILD_PATH, file)).isDirectory()
×
609
                                );
610

611
                                if ((files.length == 0) && (directories.length == 0)) {
×
612
                                        blog.state(build_id, owner, udid, "error");
×
613
                                        return callback(false, "git_fetch_failed_private");
×
614
                                }
615

616
                                // Adjust XBUILD_PATH (build path incl. inferred project folder, should be one.)
617
                                let XBUILD_PATH = BUILD_PATH;
×
618

619
                                if (directories.length > 1) {
×
620
                                        XBUILD_PATH = BUILD_PATH + "/" + directories[1]; // 1 is always git
×
621
                                        console.log("[builder] ERROR, TOO MANY DIRECTORIES!");
×
622
                                }
623

624
                                if (directories.length === 1) XBUILD_PATH = BUILD_PATH + "/" + directories[0];
×
625

626
                                console.log("[builder] XBUILD_PATH: " + XBUILD_PATH);
×
627

628
                                // static, async inside
629
                                Platform.getPlatform(XBUILD_PATH, (get_success, platform) => {
×
630

631
                                        if (!get_success) {
×
632
                                                console.log("[builder] failed on unknown platform" + platform);
×
633
                                                this.notify(udid, build_id, notifiers, "error_platform_unknown", false);
×
634
                                                blog.state(build_id, owner, udid, "error");
×
635
                                                callback(false, "unknown platform: " + platform);
×
636
                                                return;
×
637
                                        }
638

639
                                        // feature/fix ->
640
                                        //
641
                                        // Verify firmware vs. device MCU compatibility (based on thinx.yml compiler definitions)
642
                                        //
643

644
                                        platform = device.platform;
×
645

646
                                        let platform_array = platform.split(":");
×
647
                                        let device_platform = platform_array[0]; // should work even without delimiter
×
648
                                        let device_mcu = platform_array[1];
×
649

650
                                        const yml_path = XBUILD_PATH + "/thinx.yml";
×
651
                                        const isYAML = fs.existsSync(yml_path);
×
652

653
                                        let y_platform = device_platform;
×
654

655
                                        if (isYAML) {
×
656

657
                                                const y_file = fs.readFileSync(yml_path, 'utf8');
×
658
                                                const yml = YAML.parse(y_file);
×
659

660
                                                if (typeof (yml) !== "undefined") {
×
661
                                                        // This takes first key. It could be possible to have more keys (array allows same names)
662
                                                        // and find the one with closest platform.
663
                                                        y_platform = Object.keys(yml)[0];
×
664
                                                        console.log("[builder] YAML-based platform: " + y_platform);
×
665
                                                        const y_mcu = yml[y_platform].arch;
×
666
                                                        if ((typeof (y_mcu) !== "undefined") && (typeof (device_mcu) !== "undefined")) {
×
667
                                                                if (y_mcu.indexOf(device_mcu) == -1) {
×
668
                                                                        const message = "[builder] MCU defined by thinx.yml (" + y_mcu + ") not compatible with this device MCU: " + device_mcu;
×
669
                                                                        console.log(message);
×
670
                                                                        this.notify(udid, build_id, notifiers, message, false);
×
671
                                                                        blog.state(build_id, owner, udid, "error");
×
672
                                                                        callback(false, message);
×
673
                                                                        return;
×
674
                                                                } else {
675
                                                                        console.log("[builder] MCU is compatible.");
×
676
                                                                }
677
                                                        }
678

679
                                                        // Decrypt cpass and cssid using transmit_key and overwrite in YML file
680
                                                        // FROM NOW ON, ALL OPERATIONS MUST CLEAR REPO ON EXIT!
681
                                                        // LIKE: this.cleanupSecrets(BUILD_PATH);
682
                                                        if (typeof (transmit_key) !== "undefined" && transmit_key !== null) {
×
683
                                                                // if there is no devsec defined, uses shared ckey...
684
                                                                if ((typeof (yml.devsec) == "undefined") || (yml.devsec == null)) {
×
685
                                                                        console.log("DevSec not defined, using transmit_key as CKEY! in", { yml });
×
686
                                                                        yml.devsec = {
×
687
                                                                                ckey: transmit_key
688
                                                                        };
689
                                                                }
690
                                                                if (typeof (device.environment) !== "undefined") {
×
691
                                                                        if (typeof (device.environment.cssid) !== "undefined") {
×
692
                                                                                let d_cssid = this.decrypt(transmit_key, device.environment.cssid);
×
693
                                                                                try {
×
694
                                                                                        yml.devsec.ssid = d_cssid;
×
695
                                                                                } catch (e) {
696
                                                                                        console.log(e, "fixing...");
×
697
                                                                                        yml.devsec = {};
×
698
                                                                                } finally {
699
                                                                                        yml.devsec.ssid = d_cssid;
×
700
                                                                                }
701
                                                                        }
702
                                                                        if (typeof (device.environment.cpass) !== "undefined") {
×
703
                                                                                let d_cpass = this.decrypt(transmit_key, device.environment.cpass);
×
704
                                                                                yml.devsec.pass = d_cpass;
×
705
                                                                        }
706
                                                                }
707
                                                                let insecure = YAML.stringify(yml); // A bit insecure but reasonably cheap... SSID/PASS may leak in build, which WILL be fixed later (build deletion, specific secret cleanup)
×
708
                                                                fs.writeFileSync(yml_path, insecure, 'utf8');
×
709
                                                        } else {
710
                                                                console.log("[warning] no transmit_key; environment variables in build will not be secured until build!");
×
711
                                                        }
712
                                                }
713
                                        } else {
714
                                                console.log("[builder] BuildCommand-Detected platform (no YAML at " + yml_path + "): " + platform);
×
715
                                        } // isYAML
716

717
                                        // <- platform_descriptor needs header (maybe only, that's OK)
718

719
                                        /* Export device specific env-vars to environment.json, should decrypt later as well */
720
                                        let device_specific_environment = device.environment;
×
721
                                        if (typeof (device_specific_environment) !== "undefined") {
×
722
                                                let envString = JSON.stringify(device_specific_environment);
×
723
                                                let envFile = XBUILD_PATH + '/environment.json';
×
724
                                                console.log("Saving device-specific envs to", envFile);
×
725
                                                if (fs.existsSync(XBUILD_PATH)) { // validate to prevent injection
×
726
                                                        fs.writeFileSync(envFile, envString, 'utf8');
×
727
                                                }
728
                                        }
729

730
                                        let d_filename = __dirname + "/../../platforms/" + y_platform + "/descriptor.json";
×
731

732
                                        if (!fs.existsSync(d_filename)) {
×
733
                                                console.log("[builder] no descriptor found in file " + d_filename);
×
734
                                                blog.state(build_id, owner, udid, "error");
×
735
                                                this.cleanupSecrets(XBUILD_PATH);
×
736
                                                callback(false, "builder not found for platform in: " + d_filename);
×
737
                                                return;
×
738
                                        }
739

740
                                        let platform_descriptor = require(d_filename);
×
741
                                        let commit_id = exec.execSync(`cd ${XBUILD_PATH}; git rev-list --all --max-count=1`).toString();
×
742
                                        let git_revision = exec.execSync(`cd ${XBUILD_PATH}; git rev-list --all --count`).toString();
×
743
                                        let git_tag = this.getTag(XBUILD_PATH);
×
744

745
                                        let REPO_VERSION = (git_tag + "." + git_revision).replace(/\n/g, "");
×
746
                                        let HEADER_FILE_NAME = platform_descriptor.header;
×
747

748
                                        console.log("[builder] REPO_VERSION (TAG+REV) [unused var]: '" + REPO_VERSION.replace(/\n/g, "") + "'");
×
749

750

751

752
                                        //
753
                                        // Fetch API Envs and create header file
754
                                        //
755

756
                                        this.apienv.list(owner, (env_list_success, api_envs) => {
×
757

758
                                                if (!env_list_success) {
×
759
                                                        console.log("[builder] [APIEnv] Listing failed:" + owner);
×
760
                                                        // must not be blocking
761
                                                }
762

763
                                                let thinx_json = this.generate_thinx_json(api_envs, device, api_key, commit_id, git_tag, XBUILD_PATH);
×
764

765
                                                console.log("[builder] Writing template to thinx_build.json...");
×
766

767
                                                try {
×
768
                                                        fs.writeFileSync(
×
769
                                                                XBUILD_PATH + "/thinx_build.json",
770
                                                                JSON.stringify(thinx_json)
771
                                                        );
772
                                                } catch (write_err) {
773
                                                        console.log("[builder] writing template failed with error" + write_err);
×
774
                                                        blog.state(build_id, owner, udid, "error");
×
775
                                                        this.notify(udid, build_id, notifiers, "error_configuring_build", false);
×
776
                                                        return;
×
777
                                                }
778

779
                                                let header_file = null;
×
780
                                                try {
×
781
                                                        console.log("Finding", HEADER_FILE_NAME, "in", XBUILD_PATH);
×
782
                                                        let h_file = finder.from(XBUILD_PATH).findFiles(HEADER_FILE_NAME);
×
783
                                                        if ((typeof (h_file) !== "undefined") && h_file !== null) {
×
784
                                                                header_file = h_file[0];
×
785
                                                        }
786
                                                        console.log("[builder] found header_file: " + header_file);
×
787
                                                } catch (e) {
788
                                                        console.log("Exception while getting header, use FINDER!: " + e);
×
789
                                                        blog.state(build_id, owner, udid, "error");
×
790
                                                }
791

792
                                                if (header_file === null) {
×
793
                                                        header_file = XBUILD_PATH / HEADER_FILE_NAME;
×
794
                                                        console.log("header_file empty, assigning path:", header_file);
×
795
                                                }
796

797
                                                console.log("[builder] Final header_file:", header_file);
×
798

799
                                                if ((platform != "mongoose") || (platform != "python") || (platform != "nodejs")) {
×
800
                                                        console.log("[builder] Generating C-headers from into", header_file);
×
801
                                                        if (fs.existsSync(header_file)) {
×
802
                                                                JSON2H.convert(thinx_json, header_file, {});
×
803
                                                        } else {
804
                                                                console.log("[user-err] Should be reported: No header_file to write at", header_file, "for platform", platform);
×
805
                                                        }
806
                                                }
807

808
                                                callback(true, {
×
809
                                                        response: "build_started",
810
                                                        build_id: build_id
811
                                                }); // last callback before executing
812

813
                                                //
814
                                                // start the build in background (device, br, udid, build_id, owner, ROOT, fcid, git, sanitized_branch, XBUILD_PATH, api_envs...)
815
                                                //
816

817
                                                let fcid = "000000";
×
818
                                                if (typeof (device.fcid) !== "undefined") {
×
819
                                                        fcid = device.fcid;
×
820
                                                }
821

822
                                                let dry_run = (br.dryrun === true) ? " --dry-run" : "";
×
823

824
                                                if (udid === null) {
×
825
                                                        console.log("[builder] Cannot build without udid!");
×
826
                                                        this.notify(udid, build_id, notifiers, "error_starting_build", false);
×
827
                                                        blog.state(build_id, owner, udid, "error");
×
828
                                                        return;
×
829
                                                }
830

831
                                                let CMD = "";
×
832

833
                                                // Local Build
834
                                                if (br.worker === false) {
×
835
                                                        CMD = "cd " + ROOT + ";" + ROOT + "/";
×
836
                                                }
837

838
                                                // Remote Build
839
                                                CMD += "./builder --owner=" + owner +
×
840
                                                        " --udid=" + udid +
841
                                                        " --fcid=" + fcid +
842
                                                        " --mac=" + this.formatMacForDevSec(device.mac) +
843
                                                        " --git=" + git +
844
                                                        " --branch=" + sanitized_branch +
845
                                                        " --id=" + build_id +
846
                                                        " --workdir=" + XBUILD_PATH +
847
                                                        dry_run;
848

849
                                                if (!env_list_success) {
×
850
                                                        console.log("[builder] Custom ENV Vars not loaded.");
×
851
                                                } else {
852
                                                        let stringVars = JSON.stringify(api_envs);
×
853
                                                        console.log("[builder] Build with Custom ENV Vars: " + stringVars);
×
854
                                                        CMD = CMD + " --env=" + stringVars;
×
855
                                                }
856
                                                console.log("[builder] Building with command: " + CMD);
×
857
                                                this.notify(udid, build_id, notifiers, "Building...", true);
×
858

859
                                                let end_timestamp = new Date().getTime() - start_timestamp;
×
860
                                                let seconds = Math.ceil(end_timestamp / 1000);
×
861
                                                console.log("Build Preparation stage took: ", seconds, "seconds");
×
862

863
                                                if (br.worker === false) {
×
864
                                                        console.log("[builder] Executing LOCAL build...");
×
865
                                                        this.runShell(XBUILD_PATH, CMD, owner, build_id, udid, notifiers);
×
866
                                                } else {
867
                                                        console.log("[builder] Requesting REMOTE build...");
×
868
                                                        let REMOTE_ROOT_DROP = "cd " + ROOT + ";" + ROOT;
×
869
                                                        CMD = CMD.replace(REMOTE_ROOT_DROP, ".");
×
870
                                                        this.runRemoteShell(br.worker, CMD, owner, build_id, udid, notifiers, source_id);
×
871
                                                }
872
                                        }); // apienv.list
873
                                }); // Platform.getPlatform(XBUILD_PATH)
874
                        }); // this.getLastAPIKey
875
                }); // devicelib.get
876
        }
877

878
        // public
879

880
        cleanupSecrets(cpath) {
881

882
                // critical files must be deleted after each build to prevent data leak;
883
                // must happen even on errors
884

885
                let env_files = finder.in(cpath).findFiles("environment.json");
×
886
                env_files.forEach(env_file => {
×
887
                        console.log("Cleaning up secrets:", env_file);
×
888
                        fs.unlink(env_file);
×
889
                });
890

891
                let h_files = finder.in(cpath).findFiles("environment.h");
×
892
                h_files.forEach(h_file => {
×
893
                        console.log("Cleaning up headers:", h_file);
×
894
                        fs.unlink(h_file);
×
895
                });
896

897
                let yml_files = finder.in(cpath).findFiles("thinx.yml");
×
898
                yml_files.forEach(yml_file => {
×
899
                        console.log("Cleaning up build-configurations:", yml_file);
×
900
                        fs.unlink(yml_file);
×
901
                });
902
        }
903

904
        build(owner, build, notifiers, callback, worker) {
905

906
                let build_id = uuidV1();
×
907
                let udid;
908

909
                if (typeof (callback) === "undefined") {
×
910
                        callback = () => {
×
911
                                // nop
912
                                console.log("This is replacement for undefined callback, that does nothing.");
×
913
                        };
914
                }
915

916
                let dryrun = false;
×
917
                if (typeof (build.dryrun) !== "undefined") {
×
918
                        dryrun = build.dryrun;
×
919
                }
920

921
                if (typeof (build.udid) !== "undefined") {
×
922
                        if (build.udid === null) {
×
923
                                callback(false, {
×
924
                                        success: false,
925
                                        response: "missing_device_udid",
926
                                        build_id: build_id
927
                                });
928
                                return;
×
929
                        }
930
                        udid = sanitka.udid(build.udid);
×
931
                } else {
932
                        console.log("NOT Assigning empty build.udid! " + build.udid);
×
933
                }
934

935
                if (typeof (build.source_id) === "undefined") {
×
936
                        callback(false, {
×
937
                                success: false,
938
                                response: "missing_source_id",
939
                                build_id: build_id
940
                        });
941
                        return;
×
942
                }
943

944
                if (typeof (owner) === "undefined") {
×
945
                        callback(false, {
×
946
                                success: false,
947
                                response: "missing_owner",
948
                                build_id: build_id
949
                        });
950
                        return;
×
951
                }
952

953
                devicelib.view("devices", "devices_by_owner", {
×
954
                        "key": owner.replace("\"", ""),
955
                        "include_docs": true
956
                }, (err, body) => {
957

958
                        if (err) {
×
959
                                if (err.toString() == "Error: missing") {
×
960
                                        callback(false, {
×
961
                                                success: false,
962
                                                response: "no_devices",
963
                                                build_id: build_id
964
                                        });
965
                                }
966
                                console.log("[builder] /api/build: Error: " + err.toString());
×
967

968
                                if (err.toString().indexOf("No DB shards could be opened") !== -1) {
×
969
                                        let that = this;
×
970
                                        console.log("Will retry in 5s...");
×
971
                                        setTimeout(() => {
×
972
                                                that.list(owner, callback);
×
973
                                        }, 5000);
974
                                }
975

976
                                return;
×
977
                        }
978

979
                        let rows = body.rows; // devices returned
×
980
                        let device;
981

982
                        for (let row in rows) {
×
983

984
                                let hasDocProperty = Object.prototype.hasOwnProperty.call(rows[row], "doc");
×
985
                                if (!hasDocProperty) continue;
×
986
                                device = rows[row].doc;
×
987

988
                                let hasUDIDProperty = Object.prototype.hasOwnProperty.call(device, "udid");
×
989
                                if (!hasUDIDProperty) continue;
×
990
                                let db_udid = device.udid;
×
991

992
                                let device_owner = "";
×
993
                                if (typeof (device.owner) !== "undefined") {
×
994
                                        device_owner = device.owner;
×
995
                                } else {
996
                                        device_owner = owner;
×
997
                                }
998

999
                                if (device_owner.indexOf(owner) !== -1) {
×
1000
                                        if (udid.indexOf(db_udid) != -1) {
×
1001
                                                udid = device.udid; // target device ID
×
1002
                                                break;
×
1003
                                        }
1004
                                }
1005
                        }
1006

1007
                        if ((typeof (device) === "undefined") || udid === null) {
×
1008
                                console.log(`☣️ [error] No device is currently using source ${build.source_id}.`);
×
1009
                                callback(false, {
×
1010
                                        success: false,
1011
                                        response: "device_not_found",
1012
                                        build_id: build_id
1013
                                });
1014
                                return;
×
1015
                        }
1016

1017
                        // 1. Converts build.git to git url by seeking in users' repos
1018
                        // 2. uses owner's transmit_key to decrypt customer's cssid and cpass
1019
                        userlib.get(owner, (user_get_err, doc) => {
×
1020

1021
                                if (user_get_err) {
×
1022
                                        console.log(`☣️ [error] [builder] ${user_get_err}.`);
×
1023
                                        callback(false, {
×
1024
                                                success: false,
1025
                                                response: "device_fetch_error",
1026
                                                build_id: build_id
1027
                                        });
1028
                                        return;
×
1029
                                }
1030

1031
                                if ((typeof (doc) === "undefined") || doc === null) {
×
1032
                                        callback(false, {
×
1033
                                                success: false,
1034
                                                response: "no_such_owner",
1035
                                                build_id: build_id
1036
                                        });
1037
                                        return;
×
1038
                                }
1039

1040
                                // 2. Transmit key should be used when encrypting device environment values...
1041

1042
                                let transmit_key = null;
×
1043

1044
                                // Allow using global custom transmit_key for on-premise installs
1045
                                if (typeof (app_config.transmit_key) !== "undefined") {
×
1046
                                        console.log("Loading global transmit key...");
×
1047
                                        transmit_key = app_config.transmit_key;
×
1048
                                }
1049

1050
                                // Prefer custom transmit_key
1051
                                if (typeof (owner.transmit_key) !== "undefined") {
×
1052
                                        console.log("Using owner transmit key.");
×
1053
                                        transmit_key = owner.transmit_key;
×
1054
                                }
1055

1056
                                // 1.
1057

1058
                                let git = null;
×
1059
                                let branch = "origin/master";
×
1060
                                let source = {};
×
1061

1062
                                // Finds first source with given source_id
1063
                                let all_sources = Object.keys(doc.repos);
×
1064
                                for (let index in all_sources) {
×
1065
                                        let sid = all_sources[index];
×
1066
                                        if (typeof (sid) === "undefined") {
×
1067
                                                console.log("[builder] source_id at index " + index + "is undefined, skipping...");
×
1068
                                                continue;
×
1069
                                        }
1070
                                        if (sid.indexOf(build.source_id) !== -1) {
×
1071
                                                source = doc.repos[all_sources[index]];
×
1072
                                                git = source.url;
×
1073
                                                branch = source.branch;
×
1074
                                                break;
×
1075
                                        }
1076
                                }
1077

1078
                                if (!this.containsNullOrUndefined([udid, build, owner, git])) {
×
1079
                                        callback(false, {
×
1080
                                                success: false,
1081
                                                response: "invalid_params",
1082
                                                build_id: build_id
1083
                                        });
1084
                                        return;
×
1085
                                }
1086

1087
                                // Saves latest build_id to device and if success, runs the build...
1088
                                devicelib.atomic("devices", "modify", device.udid, { build_id: build_id }, (mod_error) => {
×
1089
                                        if (mod_error) {
×
1090
                                                console.log(`☣️ [error] [builder] Atomic update failed: ${mod_error}.`);
×
1091
                                                return callback(false, {
×
1092
                                                        success: false,
1093
                                                        response: "DEVICE MOD FAILED",
1094
                                                        build_id: build_id
1095
                                                });
1096
                                        } else {
1097

1098
                                                let buildRequest = {
×
1099
                                                        build_id: build_id,
1100
                                                        source_id: build.source_id,
1101
                                                        owner: owner,
1102
                                                        git: git,
1103
                                                        branch: branch,
1104
                                                        udid: udid,
1105
                                                        dryrun: dryrun,
1106
                                                        worker: worker,
1107
                                                        is_private: source.is_private
1108
                                                };
1109

1110
                                                this.run_build(buildRequest, notifiers, callback, transmit_key);
×
1111
                                        }
1112
                                });
1113
                        });
1114
                });
1115
        }
1116

1117
        supportedLanguages() {
1118
                let languages_path = __dirname + "/../../languages";
×
1119
                return fs.readdirSync(languages_path).filter(
×
1120
                        file => fs.lstatSync(path.join(languages_path, file)).isDirectory()
×
1121
                );
1122
        }
1123

1124
        // duplicate functionality to plugins... should be merged
1125
        supportedExtensions() {
1126
                let languages_path = __dirname + "/../../languages";
×
1127
                let languages = this.supportedLanguages();
×
1128
                let extensions = [];
×
1129
                for (let lindex in languages) {
×
1130
                        let dpath = languages_path + "/" + languages[lindex] + "/descriptor.json";
×
1131
                        let descriptor = require(dpath);
×
1132
                        if (typeof (descriptor) !== "undefined") {
×
1133
                                let xts = descriptor.extensions;
×
1134
                                for (let eindex in xts) {
×
1135
                                        extensions.push(xts[eindex]);
×
1136
                                }
1137
                        } else {
1138
                                console.log("No Language descriptor found at " + dpath);
×
1139
                        }
1140
                }
1141
                return extensions;
×
1142
        }
1143
};
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