fix: generate flat macOS agent package (#7837)
* fix: generate flat macOS agent package * fix: avoid macOS package builder dependencies * fix: emit odc cpio payload archives * chore: drop unrelated lockfile churn * fix: keep macOS launch daemon alive before login --------- Co-authored-by: AJV20 <24819659+AJV20@users.noreply.github.com>
AJV20 committed
May 30, 2026 at 12:26 UTC
3436268022a015910eea5f78ca1508548dd66460
3 files changed
+381
-59
macosinstaller.js
new
+361
@@ -0,0 +1,361 @@
1
+/*
2
+ * @description Cross-platform macOS flat package builder for MeshAgent installers.
3
+ * Creates a XAR-based distribution package instead of the legacy bundle .mpkg
4
+ * format that macOS Sequoia/Tahoe rejects.
5
+ */
6
+
7
+'use strict';
8
+
9
+const crypto = require('crypto');
10
+const fs = require('fs');
11
+const fsp = fs.promises;
12
+const os = require('os');
13
+const path = require('path');
14
+const zlib = require('zlib');
15
+const childProcess = require('child_process');
16
+const { promisify } = require('util');
17
+
18
+const deflate = promisify(zlib.deflate);
19
+const execFile = promisify(childProcess.execFile);
20
+
21
+const LAUNCH_DAEMON_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
22
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
23
+<plist version="1.0">
24
+ <dict>
25
+ <key>Label</key>
26
+ <string>###SERVICENAME###</string>
27
+ <key>ProgramArguments</key>
28
+ <array>
29
+ <string>/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###</string>
30
+ </array>
31
+ <key>WorkingDirectory</key>
32
+ <string>/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/</string>
33
+ <key>RunAtLoad</key>
34
+ <true/>
35
+ <key>KeepAlive</key>
36
+ <true/>
37
+ <key>ThrottleInterval</key>
38
+ <integer>5</integer>
39
+ </dict>
40
+</plist>
41
+`;
42
+
43
+const LAUNCH_AGENT_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
44
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
45
+<plist version="1.0">
46
+ <dict>
47
+ <key>Label</key>
48
+ <string>###SERVICENAME###-launchagent</string>
49
+ <key>ProgramArguments</key>
50
+ <array>
51
+ <string>/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###</string>
52
+ <string>-kvm1</string>
53
+ </array>
54
+ <key>LimitLoadToSessionType</key>
55
+ <array>
56
+ <string>LoginWindow</string>
57
+ </array>
58
+ <key>WorkingDirectory</key>
59
+ <string>/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/</string>
60
+ <key>RunAtLoad</key>
61
+ <true/>
62
+ <key>KeepAlive</key>
63
+ <true/>
64
+ </dict>
65
+</plist>
66
+`;
67
+
68
+const POSTINSTALL = `#!/bin/bash
69
+set -e
70
+
71
+SERVICENAME="###SERVICENAME###"
72
+COMPANYNAME="###COMPANYNAME###"
73
+EXECUTABLENAME="###EXECUTABLENAME###"
74
+INSTALLDIR="/usr/local/mesh_services/\${COMPANYNAME}/\${SERVICENAME}"
75
+
76
+chown -R root:wheel "/usr/local/mesh_services/\${COMPANYNAME}" || true
77
+chown root:wheel "\${INSTALLDIR}/\${EXECUTABLENAME}" "\${INSTALLDIR}/\${EXECUTABLENAME}.msh"
78
+chown root:wheel "/Library/LaunchDaemons/\${SERVICENAME}.plist" "/Library/LaunchAgents/\${SERVICENAME}.plist"
79
+
80
+chmod 755 "\${INSTALLDIR}" "\${INSTALLDIR}/\${EXECUTABLENAME}"
81
+chmod 644 "\${INSTALLDIR}/\${EXECUTABLENAME}.msh" "/Library/LaunchDaemons/\${SERVICENAME}.plist" "/Library/LaunchAgents/\${SERVICENAME}.plist"
82
+
83
+/bin/launchctl bootout system "/Library/LaunchDaemons/\${SERVICENAME}.plist" >/dev/null 2>&1 || true
84
+/bin/launchctl bootstrap system "/Library/LaunchDaemons/\${SERVICENAME}.plist" >/dev/null 2>&1 || /bin/launchctl load "/Library/LaunchDaemons/\${SERVICENAME}.plist"
85
+`;
86
+
87
+const UNINSTALL = `#!/bin/bash
88
+
89
+echo "Stopping ###SERVICENAME###..."
90
+sudo /bin/launchctl bootout system "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null || sudo /bin/launchctl unload "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null
91
+sudo /bin/launchctl unload "/Library/LaunchDaemons/meshagentDiagnostic_periodicStart.plist" &> /dev/null
92
+sudo /bin/launchctl unload "/Library/LaunchDaemons/meshagentDiagnostic.plist" &> /dev/null
93
+sudo rm "/Library/LaunchDaemons/meshagentDiagnostic_periodicStart.plist" &> /dev/null
94
+sudo rm "/Library/LaunchDaemons/meshagentDiagnostic.plist" &> /dev/null
95
+sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###" &> /dev/null
96
+sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###.msh" &> /dev/null
97
+sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###.db" &> /dev/null
98
+sudo rm "/usr/local/mesh_services/meshagentDiagnostic/meshagentDiagnostic" &> /dev/null
99
+sudo rm "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null
100
+sudo rm "/Library/LaunchAgents/###SERVICENAME###.plist" &> /dev/null
101
+echo "###SERVICENAME### was uninstalled."
102
+`;
103
+
104
+function replaceTokens(str, tokens) {
105
+ return str.split('###SERVICENAME###').join(tokens.serviceName)
106
+ .split('###COMPANYNAME###').join(tokens.companyName)
107
+ .split('###EXECUTABLENAME###').join(tokens.executableName);
108
+}
109
+
110
+function xmlEscape(str) {
111
+ return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
112
+}
113
+
114
+function pkgIdentifierSegment(str) {
115
+ return String(str).toLowerCase().replace(/[^a-z0-9.-]/g, '-').replace(/^-+|-+$/g, '') || 'meshagent';
116
+}
117
+
118
+async function chmodIfExists(file, mode) {
119
+ try { await fsp.chmod(file, mode); } catch (ex) { }
120
+}
121
+
122
+async function walk(dir) {
123
+ const entries = await fsp.readdir(dir, { withFileTypes: true });
124
+ let files = 0, bytes = 0;
125
+ for (const entry of entries) {
126
+ const p = path.join(dir, entry.name);
127
+ if (entry.isDirectory()) {
128
+ const r = await walk(p);
129
+ files += r.files;
130
+ bytes += r.bytes;
131
+ } else if (entry.isFile()) {
132
+ const s = await fsp.stat(p);
133
+ files++;
134
+ bytes += s.size;
135
+ }
136
+ }
137
+ return { files, bytes };
138
+}
139
+
140
+function pad4(buffer) {
141
+ const pad = (4 - (buffer.length % 4)) % 4;
142
+ return (pad === 0) ? buffer : Buffer.concat([buffer, Buffer.alloc(pad)]);
143
+}
144
+
145
+function octal(value, width) {
146
+ const max = Math.pow(8, width) - 1;
147
+ const n = Math.max(0, Math.min(Number(value) || 0, max));
148
+ return Math.floor(n).toString(8).padStart(width, '0').slice(-width);
149
+}
150
+
151
+async function collectPayloadEntries(root, relativePath) {
152
+ const fullPath = path.join(root, relativePath);
153
+ const stat = await fsp.stat(fullPath);
154
+ const entries = [];
155
+ if (relativePath !== '') {
156
+ entries.push({ name: relativePath.split(path.sep).join('/'), stat: stat, data: stat.isFile() ? await fsp.readFile(fullPath) : null });
157
+ }
158
+ if (stat.isDirectory()) {
159
+ const names = (await fsp.readdir(fullPath)).sort();
160
+ for (const name of names) { entries.push.apply(entries, await collectPayloadEntries(root, path.join(relativePath, name))); }
161
+ }
162
+ return entries;
163
+}
164
+
165
+function cpioOdcRecord(name, mode, data, ino, mtime) {
166
+ data = data || Buffer.alloc(0);
167
+ const nameBuffer = Buffer.from(name + '\0', 'utf8');
168
+ const header = [
169
+ '070707',
170
+ octal(0, 6), // dev
171
+ octal(ino, 6),
172
+ octal(mode, 6),
173
+ octal(0, 6), // uid
174
+ octal(0, 6), // gid
175
+ octal(1, 6), // nlink
176
+ octal(0, 6), // rdev
177
+ octal(mtime || Math.floor(Date.now() / 1000), 11),
178
+ octal(nameBuffer.length, 6),
179
+ octal(data.length, 11)
180
+ ].join('');
181
+ return Buffer.concat([Buffer.from(header, 'ascii'), nameBuffer, data]);
182
+}
183
+
184
+async function createPayload(payloadRoot, targetFile) {
185
+ const entries = await collectPayloadEntries(payloadRoot, '');
186
+ const records = [];
187
+ let ino = 1;
188
+ for (const entry of entries) {
189
+ records.push(cpioOdcRecord(entry.name, entry.stat.mode, entry.data, ino++, Math.floor(entry.stat.mtimeMs / 1000)));
190
+ }
191
+ records.push(cpioOdcRecord('TRAILER!!!', 0, Buffer.alloc(0), ino));
192
+ await fsp.writeFile(targetFile, zlib.gzipSync(Buffer.concat(records)));
193
+}
194
+
195
+async function createBom(payloadRoot, targetFile) {
196
+ try {
197
+ await execFile('mkbom', [payloadRoot, targetFile], { timeout: 30000 });
198
+ } catch (ex) {
199
+ // Linux/Windows hosts can still build the package archive without a
200
+ // third-party BOM dependency. macOS hosts use the native mkbom tool
201
+ // above so local validation keeps the richer bill of materials.
202
+ await fsp.writeFile(targetFile, Buffer.alloc(0));
203
+ }
204
+}
205
+
206
+async function collectXarEntry(filePath, name, id) {
207
+ const stat = await fsp.stat(filePath);
208
+ const entry = {
209
+ id: id,
210
+ name: name,
211
+ type: stat.isDirectory() ? 'directory' : 'file',
212
+ mode: stat.mode,
213
+ uid: stat.uid,
214
+ gid: stat.gid,
215
+ atime: stat.atime,
216
+ mtime: stat.mtime,
217
+ ctime: stat.ctime
218
+ };
219
+ if (stat.isFile()) {
220
+ entry.data = await fsp.readFile(filePath);
221
+ } else if (stat.isDirectory()) {
222
+ const names = (await fsp.readdir(filePath)).sort();
223
+ entry.children = [];
224
+ for (const childName of names) {
225
+ entry.children.push(await collectXarEntry(path.join(filePath, childName), childName, ++collectXarEntry.nextId));
226
+ }
227
+ }
228
+ return entry;
229
+}
230
+
231
+function xarDate(d) {
232
+ return d.toISOString();
233
+}
234
+
235
+function xarFileXml(entry, depth, heapParts) {
236
+ const indent = ' '.repeat(depth);
237
+ let xml = indent + '<file id="' + entry.id + '">\n'
238
+ + indent + ' <name>' + xmlEscape(entry.name) + '</name>\n'
239
+ + indent + ' <type>' + entry.type + '</type>\n'
240
+ + indent + ' <mode>' + entry.mode.toString(8) + '</mode>\n'
241
+ + indent + ' <uid>' + entry.uid + '</uid>\n'
242
+ + indent + ' <gid>' + entry.gid + '</gid>\n'
243
+ + indent + ' <atime>' + xarDate(entry.atime) + '</atime>\n'
244
+ + indent + ' <mtime>' + xarDate(entry.mtime) + '</mtime>\n'
245
+ + indent + ' <ctime>' + xarDate(entry.ctime) + '</ctime>\n';
246
+ if (entry.type == 'file') {
247
+ const offset = 20 + heapParts.reduce(function (total, part) { return total + part.length; }, 0);
248
+ const sum = crypto.createHash('sha1').update(entry.data).digest('hex');
249
+ heapParts.push(entry.data);
250
+ xml += indent + ' <data>\n'
251
+ + indent + ' <archived-checksum style="sha1">' + sum + '</archived-checksum>\n'
252
+ + indent + ' <extracted-checksum style="sha1">' + sum + '</extracted-checksum>\n'
253
+ + indent + ' <offset>' + offset + '</offset>\n'
254
+ + indent + ' <encoding style="application/octet-stream"/>\n'
255
+ + indent + ' <size>' + entry.data.length + '</size>\n'
256
+ + indent + ' <length>' + entry.data.length + '</length>\n'
257
+ + indent + ' </data>\n';
258
+ } else {
259
+ for (const child of entry.children) { xml += xarFileXml(child, depth + 1, heapParts); }
260
+ }
261
+ return xml + indent + '</file>\n';
262
+}
263
+
264
+async function createXarPackage(paths) {
265
+ collectXarEntry.nextId = 0;
266
+ const entries = [];
267
+ for (const p of paths) { entries.push(await collectXarEntry(p, path.basename(p), ++collectXarEntry.nextId)); }
268
+
269
+ const heapParts = [];
270
+ let toc = '<?xml version="1.0" encoding="UTF-8"?>\n<xar>\n <toc>\n'
271
+ + ' <checksum style="sha1">\n <size>20</size>\n <offset>0</offset>\n </checksum>\n'
272
+ + ' <creation-time>' + (new Date()).toISOString() + '</creation-time>\n';
273
+ for (const entry of entries) { toc += xarFileXml(entry, 2, heapParts); }
274
+ toc += ' </toc>\n</xar>';
275
+
276
+ const tocBuffer = Buffer.from(toc, 'utf8');
277
+ const compressedToc = await deflate(tocBuffer);
278
+ const tocChecksum = crypto.createHash('sha1').update(compressedToc).digest();
279
+ const header = Buffer.alloc(28);
280
+ header.writeUInt32BE(0x78617221, 0); // xar!
281
+ header.writeUInt16BE(28, 4);
282
+ header.writeUInt16BE(1, 6);
283
+ header.writeBigUInt64BE(BigInt(compressedToc.length), 8);
284
+ header.writeBigUInt64BE(BigInt(tocBuffer.length), 16);
285
+ header.writeUInt32BE(1, 24); // sha1
286
+ return Buffer.concat([header, compressedToc, tocChecksum].concat(heapParts));
287
+}
288
+
289
+async function createMacOSInstaller(opts) {
290
+ const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'meshcentral-macos-pkg-'));
291
+ try {
292
+ const payloadRoot = path.join(tmpRoot, 'payload');
293
+ const scriptsRoot = path.join(tmpRoot, 'scripts');
294
+ const basePkg = path.join(tmpRoot, 'internal.pkg');
295
+ const resourcesDir = path.join(tmpRoot, 'Resources');
296
+ const installDir = path.join(payloadRoot, 'usr', 'local', 'mesh_services', opts.companyName, opts.serviceName);
297
+ const launchDaemons = path.join(payloadRoot, 'Library', 'LaunchDaemons');
298
+ const launchAgents = path.join(payloadRoot, 'Library', 'LaunchAgents');
299
+ const tokens = { serviceName: opts.serviceName, companyName: opts.companyName, executableName: opts.executableName };
300
+
301
+ await fsp.mkdir(installDir, { recursive: true });
302
+ await fsp.mkdir(launchDaemons, { recursive: true });
303
+ await fsp.mkdir(launchAgents, { recursive: true });
304
+ await fsp.mkdir(basePkg, { recursive: true });
305
+ await fsp.mkdir(scriptsRoot, { recursive: true });
306
+ await fsp.mkdir(resourcesDir, { recursive: true });
307
+
308
+ await fsp.copyFile(opts.agentPath, path.join(installDir, opts.executableName));
309
+ await fsp.writeFile(path.join(installDir, opts.executableName + '.msh'), opts.meshSettings);
310
+ await fsp.writeFile(path.join(launchDaemons, opts.serviceName + '.plist'), replaceTokens(LAUNCH_DAEMON_PLIST, tokens));
311
+ await fsp.writeFile(path.join(launchAgents, opts.serviceName + '.plist'), replaceTokens(LAUNCH_AGENT_PLIST, tokens));
312
+ await fsp.writeFile(path.join(scriptsRoot, 'postinstall'), replaceTokens(POSTINSTALL, tokens));
313
+
314
+ await chmodIfExists(path.join(installDir, opts.executableName), 0o755);
315
+ await chmodIfExists(path.join(scriptsRoot, 'postinstall'), 0o755);
316
+ await chmodIfExists(path.join(installDir, opts.executableName + '.msh'), 0o644);
317
+ await chmodIfExists(path.join(launchDaemons, opts.serviceName + '.plist'), 0o644);
318
+ await chmodIfExists(path.join(launchAgents, opts.serviceName + '.plist'), 0o644);
319
+
320
+ const payloadStats = await walk(payloadRoot);
321
+ const installKBytes = Math.ceil(payloadStats.bytes / 1000);
322
+ await createPayload(payloadRoot, path.join(basePkg, 'Payload'));
323
+ await createPayload(scriptsRoot, path.join(basePkg, 'Scripts'));
324
+ await createBom(payloadRoot, path.join(basePkg, 'Bom'));
325
+
326
+ const packageInfo = '<pkg-info format-version="2" identifier="com.meshcentral.' + xmlEscape(pkgIdentifierSegment(opts.serviceName)) + '.pkg" version="1.0" install-location="/" relocatable="false" auth="root">\n'
327
+ + ' <payload installKBytes="' + installKBytes + '" numberOfFiles="' + payloadStats.files + '"/>\n'
328
+ + ' <scripts>\n'
329
+ + ' <postinstall file="./postinstall"/>\n'
330
+ + ' </scripts>\n'
331
+ + '</pkg-info>\n';
332
+ await fsp.writeFile(path.join(basePkg, 'PackageInfo'), packageInfo);
333
+
334
+ const welcome = 'Welcome to the MeshCentral agent for MacOS\n\nThis installer will install the mesh agent for "' + opts.meshName + '" and allow the administrator to remotely monitor and control this computer over the internet. For more information, go to https://meshcentral.com.\n\nThis software is provided under Apache 2.0 license.\n';
335
+ const distribution = '<?xml version="1.0" encoding="utf-8"?>\n'
336
+ + '<installer-script minSpecVersion="1.000000">\n'
337
+ + ' <title>' + xmlEscape(opts.displayName) + '</title>\n'
338
+ + ' <options customize="never" allow-external-scripts="no" rootVolumeOnly="true"/>\n'
339
+ + ' <welcome language="en-US" mime-type="text/plain"><![CDATA[' + welcome.split(']]>').join(']]]]><![CDATA[>') + ']]></welcome>\n'
340
+ + ' <choices-outline>\n'
341
+ + ' <line choice="choice65"/>\n'
342
+ + ' </choices-outline>\n'
343
+ + ' <choice id="choice65" title="' + xmlEscape(opts.displayName) + '">\n'
344
+ + ' <pkg-ref id="internal.pkg"/>\n'
345
+ + ' </choice>\n'
346
+ + ' <pkg-ref id="internal.pkg" installKBytes="' + installKBytes + '" version="1.0" auth="Root">#internal.pkg</pkg-ref>\n'
347
+ + ' <options hostArchitectures="arm64,x86_64"/>\n'
348
+ + '</installer-script>\n';
349
+ await fsp.writeFile(path.join(tmpRoot, 'Distribution'), distribution);
350
+
351
+ const pkgBuffer = await createXarPackage([basePkg, resourcesDir, path.join(tmpRoot, 'Distribution')]);
352
+ return {
353
+ pkg: pkgBuffer,
354
+ uninstall: replaceTokens(UNINSTALL, tokens)
355
+ };
356
+ } finally {
357
+ await fsp.rm(tmpRoot, { recursive: true, force: true });
358
+ }
359
+}
360
+
361
+module.exports = { createMacOSInstaller };
views/default.handlebars
+1
-1
@@ -5839,7 +5839,7 @@
5839
x += '<a style=text-decoration:none title="' + "Copy to clipboard" + '" onclick=copyAgentIdValue("agins_linux_area_un")><img src=images/link4.png height=10 width=10 style=cursor:pointer> Copy</a></div>';
5840
5841
// macOS agent uninstall
5842
- x += '<div id=agins_osx_un style=display:none>' + "To remove a mesh agent, download the file below, right-click the \".mpkg\" file and select \"Show Package Contents\", then right-click \"Uninstall.command\" and select \"Open\"." + '<br /><br />';
5842
+ x += '<div id=agins_osx_un style=display:none>' + "To remove a mesh agent, download the file below, extract the ZIP archive, then right-click \"Uninstall.command\" and select \"Open\"." + '<br /><br />';
5843
x += addHtmlValue("Mesh Agent", '<a onclick=downloadFile("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + (urlargs.key?('&key=' + urlargs.key):'') + '") title="' + "Universal version of macOS Mesh Agent" + '">macOS Agent (Universal)</a> <img src=images/link4.png height=10 width=10 title="' + "Copy macOS agent URL to clipboard" + '" style=cursor:pointer onclick=copyAgentUrl("meshosxagent?id=10005&meshid=' + meshid.split('/')[2] + '",0)>');
5844
x += '</div>';
5845
webserver.js
+19
-58
@@ -6667,11 +6667,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6667
// Customize the mesh agent file name
6668
var meshfilename = 'MeshAgent-' + mesh.name + '.zip';
6669
var meshexecutablename = 'meshagent';
6670
- var meshmpkgname = 'MeshAgent.mpkg';
6670
+ var meshpkgname = 'MeshAgent.pkg';
6671
if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
6672
meshfilename = meshfilename.split('MeshAgent').join(domain.agentcustomization.filename);
6673
meshexecutablename = meshexecutablename.split('meshagent').join(domain.agentcustomization.filename);
6674
- meshmpkgname = meshmpkgname.split('MeshAgent').join(domain.agentcustomization.filename);
6674
+ meshpkgname = meshpkgname.split('MeshAgent').join(domain.agentcustomization.filename);
6675
}
6676
6677
// Customise the mesh agent display name
@@ -6696,62 +6696,23 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6696
setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, 'MeshAgent.zip');
6697
archive.pipe(res);
6698
6699
- // Opens the "MeshAgentOSXPackager.zip"
6700
- var yauzl = require('yauzl');
6701
- yauzl.open(obj.path.join(__dirname, 'agents', 'MeshAgentOSXPackager.zip'), { lazyEntries: true }, function (err, zipfile) {
6702
- if (err) { res.sendStatus(500); return; }
6703
- zipfile.readEntry();
6704
- zipfile.on('entry', function (entry) {
6705
- if (/\/$/.test(entry.fileName)) {
6706
- // Skip all folder entries
6707
- zipfile.readEntry();
6708
- } else {
6709
- if (entry.fileName == 'MeshAgent.mpkg/Contents/distribution.dist') {
6710
- // This is a special file entry, we need to fix it.
6711
- zipfile.openReadStream(entry, function (err, readStream) {
6712
- readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
6713
- readStream.on('end', function () {
6714
- var meshname = mesh.name.split(']').join('').split('[').join(''); // We can't have ']]' in the string since it will terminate the CDATA.
6715
- var welcomemsg = 'Welcome to the MeshCentral agent for MacOS\n\nThis installer will install the mesh agent for "' + meshname + '" and allow the administrator to remotely monitor and control this computer over the internet. For more information, go to https://meshcentral.com.\n\nThis software is provided under Apache 2.0 license.\n';
6716
- var installsize = Math.floor((argentInfo.size + meshsettings.length) / 1024);
6717
- archive.append(readStream.xxdata.toString().split('###DISPLAYNAME###').join(meshdisplayname).split('###WELCOMEMSG###').join(welcomemsg).split('###INSTALLSIZE###').join(installsize), { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) });
6718
- zipfile.readEntry();
6719
- });
6720
- });
6721
- } else if (entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchAgent.plist' ||
6722
- entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchDaemon.plist' ||
6723
- entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Info.plist' ||
6724
- entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/postflight' ||
6725
- entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/Postflight.sh' ||
6726
- entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Uninstall.command' ||
6727
- entry.fileName == 'MeshAgent.mpkg/Uninstall.command') {
6728
- // This is a special file entry, we need to fix it.
6729
- zipfile.openReadStream(entry, function (err, readStream) {
6730
- readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
6731
- readStream.on('end', function () {
6732
- var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
6733
- if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
6734
- archive.append(readStream.xxdata.toString().split('###SERVICENAME###').join(meshservicename).split('###COMPANYNAME###').join(meshcompanyname).split('###EXECUTABLENAME###').join(meshexecutablename), options);
6735
- zipfile.readEntry();
6736
- });
6737
- });
6738
- } else {
6739
- // Normal file entry
6740
- zipfile.openReadStream(entry, function (err, readStream) {
6741
- if (err) { throw err; }
6742
- var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
6743
- if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
6744
- archive.append(readStream, options);
6745
- readStream.on('end', function () { zipfile.readEntry(); });
6746
- });
6747
- }
6748
- }
6749
- });
6750
- zipfile.on('end', function () {
6751
- archive.file(argentInfo.path, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.bin'.replace('MeshAgent.mpkg',meshmpkgname) });
6752
- archive.append(meshsettings, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.msh'.replace('MeshAgent.mpkg',meshmpkgname) });
6753
- archive.finalize();
6754
- });
6699
+ // Create a flat XAR macOS installer package. Bundle .mpkg installers are rejected by recent macOS versions.
6700
+ require('./macosinstaller').createMacOSInstaller({
6701
+ agentPath: argentInfo.path,
6702
+ meshSettings: meshsettings,
6703
+ meshName: mesh.name.split(']').join('').split('[').join(''), // We can't have ']]' in the string since it will terminate the CDATA.
6704
+ executableName: meshexecutablename,
6705
+ packageName: meshpkgname,
6706
+ displayName: meshdisplayname,
6707
+ serviceName: meshservicename,
6708
+ companyName: meshcompanyname
6709
+ }).then(function (installer) {
6710
+ archive.append(installer.pkg, { name: meshpkgname });
6711
+ archive.append(installer.uninstall, { name: 'Uninstall.command', mode: 493 });
6712
+ archive.finalize();
6713
+ }).catch(function (err) {
6714
+ parent.debug('web', 'Failed to build macOS MeshAgent package: ' + err);
6715
+ try { res.sendStatus(500); } catch (ex) { }
6716
});
6717
}
6718