nix-darwin icon indicating copy to clipboard operation
nix-darwin copied to clipboard

recommendation: wrap launch agents and daemons in named files

Open tmillr opened this issue 1 year ago • 19 comments

This isn't a bug nor anything super important, but it would be nicer if agents and daemons were wrapped into appropriately named files/executables. With the current setup (i.e. just using sh in ProgramArguments), you end up with something like this in the System Settings > Login Items pane:

Screenshot 2024-02-16 at 9 00 48 PM

and furthermore, clicking on the info icon takes you to the sh executable (which isn't very useful: this tells me nothing about which executables/commands are actually invoked and it's hard to determine which is which).

To remedy this, it would probably be necessary to wrap most of the existing agents with perhaps the exception of executables which are already self-explanatory (e.g. gpg-connect-agent). It seems macOS just uses the name/path of the executable here (ignoring the name of the plist, the label defined within the plist, as well as any shebangs the executable file might have).

tmillr avatar Feb 17 '24 05:02 tmillr

Agreed. I especially tried to use writeShellScriptBin as the launchd.user.agents.<name>.command instead of just using launchd.user.agents.<name>.script hoping that it would use the script directly, but alas I only see sh.

bjeanes avatar Aug 11 '24 07:08 bjeanes

Bump

This is pretty critical imo, having a bunch of anon launch items is not cool and makes me want to immediately revert this installation and wipe out LaunchDaemons

~ ❯ ls /Library/LaunchAgents
org.gpgtools.Libmacgpg.xpc.plist  org.gpgtools.macgpg2.shutdown-gpg-agent.plist
org.gpgtools.macgpg2.fix.plist    org.gpgtools.updater.plist

~ ❯ ls /Library/LaunchDaemons
com.docker.socket.plist
com.docker.vmnetd.plist
com.nordvpn.macos.helper.plist
org.nixos.activate-system.plist
org.nixos.darwin-store.plist
org.nixos.nix-daemon.plist
org.nixos.nix-gc.plist
systems.determinate.nix-installer.nix-hook.plist

gjolund avatar Sep 09 '24 15:09 gjolund

I understand that the sh display can be solved by wrapping stuff in a bundle, but it seems tricky to make that work without adding a dependency on the Nix store. We would need logic to manually manage those files. This would also want solving upstream in the Nix installers too.

If you want to work on it we’d review PRs to handle this.

emilazy avatar Sep 09 '24 15:09 emilazy

I understand that the sh display can be solved by wrapping stuff in a bundle, but it seems tricky to make that work without adding a dependency on the Nix store. We would need logic to manually manage those files. This would also want solving upstream in the Nix installers too.

If you want to work on it we’d review PRs to handle this.

I can take a look, seems like the core issue is like you described

the .plist is running an arbitrary command instead of a binary

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>KeepAlive</key>
	<dict>
		<key>SuccessfulExit</key>
		<false/>
	</dict>
	<key>Label</key>
	<string>org.nixos.activate-system</string>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/sh</string>
		<string>-c</string>
		<string>exec /nix/store/sc025m1df2knrv1b0hagzmg42lcsjs0b-activate-system-start</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
</dict>
</plist>

gjolund avatar Sep 09 '24 15:09 gjolund

what I can't seem to figure out is why I have 6 entries in login items and only 5 nixos daemons

Screenshot 2024-09-09 at 10 30 48 PM

gjolund avatar Sep 09 '24 16:09 gjolund

the .plist is running an arbitrary command instead of a binary

Yes, we can’t really fix that. But we can wrap the shell script inside a bundle that will at least show up with a useful description. It’s just that it means managing files outside the Nix store, which is always a pain.

I’m guessing the sixth entry is one of the non‐Nix daemons, but I don’t know.

emilazy avatar Sep 09 '24 16:09 emilazy

the .plist is running an arbitrary command instead of a binary

Yes, we can’t really fix that. But we can wrap the shell script inside a bundle that will at least show up with a useful description. It’s just that it means managing files outside the Nix store, which is always a pain.

I’m guessing the sixth entry is one of the non‐Nix daemons, but I don’t know.

Sixth entry might come from https://github.com/dustinlyons/nixos-config which I based my config off of.

Something along these lines?

#!/bin/sh

ID="$1"

BASE_PATH="/nix/store"

# Create the full path by appending the ID to the base path
ACTIVATION_COMMAND="${BASE_PATH}/${ID}-activate-system-start"

exec "$ACTIVATION_COMMAND"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>KeepAlive</key>
	<dict>
		<key>SuccessfulExit</key>
		<false/>
	</dict>
	<key>Label</key>
	<string>org.nixos.activate-system</string>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/local/bin/nixos-store-activate-system-start</string>
		<string>sc025m1df2knrv1b0hagzmg42lcsjs0b</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
</dict>
</plist>

gjolund avatar Sep 09 '24 16:09 gjolund

I think we would prefer to make a bundle per daemon, so that we can give them human‐readable descriptions.

emilazy avatar Sep 09 '24 16:09 emilazy

I can test this out locally, but personally I wouldn't trust myself to push any of this upstream, still pretty new to nixos

from a maintainers perspective what is the biggest technical hurdle preventing this?

managing these bundles outside of nix?

gjolund avatar Sep 09 '24 16:09 gjolund

Someone has to do the work and someone else has to check that it’ll work correctly and not break things :)

There’s no inherent obstacle, as far as I know, we just have limited maintainer resources. Generally any time we have to manage a file that isn’t just a symlink into /nix/store it’s a bit of a pain. Probably what we’d want to do is make a /Library/Application Support/Nix or something to contain bundles containing the shell scripts, use rsync during activation to synchronize that with the system generation being activated, and make the LaunchDaemons and LaunchAgents point to there. But this hasn’t been a priority for me and I haven’t had time to look into how using bundles for this works, so it just hasn’t happened so far.

emilazy avatar Sep 09 '24 16:09 emilazy

Someone has to do the work and someone else has to check that it’ll work correctly and not break things :)

There’s no inherent obstacle, as far as I know, we just have limited maintainer resources. Generally any time we have to manage a file that isn’t just a symlink into /nix/store it’s a bit of a pain. Probably what we’d want to do is make a /Library/Application Support/Nix or something to contain bundles containing the shell scripts, use rsync during activation to synchronize that with the system generation being activated, and make the LaunchDaemons and LaunchAgents point to there. But this hasn’t been a priority for me and I haven’t had time to look into how using bundles for this works, so it just hasn’t happened so far.

Ok, I'll take a crack at the bundling, I've worked on that kind of thing before.

Once I get something local running well I'll update this issue and we can figure out how to get it upstream.

I'm not sure how to keep it in sync with nix, which is what you mentioned, but I should be able to get something working for my current generation.

gjolund avatar Sep 09 '24 16:09 gjolund

Here's a little workaround I used for one of my own/custom agents:

{
  launchd = {
    user.agents =
      {}
      // (let
        name = "clean-nvim-cache";
      in {
        ${name} = {
          serviceConfig = {
            Program = "${writeShellApplication {
              inherit name;
              runtimeInputs = [findutils neovim];
              text = ''
                cache_path="$(
                  command nvim -es -i NONE 2>&1 <<< '1verbose lua print(vim.loader.path)'
                )"

                command find \
                  "$cache_path" \
                  -mindepth 1 \
                  -atime +49 \
                  -delete
              '';
            }}/bin/${name}";
            RunAtLoad = false;
            LowPriorityIO = true;
            ProcessType = "Background";
            StartCalendarInterval = [
              {
                Day = 1;
                Hour = 1;
                Minute = 22;
              }
            ];
          };
        };
      });
  };
}

which results in:

Screenshot 2024-09-10 at 4 32 31 AM

But I think it'd be nice if:

  1. The builtin non-binary agents did something like this by default
  2. It did it for custom/user-added agents as well (or there was a utility function? idk)

It does say unidentified developer, but this is better than before. And now when I click the info icon it takes me to the shell script/wrapper (which I can directly open via Finder and view the commands being run), as opposed to getting the sh binary every time, which is another improvement.

What would be the point of using an app bundle? So that the script is signed and/or becomes identified developer?

tmillr avatar Sep 10 '24 11:09 tmillr

Here's a little workaround I used for one of my own/custom agents:

{
  launchd = {
    user.agents =
      {}
      // (let
        name = "clean-nvim-cache";
      in {
        ${name} = {
          serviceConfig = {
            Program = "${writeShellApplication {
              inherit name;
              runtimeInputs = [findutils neovim];
              text = ''
                cache_path="$(
                  command nvim -es -i NONE 2>&1 <<< '1verbose lua print(vim.loader.path)'
                )"

                command find \
                  "$cache_path" \
                  -mindepth 1 \
                  -atime +49 \
                  -delete
              '';
            }}/bin/${name}";
            RunAtLoad = false;
            LowPriorityIO = true;
            ProcessType = "Background";
            StartCalendarInterval = [
              {
                Day = 1;
                Hour = 1;
                Minute = 22;
              }
            ];
          };
        };
      });
  };
}

which results in:

Screenshot 2024-09-10 at 4 32 31 AM

But I think it'd be nice if:

  1. The builtin non-binary agents did something like this by default
  2. It did it for custom/user-added agents as well (or there was a utility function? idk)

It does say unidentified developer, but this is better than before. And now when I click the info icon it takes me to the shell script/wrapper (which I can directly open via Finder and view the commands being run), as opposed to getting the sh binary every time, which is another improvement.

What would be the point of using an app bundle? So that the script is signed and/or becomes identified developer?

this is great, thanks for sharing

yeah the primary reason to bundle is to sign it and distribute it through the installer

gjolund avatar Sep 11 '24 15:09 gjolund

I don’t know if signing is on the cards but associated bundles mean we could do things like icons etc. rather than just jamming everything into the name of a script file that shows up with a Terminal icon.

The writeShellApplication solution doesn’t work because it races the mount of the Nix store when encryption is enabled; it would prevent us from using wait4path etc.

emilazy avatar Sep 11 '24 15:09 emilazy

The writeShellApplication solution doesn’t work because it races the mount of the Nix store when encryption is enabled; it would prevent us from using wait4path etc.

Can you say more about this? I don't understand what race is at play here.

bjeanes avatar Sep 21 '24 11:09 bjeanes

When you have encryption enabled, your Nix Store won’t get mounted until you log in and as launchd doesn’t support unit dependencies, any launchd daemons/agents that run executables in the Nix Store will fail if they run before the Nix Store gets decrypted. The problem is even more annoying because even if you have the agent set to restart on failure if it fails due to the file/executable not existing, launchd will not restart it and try again.

One workaround for this is by using /bin/wait4path /nix/store as part of the launchd commands which has an open PR to automatically add this #1052

Enzime avatar Sep 21 '24 12:09 Enzime

OK that makes sense (also hello fellow Melbourner). However, the example above doing this has RunAtLoad = false and uses StartCalendarInterval to run on a timer, so that doesn't seem as relevant for this case (nor mine)

bjeanes avatar Sep 21 '24 12:09 bjeanes

The writeShellApplication solution doesn’t work because it races the mount of the Nix store when encryption is enabled; it would prevent us from using wait4path etc.

I would think that the only thing that needs to use wait4path is the services.activate-system daemon? Because doesn't that reload all nix-darwin managed agents/daemons when it runs? So everything should work out ok as long as that isn't disabled in your config?

tmillr avatar Sep 22 '24 02:09 tmillr

Currently activate-system only runs the etc, etcChecks and keyboard activation scripts:

https://github.com/LnL7/nix-darwin/blob/bd7d1e3912d40f799c5c0f7e5820ec950f1e0b3d/modules/services/activate-system/default.nix#L37-L39

Enzime avatar Sep 23 '24 06:09 Enzime

By the way, we do have a path forward for this: https://github.com/LnL7/nix-darwin/issues/1219#issuecomment-2573095530

Samasaur1 avatar Jan 29 '25 07:01 Samasaur1