Skip to content

Conversation

@PeetMcK
Copy link

@PeetMcK PeetMcK commented Dec 12, 2025

Summary

This PR enhances the macOS agent update process and adds a new installer UI option for deploying macOS agents.

This PR is required for Ylianst/MeshAgent#313 to perform agent updates/self-updates successfully on macOS. The MeshAgent PR introduces improved update mechanisms, and this PR provides the necessary meshcore.js infrastructure to support those updates reliably, plus UI improvements for initial agent deployment.

Key Improvements

MeshCore Improvements

1. Startup Cleanup

  • Automatically removes stale meshagent.upgrade launchctl jobs on agent startup
  • Prevents accumulation of failed upgrade processes from previous update attempts

2. darwin_spawn() Function

  • Native posix_spawn wrapper via _GenericMarshal for detached process execution
  • Creates isolated process groups (POSIX_SPAWN_SETPGROUP)
  • Required for reliable launchctl operations that must survive parent process termination

3. Enhanced Service Name Handling

  • Support for both serviceId and serviceName lookups
  • Better compatibility with custom launchd configurations
  • Improved error messages for debugging service lookup failures

4. Update Progress Tracking

  • Stores update state in database before update starts
  • Verifies and reports successful completion after restart
  • Sends agentupdatecomplete action to server on successful update
  • Helps diagnose failed updates

5. macOS-Specific Update Process

  • Uses launchctl submit to run -upgrade as an isolated launchd job
  • Three-layer cleanup mechanism (startup, pre-update, post-upgrade)
  • Fallback to service restart if launchctl operations fail
  • User-friendly status messages

6. App Bundle Support

  • Add app bundle serving capability for macOS agent updates
  • Implement app bundle capability checks
  • Add ZIP hash verification for app bundles

UI Improvements

1. macOS Binary Installer Option

  • New installation method (option 9) specifically for macOS agents
  • Provides one-line install command for quick deployment
  • Automatically handles quarantine attribute removal via xattr

2. Dynamic Architecture Selection

  • Support for Universal (10005), x86-64 (16), and ARM-64 (29) binaries
  • Install command updates automatically based on architecture selection
  • Proper URL encoding for special characters in mesh IDs and authentication keys

3. ZIP vs Deprecated Installation Methods

  • ZIP mode (installflags 10-12) preserves code signatures by keeping binary and .msh separate
  • Deprecated mode (installflags 0-2) embeds settings into binary, breaking signatures
  • Clear deprecation warnings guide users to recommended ZIP installation
  • Link text updates dynamically ("meshagent.zip" vs "meshagent") based on mode

4. Installation Type Options

  • Background & interactive (ZIP) - Recommended
  • Background only (ZIP)
  • Interactive only (ZIP)
  • Legacy deprecated options for backward compatibility

5. One-Line Install Command

curl -o meshagent.zip "[url]" && unzip meshagent.zip && (xattr -d com.apple.quarantine meshagent 2>/dev/null || true) && chmod +x meshagent && sudo ./meshagent -install
  • Downloads and extracts agent
  • Removes macOS quarantine attribute
  • Makes executable and installs
  • Gracefully handles missing quarantine attribute

Technical Details

The Three-Layer Protection System

  1. Layer 1 - Startup: Clean up any leftover jobs when meshcore loads
  2. Layer 2 - Pre-Update: Remove existing jobs before spawning new upgrade
  3. Layer 3 - Post-Upgrade: The agent binary itself removes the job on exit

This ensures no accumulation of stuck upgrade processes regardless of where failures occur.

Process Isolation

The darwin_spawn() function properly detaches child processes using posix_spawn with the POSIX_SPAWN_SETPGROUP flag, creating a new process group. This is critical for launchctl operations that must continue running after meshcore restarts during the update.

Code Signature Preservation

The ZIP installation method keeps the agent binary clean and stores mesh settings in a separate .msh file. This preserves the code signature on macOS, which is important for security and compatibility with macOS security features like Gatekeeper.

Testing

MeshCore: Tested on macOS with launchd-managed agents. The update process:

  • Successfully runs -upgrade as an isolated job
  • Cleans up stale jobs on startup
  • Reports completion status to server
  • Falls back gracefully if launchctl fails

UI: Tested macOS binary installer with:

  • All three architectures (Universal, x86-64, ARM-64)
  • Both ZIP and deprecated installation modes
  • Special characters in mesh IDs and authentication keys
  • Quarantine attribute removal on downloaded binaries

Changes

  • Files modified: agents/meshcore.js, views/default.handlebars
  • Lines changed: +742, -114
  • Impact: macOS agent deployment and updates
  • Breaking changes: None

Related PRs

Notes

  • All changes are additive and macOS-specific
  • No impact on other platforms (Windows, Linux, etc.)
  • Maintains backward compatibility with existing agents
  • User-facing messages are clear and non-technical
  • Deprecated installation methods remain available for compatibility but show warnings

@PeetMcK PeetMcK marked this pull request as ready for review December 12, 2025 04:47
@PeetMcK PeetMcK force-pushed the _100-meshcore.js_macos-upgrade branch from f204bab to 710c2fd Compare December 13, 2025 10:25
@silversword411
Copy link
Contributor

Thanks for all the work you've done on this!

It's a huge change, I haven't had time to try and delve into this yet but it's on my TODO list!

@PeetMcK PeetMcK force-pushed the _100-meshcore.js_macos-upgrade branch 2 times, most recently from d4e2077 to 41c02a2 Compare December 14, 2025 08:56
Implements app bundle ZIP delivery for macOS agents, preserving code
signatures by using external .msh configuration files instead of
embedding settings into binaries.

Features:
- Dynamic app bundle packaging with separate .msh files
- Installation type options: app ZIP, binary ZIP, deprecated mpkg
- macOS uninstall command with bare binary download
- Self-update support with hash verification
- MeshAgentAPP container structure for app bundles
- Agent update reliability improvements

Changes:
- webserver.js: Add createMacOSAppBundleZipPackage() for dynamic packaging
- webserver.js: Detect self-updates vs initial installs for correct file delivery
- meshcentral.js: Hash app bundle ZIPs and store paths on startup
- meshagent.js: Use appBundleHashHex for agents with App Bundle capability
- views/default.handlebars: Add app bundle UI with installation type dropdown
- agents/meshcore.js: App bundle detection and .msh file support

Fixes agent installation and self-update hash verification issues.
Deprecates mpkg installer in favor of ZIP-based delivery.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@PeetMcK PeetMcK force-pushed the _100-meshcore.js_macos-upgrade branch from 41c02a2 to 1b77dac Compare December 14, 2025 08:58
PeetMcK and others added 2 commits December 14, 2025 02:50
This commit makes two related improvements to macOS installer handling:

1. Remove embedding from bare binary mode (installflags 0-2):
   - Serve bare binary without embedding for macOS (preserves code signature)
   - Non-macOS platforms still use embedding as before
   - Remove deprecated warning from UI

2. Add .msh-only download options (installflags 30-32):
   - New options: Background & interactive (msh), etc.
   - Downloads just meshagent.msh configuration file
   - Shows simple curl command for .msh download
   - Add &mshonly=1 server-side handler

UI Changes (default.handlebars):
- Rename installflags 0,1,2 to include "(binary)" suffix
- Add new .msh-only options (30, 31, 32)
- Detect .msh-only mode (installflags >= 30)
- Add bare binary command handler (installflags < 10)
- Remove deprecated warning message

Server Changes (webserver.js):
- Add handler for &mshonly=1 parameter
- Serve .msh file directly with text/plain content-type
- Add macOS platform check before embedding
- Serve bare binary for macOS without embedding

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
The bare binary mode (installflags 0-2) now serves binaries without
embedded .msh data to preserve code signatures. Update the install
command to download the .msh file separately using the new mshonly
endpoint.

Changes:
- Download binary: curl -o meshagent "URL&installflags=X"
- Download .msh: curl -o meshagent.msh "URL&installflags=X&mshonly=1"
- Add --copy-msh=1 flag to installation command
- Ensures .msh installflags match the selected installation type

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@PeetMcK
Copy link
Author

PeetMcK commented Dec 14, 2025

Thanks for all the work you've done on this!

It's a huge change, I haven't had time to try and delve into this yet but it's on my TODO list!

Not a worry. I get that this is a lot but there was a lot to clean up after 3+ years of skipping out on macos. And after talking to Simon, I realized there was a lot to polish still needed, so this MeshCentral pull cleans up the server-side functions

  • removes .msh embeding in the macOS binary
  • builds dynamic .zip archives of the .app bundle + .msh and binary + .msh
  • direct binary links
  • direct .msh file links
  • script snippits for download/install
  • uninstall script snippit
  • removed mpkg
  • ensured binary and bundle macos agents selfupdate with -upgrade instead of an agent kickstart

webserver.js Outdated
if (req.query.sitestyle == 3) { uiViewMode = 'default3'; }
} else if (webstateJSON && webstateJSON.uiViewMode == 3) {
uiViewMode = 'default3';
else if (req.query.sitestyle == 78) { uiViewMode = 'default78'; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

erm this will break your meshcentral web ui?
the default78 is the file name and the is no default78.handlebars file!

Copy link
Author

@PeetMcK PeetMcK Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right 78's only on my fork. Should be fixed on ... 2cf8757

These references were accidentally included and would cause errors
for users without the default78.handlebars template file.

This PR is focused on macOS agent updates, not UI customization.
@DaanSelen
Copy link
Contributor

Thanks for all the work you've done on this!
It's a huge change, I haven't had time to try and delve into this yet but it's on my TODO list!

Not a worry. I get that this is a lot but there was a lot to clean up after 3+ years of skipping out on macos. And after talking to Simon, I realized there was a lot to polish still needed, so this MeshCentral pull cleans up the server-side functions

* removes .msh embeding in the macOS binary

* builds dynamic .zip archives of the .app bundle + .msh and binary + .msh

* direct binary links

* direct .msh file links

* script snippits for download/install

* uninstall script snippit

* removed mpkg

* ensured binary and bundle macos agents selfupdate with -upgrade instead of an agent kickstart

Sounds very good!

I got 2 things, can I still run it with a plain .msh file and a binary next to eachother?

And are you willing to support the MacOS Agent longer-term? Like I am doing with Docker :)

@PeetMcK
Copy link
Author

PeetMcK commented Dec 17, 2025

@DaanSelen There's a myriad of ways the macOS MeshAgent handles installs and ./meshagent -fullinstall --copy-msh=1 does what you're asking. I did update the logic for that specific call to not magically decide it was an inplace install, so without a --installPath=/path ... ./meshagent -fullinstall --copy-msh=1 will install to the default location.

The logic for --copy-msh=1 had been updated, so the order goes exatbinaryname.msh, mshagent.msh, anyfilename.msh ... the anyfilename.msh will only be used if it is the ONLY .msh file in the directory.

There is also a --mshPath=/path/to/msh that works regardless of naming.

"And are you willing to support the MacOS Agent longer-term? Like I am doing with Docker :)"

Thats a question. As long as it's in my stack I'll look at it. But I'm not in a place where I can devote anything like the time that I have. It directly affects my ability to support my family.

@DaanSelen
Copy link
Contributor

The logic for --copy-msh=1 had been updated, so the order goes exatbinaryname.msh, mshagent.msh, anyfilename.msh ... the anyfilename.msh will only be used if it is the ONLY .msh file in the directory.

Sounds good what is then embedded inside the binary?
I understand your answer regarding the maintainability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants