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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
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