Students' eternal resistance to simple instructions

Morningside
Contributor II

Sadly, due to the nature of the environment, and the users, our school laptops (Macbook Airs, all) rarely get properly logged out of. The default behavior is to close the lid and quick-rack it as soon as the class is over. Despite numerous education campaigns and trainings by the teachers, as well as myself, the kids simply do not log out of their accounts when they are done. Since the Macbooks are in a pool, and not 1-to-1, this is a problem.

I am hoping I can solve this problem with technology. Specifically, with Jamf + Scripting.

How can I detect a lid close and then trigger a forced-logout script when it is detected?

Worth noting: this is default behavior for Chromebooks. Surely is can be done for Macs as well...

18 REPLIES 18

larry_barrett
Valued Contributor

:) You can't detect a lid closure. You could use a GREP command to see if a Mac in clamshell mode has a closed lid or not, but that's really the only way I can think of doing it. Probably doesn't apply to this scenario.

Do you have the kids sign in using their own unique log in or are the shared devices auto-logged in to an account?

You could go to Security & Privacy and check Require Password Immediately after sleep or screen saver begins, but if you're using a shared log in it won't matter.

My cart devices are all on login triggers for policies. In Users and Groups->Login Options Choose Name and Password so whoever opens the Mac has to login as themselves. I actually log it in automatically as a generic account after reboot. Kids can choose to logout from the basic account and login as themselves or they can just let is sleep/close the lid and be forced to log in anyways.

BoscoATX
New Contributor III

Caffeinate command built into a launch daemon + an auto logout prompt for inactivity? You should be able to disable system sleep even with the lid closure, it will just sleep the screen.

sshort
Valued Contributor

Interesting challenge... you can detect the clamshell state, and I'm guessing in your environment you can safely assume clamshell shut = the user is putting it to sleep vs being connected to an external monitor.

I'm guessing this would need to be wrapped in a launchagent, b/c you can't rely on a Jamf policy running the command if the lid is shut and the Mac is asleep without a network connection.

detect clamshell status:

ioreg -r -k AppleClamshellState -d 4 | grep AppleClamshellState  | head -1

force logout a specified user. You could test this with a variable to force logout the currently signed in account.

launchctl bootout gui/$(id -u <username>)

ryan_ball
Valued Contributor

This is quick and dirty, but should work. You'd need to create a LaunchDaemon to run this script, it will continually run and log the user out if they are logged in when the lid is closed. This should work on 10.13 and above, as those OS migrated away from /var/log/syslog.log to use 'log stream'.

#!/bin/bash

log="/Library/Logs/Auto_Logout/Auto_Logout.log"

function writelog () {
    DATE=$(date +%Y-%m-%d %H:%M:%S)
    /bin/echo "${1}"
    /bin/echo "$DATE" " $1" >> "$log"
}

function auto_logout () {
    log stream --style syslog | grep --line-buffered 'clamshell closed: YES' | 
    while read -r; do
        loggedInUser=$(/usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "
");')
        if [[ -n "$loggedInUser" ]]; then
            writelog "Lid has been closed while $loggedInUser is still logged in; logging out."
            launchctl bootout gui/"$(id -u "$loggedInUser")"
            writelog "Done."
        else
            writelog "Lid has been closed without anybody logged in, taking no action."
        fi
     done
}

mkdir -p /Library/Logs/Auto_Logout

# Begin our never ending loop
while true; do
    auto_logout
done

Morningside
Contributor II

These solutions, especially @ryan.ball 's look quite promising. I'll give them a go in a few moments. Another thought is this: We do have a few dozen iMacs in 2 labs with the same issue. Kids will simply get up and walk away without logging out. And those that do attempt to log out, never click on the warning dialog when they are logging out of the guest account. Perhaps what I need, instead of detecting the close of a lid, is a 10 minute idleness timer. If a logged in account is idle for 10+ minutes then force-boot them, even if it is the guest account (which is very heavily used by the younger students).

Morningside
Contributor II

Uh oh! I launched that script as is with the following launchdaemon, and now finder will not respond :(

<?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.apple.forcelogout</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/forcelogout.sh</string>
    </array>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>

ryan_ball
Valued Contributor

Any output in /Library/Logs/Auto_Logout/Auto_Logout.log?

Morningside
Contributor II

Nope, but interestingly, the Auto_Logout dir was created, but there were no logs in it. I booted into safe mode and rm's the launch daemon out of /Library/Launchdaemons and rebooted, but still, finder will not respond. very curious. I didn't make any other changes as far as I am aware...

Morningside
Contributor II

No biggie. I am going to do a quick reinstall of the OS and try again. I made a couple changes to the script as well as to the launch daemon. In the launch daemon, instead of using KeepAlive/true, I am going to use a 60 second interval, and within the script itself, I am going to take that autolog command out of the loop so that it just runs once per 60 secs...

Update: even after a basic reinstall finder is hanging. I am going to erase the drive and then reinstall this morning.

Morningside
Contributor II

@ryan.ball

So, I think this project is making progress, but I have found a couple things that make me think it needs more refinement. With the above small mods to the script and the launch daemon, if I close the lid, wait a couple minutes, and then reopen the macbook, the login screen presents, and visually, there are no indicators that any of the users are currently logged in. However,

  1. If I issue a 'last' command, I see that several users are listed as still logged in, including the one that was logged in when the lid was closed, and

  2. If I look in the Users directory, the Guest folder is still there, which it should not be if Guest was properly logged out.

Worth noting: the Auto_Logout log shows an entry for each time the lid was closed, and claims to have logged out the user each time. That seems to be in conflict with the results of both 'last' and 'w'.

Any ideas here?

ryan_ball
Valued Contributor

By removing the continual loop of the log stream read, that might at least have some impact on the script due to the next time the script is run, the log stream might not have the line regarding the lid being closed in the log at that time.

It could probably be reworked using @sshort's provided command to check for the status of the lid closed every X seconds, and if the value is "Yes", then check for a logged in user, and if so log them out.

lidClosed=$(ioreg -r -k AppleClamshellState -d 4 | grep AppleClamshellState | head -1 | awk '{print $NF}')

Next, the launchctl bootout might not be the best method of logging a user out. You can maybe try this way:
(Tested on 10.13.6)

sudo -u $loggedInUser osascript -e 'tell application "loginwindow" to  «event aevtrlgo»'

ryan_ball
Valued Contributor

@Morningside This is reworked:

#!/bin/bash

log="/Library/Logs/Auto_Logout/Auto_Logout.log"

function writelog () {
    DATE=$(date +%Y-%m-%d %H:%M:%S)
    /bin/echo "${1}"
    /bin/echo "$DATE" " $1" >> "$log"
}

mkdir -p /Library/Logs/Auto_Logout

lidClosed=$(ioreg -r -k AppleClamshellState -d 4 | grep AppleClamshellState | head -1 | awk '{print $NF}')
if [[ "$lidClosed" == "Yes" ]]; then
    loggedInUser=$(/usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "
");')
    if [[ -n "$loggedInUser" ]]; then
        writelog "Lid has been closed while $loggedInUser is still logged in; logging out."
        sudo -u "$loggedInUser" osascript -e 'tell application "loginwindow" to  «event aevtrlgo»'
    else
        writelog "Lid has been closed without anybody logged in, taking no action."
    fi
else
    writelog "Lid is not closed; exiting."
fi

exit 0

On a much less cool note, you can create a config profile using the loginwindow payload to log users out after X minutes of inactivity. This might do what you want if the LaunchDaemon will not work for you.

Morningside
Contributor II

@ryan.ball Thanks for the updated script. This is a step closer, as it now appears to completely log out normal users (or at least the root user) but the guest user is not logging out completely. Even though the login screen shows that guest is not logged in when I reopen the lid, the Guest User folder still exists (it is normally deleted upon logout) and also both 'w' and 'last' commands show Guest as still being logged in.

Morningside
Contributor II

It occurs to me that the solution to the Guest Users folder not being deleted is to... delete it :/ . So I added a 'sudo rm -r /Users/Guest' to the script. For some reason it is not working but I am working on figuring out why...

#!/bin/bash

log="/Library/Logs/Auto_Logout/Auto_Logout.log"

function writelog () {
    DATE=$(date +%Y-%m-%d %H:%M:%S)
    /bin/echo "${1}"
    /bin/echo "$DATE" " $1" >> "$log"
}

mkdir -p /Library/Logs/Auto_Logout

lidClosed=$(ioreg -r -k AppleClamshellState -d 4 | grep AppleClamshellState | head -1 | awk '{print $NF}')
if [[ "$lidClosed" == "Yes" ]]; then
    loggedInUser=$(/usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "
");')
    if [[ -n "$loggedInUser" ]]; then
        writelog "Lid has been closed while $loggedInUser is still logged in; logging out."
        sudo -u "$loggedInUser" osascript -e 'tell application "loginwindow" to  «event aevtrlgo»'
        sudo rm -r /Users/Guest
    else
        writelog "Lid has been closed without anybody logged in, taking no action."
    fi
else
    writelog "Lid is not closed; exiting."
fi

exit 0

ryan_ball
Valued Contributor

@Morningside You would not need the sudo in front of your rm command, remove that and see if you get any different results.

Morningside
Contributor II

@ryan.ball Awesome. Removing the sudo did the trick. I do believe that my solution is ready for a roll out to a subset of devices. For posterity, here are the final components of the solution:

This is the plist that is placed in /Library/LaunchDaemons, it is named com.apple.forcelogout.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.apple.forcelogout</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/forcelogout.sh</string>
    </array>
    <key>StartInterval</key>
    <integer>60</integer>
</dict>
</plist>

This is the shell script that is paced in /bin, it is named forcelogout.sh:

#!/bin/bash

log="/Library/Logs/Auto_Logout/Auto_Logout.log"

function writelog () {
    DATE=$(date +%Y-%m-%d %H:%M:%S)
    /bin/echo "${1}"
    /bin/echo "$DATE" " $1" >> "$log"
}

mkdir -p /Library/Logs/Auto_Logout

lidClosed=$(ioreg -r -k AppleClamshellState -d 4 | grep AppleClamshellState | head -1 | awk '{print $NF}')
if [[ "$lidClosed" == "Yes" ]]; then
    loggedInUser=$(/usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "
");')
    if [[ -n "$loggedInUser" ]]; then
        writelog "Lid has been closed while $loggedInUser is still logged in; logging out."
        sudo -u "$loggedInUser" osascript -e 'tell application "loginwindow" to  «event aevtrlgo»'
        rm -r /Users/Guest
    else
        writelog "Lid has been closed without anybody logged in, taking no action."
    fi
else
    writelog "Lid is not closed; exiting."
fi

exit 0

nagiordano
New Contributor

Thank you for this awesome script. A couple of questions...how do I place the script in /bin? I don't seem to have access. The only bin folder I can write to is /usr/local/bin. Also, has anyone tested this in Big Sur?

I made some modifications and compromises to the script and have successfully deployed it to a mobile cart of 2015 MacBooks running 11.4. The way I deploy it is by Composer to create a package with forcelogout.sh placed in /usr/local/bin and forcelogout.plist created by a post install script.

https://community.jamf.com/t5/jamf-school/inactivity-idle-alternative-to-log-out/m-p/254459#M629