Skip to content

Instantly share code, notes, and snippets.

@Samasaur1
Last active January 25, 2024 00:41
Show Gist options
  • Save Samasaur1/57e749247a1ba81853a73a04fa6d2e94 to your computer and use it in GitHub Desktop.
Save Samasaur1/57e749247a1ba81853a73a04fa6d2e94 to your computer and use it in GitHub Desktop.
macOS login tracking
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.samasaur.login-data.system</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/login-data-system.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.samasaur.login-data.user</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/login-data-user.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

What is this?

This is a set of scripts, along with a LaunchAgent and a LaunchDaemon, that tracks when users login and logout of a macOS systtem, as well as when the system starts up or shuts down.

Why not use last(1)?

Short answer: last is probably fine in most cases.

Long answer: last is three versions out of date, and all the other options are worse.

man last gives no indication of this. However, if we read man getutxent (from the "See Also" section of man last), we can see the following section:

Backward compatibility

Successful calls to pututxline() will automatically write equivalent entries into the utmp, wtmp and lastlog files. Programs that read these old files should work as expected. However, directly writing to these files does not make corresponding entries in utmpx and the wtmpx and lastlogx equivalent files, so such write-access is deprecated.

And indeed, man lastlog (or man utmp or man wtmp) are all described as "login records (DEPRECATED)". And although the C interfaces remain, the man page also notes that the relevant files do not exist on macOS 10.5 or later. Their sources of truth are their replacements, utmpx, wtmpx, and lastlogx.

The man page for utmpx, however, includes this text:

Traditionally, separate files would be used to store the running log of the logins and logouts (wtmpx), and the last login of each user (lastlogx). With the availability of the Apple system log facility asl(3), these separate files can be replace with log entries, which are automatically generated when utmpx entries are written. The API to access the logins and logouts is described in endutxent_wtmp(3) while the last login info is accessible with getlastlogx(3).

For compatibility, changes to utmpx are reflected in utmp(3) (in the utmp, wtmp and lastlog files), but not the other way around.

However, if we check man asl, we see that the first line of the description is "This interface is obsoleted by os_log(3)."

So the actual source of truth for utmpx (and transitively utmp and last) is the Unified System Log. However, as Apple has converted more and more systems to use the system log, it has started to fill up extremely quickly. On some systems, the log will fill up within 24 hours.

So the options are:

  • Use last and hope that a) it does not roll over; b) that the chain of deprecated API to deprecated API that Apple has built continues to work. I don't actually know how often the lastlog rolls over, but it does seem to just happen sometimes. And to be fair, the APIs that Apple has set up will probably continue to work, as I imagine they incur little to no maintenance burden.
  • Read from the system log multiple times per day, to ensure that you don't miss anything. I actually tried to write code that would read logins from the system log, and it was a pain.
  • Roll your own solution, like this one.

How does it work?

This project is made up of a LaunchAgent (a user-scoped service) and a LaunchDaemon (a system-scoped service), which each run a script.

LaunchAgent

On macOS, LaunchAgents can be installed to two directories1: /Library/LaunchAgents, and ~/Library/LaunchAgents. An agent in ~/Library/LaunchAgents will run only for that user, while one in the system /Library/LaunchAgents will run for every user. Regardless of which directory the agent is in, it will run as the current user, starting up on a graphical login and receiving SIGTERM on logout.

The script attached to the LaunchAgent appends "$(whoami),login,$(date)" to a CSV file at /var/login-data/file.csv (that is assumed to exist) when the script starts, and then it simply waits forever. When it receives SIGTERM, it appends "$(whoami),logout,$(date)" to the same CSV file, and then exits cleanly.

LaunchDaemon

LaunchDaemons, on the other hand, must be put in /Library/LaunchDaemons2, start at boot, and stop at shutdown, running as root. They also receive SIGTERM when shutting down.

The script attached to the LaunchDaemon is a little more complicated. It starts by ensuring that the CSV file:

  • exists
  • is owned by root (chown root:wheel)
  • is world-writable (chmod 666)
  • is append-only (chflags sappend)

Then it puts an entry in the file indicating startup, and waits for SIGTERM, upon which it puts an entry in the file indicating shutdown.

How do I set it up?

  1. Put the scripts in /usr/local/bin and make them executable (sudo chmod +x /usr/local/bin/login-data-*.sh)
  2. Put com.samasaur.login-data.system.plist in /Library/LaunchDaemons
  3. Put com.samasaur.login-data.user.plist in /Library/LaunchAgents
  4. For each plist file
    1. Remove any quarantine flags (xattr -c /path/to/plist)
    2. Ensure the file is owned by user root and group wheel (chown root:wheel /path/to/plist)
    3. Ensure the file permissions are 644 (chmod 644 /path/to/plist)
  5. Reboot There is probably a way to activate these services without a reboot (launchctl enable?) but it is always a pain. Rebooting will start the system daemon and logging back in will start the user agent.

Footnotes

  1. There are also pre-installed LaunchAgents in /System/Library/LaunchAgents, but you cannot add or remove LaunchAgents from this directory, so it is irrelevant in this case.

  2. Again, LaunchDaemons can be in /System/Library/LaunchDaemons, but as these are part of the Signed System Volume, you cannot add or remove them from this directory.

#!/usr/bin/env bash
# https://gist.github.com/Samasaur1/57e749247a1ba81853a73a04fa6d2e94
mkdir -p /var/login-data
if [ -e "/var/login-data/file.csv" ]
then
chown root:wheel /var/login-data/file.csv
chmod 666 /var/login-data/file.csv
chflags sappend /var/login-data/file.csv
echo "root,poweron,$(date)" >> /var/login-data/file.csv
else
echo "user,action,date" > /var/login-data/file.csv
chown root:wheel /var/login-data/file.csv
chmod 666 /var/login-data/file.csv
chflags sappend /var/login-data/file.csv
echo "root,poweron,$(date)" >> /var/login-data/file.csv
fi
onLogout() {
echo "root,poweroff,$(date)" >> /var/login-data/file.csv
exit
}
trap onLogout SIGTERM
while true
do
sleep 10000 &
wait $!
done
#!/usr/bin/env bash
# https://gist.github.com/Samasaur1/57e749247a1ba81853a73a04fa6d2e94
echo "$(whoami),login,$(date)" >> /var/login-data/file.csv
onLogout() {
echo "$(whoami),logout,$(date)" >> /var/login-data/file.csv
exit
}
trap onLogout SIGTERM
while true
do
sleep 10000 &
wait $!
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment