Managing Mac Kiosks

Managing Mac Kiosks

Overview


We have implemented multiple forms of kiosk throughout the years at the Marriott Library or other locations on campus that we support. Initially, we implemented web kiosks that provided anonymous users at the Marriott Library quick access to limited library & campus resources like the catalog system, databases, etc. These web kiosks needed to be secure, resilient and easy to use for the public but at the same time prevent anonymous access to resources we do not allow. This expanded to “exhibit” kiosks that would include media content like audio, video and other types of content focused around a specific library exhibits.

And finally, we have implemented “Digital Displays” which use technologies such as LCD, LED and projection to display digital images, video, web pages, or text. They can be found in public spaces of the Marriott Library to provide wayfinding, exhibitions and marketing & communication information including text, animated or video messages for advertising, information, entertainment and merchandising to campus students, staff and faculty.

This post will cover a history of the methodologies & technologies we have implemented and modifications we have developed to address IT development & resource impact and ease access to outside groups like web development & content providers.

History


Starting around 2004, we implemented public web kiosks using Mac OS X. On a standard Mac OS X system, users have greater overall access to the operating system features. They can run the Finder, use the Dock, launch most applications, and save files in their home folder. On our public web kiosks, however, are significantly more “restrictive”, limiting users’ access to just a few applications. The available applications are confined to a small subset like a web browser, utilities, and helper applications. The kiosks themselves have been set up to be extremely easy to use, capable of operating relatively unattended, require minimal maintenance, and are easy to update. Their key operating feature is the ability to restrict users from making changes to the system or hard disk.

   

This is a list of the key modifications we made to set up our kiosks originally:

  • Guest User – a new user that does NOT have administrative rights to the machine and that has a REAL password
  • Limit Write Access – limit what a guest user can change on the hard disk by changing file permissions and ownership
  • Enable Firmware Security – prevents users from making changes to the OS by bypassing default startup disk.
  • Customized Apple Menu – using third party applications such as Fruit Menu, the Apple Menu can be customized to remove shutdown/logout options and prevent users from shutting down or logging out of the Mac
  • Administrative Utility – to allow administrative users to perform some admin procedures like restarting, shutting down, logging out, launching the Finder, but restrict these processes from the general kiosk user, we created an application which requires a password to access to these functions
  • Replace the Finder – Many kiosks only allow users to use one application, such as a web browser. To do this, you will need to replace the Finder with the desired application by either changing the loginwindow preferences or swapping the Finder with another application
  • Disable the Dock – disabling the Dock.app by renaming it, or changing the permissions prevents users from launching the it. It also creates more usable screen space on your kiosk machines
  • Maintenance – to clean up these changes, use a script that runs at login, logout, a defined schedule, or when the machine is idle.

The public kiosks refreshed based on maintenance schedule and activation of screensaver and would restore the kiosk users home folder to a default including browser settings, bookmarks, default home page, etc. We restricted web content using open source software called “Privoxy“. Privoxy is a non-caching web proxy with advanced filtering capabilities for enhancing privacy, modifying web page data and HTTP headers, controlling access, and removing ads and other obnoxious Internet junk. Privoxy has a flexible configuration and can be customized to suit individual needs and tastes.

Whitelist
We use Privoxy to give access to specific sites using a defined whitelist and block all other websites.

Image result for whitelist

ACLs: permit-access and deny-access

Specifies:
Who can access what.

Type of value:
src_addr[:port][/src_masklen] [dst_addr[:port][/dst_masklen]]

Where src_addr and dst_addr are IPv4 addresses in dotted decimal
notation or valid DNS names, port is a port number, and src_masklen and
dst_masklen are subnet masks in CIDR notation, i.e. integer values from
2 to 30 representing the length (in bits) of the network address. The
masks and the whole destination part are optional.

If your system implements RFC 3493, then src_addr and dst_addr can be
IPv6 addresses delimeted by brackets, port can be a number or a service
name, and src_masklen and dst_masklen can be a number from 0 to 128.
Examples:

     Explicitly define the default behavior if no ACL and
     listen-address are set: "localhost" is OK. The absence of a
     dst_addr implies that all destination addresses are OK:

       permit-access  localhost

     Allow any host on the same class C subnet as www.privoxy.org
     access to nothing but www.example.com (or other domains hosted
     on the same system):

       permit-access  www.privoxy.org/24 www.example.com/32

     Allow access from any host on the 26-bit subnet 192.168.45.64
     to anywhere, with the exception that 192.168.45.73 may not
     access the IP address behind www.dirty-stuff.example.com:

       permit-access  192.168.45.64/26
       deny-access    192.168.45.73    www.dirty-stuff.example.com

     Allow access from the IPv4 network 192.0.2.0/24 even if
     listening on an IPv6 wild card address (not supported on all
     platforms):

       permit-access  192.0.2.0/24

     This is equivalent to the following line even if listening on
     an IPv4 address (not supported on all platforms):

       permit-access  [::ffff:192.0.2.0]/120

Then we provide the user with feedback regarding blocked web sites. If they user needs to access restricted web sites, we direct users to a non-kiosk system, one requiring an authenticated login to access those sites.

Proxy Configuration & Bypass
We set the kiosks to use they proxy by programmatically modifying the System Preferences -> Network -> Proxies Settings to set and specify the proxy settings and can exclude sites from the proxy that don’t work properly with a proxy or specifically with Privoxy.

We have an old perl script that we use to manage the proxy configuration which uses the networksetup command line utility with these commands:

  • networksetup -setwebproxy networkservice domain portnumber authenticated username password
  • networksetup -setsecurewebproxy networkservice domain portnumber authenticated username password
  • networksetup -setproxybypassdomains networkservice domain1 [domain2] […]

For example here is the proxy perl script:

#!/usr/bin/perl

#use strict;

################################################################################
# Copyright (c) 2011 University of Utah Student Computing Labs.
# All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appears in all copies and
# that both that copyright notice and this permission notice appear
# in supporting documentation, and that the name of The University
# of Utah not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission. This software is supplied as is without expressed or
# implied warranties of any kind.
################################################################################

use lib '/usr/local/lib';
use Xhooks::Common;
require '/Library/Xhooks/Preferences/includes/xhooks_vars.perl';

our $debug ||= 0;
$debug = 1 if $ARGV[0];

my $networksetup = "/usr/sbin/networksetup";	# Only on 10.5

our (
	$PROXY_PASSIVE_FTP, 
	$HTTP_PROXY_SERVER, 
	$HTTPS_PROXY_SERVER,
	$HTTP_PROXY_PORT,
	$HTTPS_PROXY_PORT,
	@PROXY_HOST_EXCEPTIONS, 
	@PROXY_INTERFACES,
);

################################################################################
# Set Proxy Settings

if ( -e $networksetup ) {

	print "Setting proxy settings (10.5+).\n" if $debug;
	
	if ( ! nonempty_array ( @PROXY_INTERFACES ) ) {
		@PROXY_INTERFACES = ( "en0" );
	}
	
	foreach my $interface ( @PROXY_INTERFACES ) {
	
		my @bla = `$networksetup listnetworkserviceorder | grep -b1 $interface`;
		$bla[0] =~ s/^\d+-\(\d+\) //;
		chomp $bla[0];
		my $service_name = $bla[0];
		print "Service Name: $service_name\n" if $debug;

		print "$networksetup -setpassiveftp $service_name $PROXY_PASSIVE_FTP\n" if nonempty_var ( $PROXY_PASSIVE_FTP ) and $debug;
		system $networksetup, "-setpassiveftp", $service_name, $PROXY_PASSIVE_FTP if nonempty_var ( $PROXY_PASSIVE_FTP );

		if ( nonempty_var ( $HTTP_PROXY_SERVER ) and nonempty_var ( $HTTP_PROXY_PORT ) ) {
			print "$networksetup setwebproxy $service_name $HTTP_PROXY_SERVER, $HTTP_PROXY_PORT\n" if $debug;
			system $networksetup, "setwebproxy", $service_name, $HTTP_PROXY_SERVER, $HTTP_PROXY_PORT;
			print "$networksetup setwebproxystate $service_name on\n" if $debug;
			system $networksetup, "setwebproxystate", $service_name, "on";
		} else {
			print "$networksetup setwebproxy $service_name\n" if $debug;
			system $networksetup, "setwebproxy", $service_name, "", "";
			print "$networksetup setwebproxystate $service_name off\n" if $debug;
			system $networksetup, "setwebproxystate", $service_name, "off";
		}

		if ( nonempty_var ( $HTTPS_PROXY_SERVER ) and nonempty_var ( $HTTPS_PROXY_PORT ) ) {
			print "$networksetup setsecurewebproxy $service_name $HTTPS_PROXY_SERVER, $HTTPS_PROXY_PORT\n" if $debug;
			system $networksetup, "setsecurewebproxy", $service_name, $HTTPS_PROXY_SERVER, $HTTPS_PROXY_PORT;
			print "$networksetup setsecurewebproxystate $service_name on\n" if $debug;
			system $networksetup, "setsecurewebproxystate", $service_name, "on";
		} else {
			print "$networksetup setsecurewebproxy $service_name\n" if $debug;
			system $networksetup, "setsecurewebproxy", $service_name, "", "";
			print "$networksetup setsecurewebproxystate $service_name off\n" if $debug;
			system $networksetup, "setsecurewebproxystate", $service_name, "off";
		}

		if ( nonempty_array ( @PROXY_HOST_EXCEPTIONS ) ) {
			print "$networksetup setproxybypassdomains $service_name @PROXY_HOST_EXCEPTIONS\n" if $debug;
			system $networksetup, "setproxybypassdomains", $service_name, @PROXY_HOST_EXCEPTIONS;
		} else {
			print "$networksetup setproxybypassdomains $service_name Empty\n" if $debug;
			system $networksetup, "setproxybypassdomains", $service_name, "Empty";
		}
	}

} else {
	print STDERR "ERROR $0: $networksetup is missing.\n";
}

While this method was effective for many years, other backend code and configuration maintenance made supporting new operating system versions and Safari browser updates more and more difficult. And with increasing demands, projects & priorities on our IT group it was decided to move to a commercial application called, xStand.

xStand allowed us to restrict access to some web sites, the operating system, system settings, the downloading of files and applications with little additional development and free up our time from continually updating our previously internally  developed public web kiosk scripts & configuration. It had a few minor issues and annoyances, but overall worked well for our needs.

Unfortunately, after many years of solid service, the company supporting xStand closed its doors and sold the software to another developer and then their license servers were decommissioned that prevented the software from running properly and displayed the following message:

We tried working with the “new” company, Noblic Inc., but found their customer support and server licensing less than desired. We began implementing an internal solution, one designed to require much less maintenance and effort integrating new operating system versions and browser updates.

For example, this FAQ was posted to the NobKiosk, but as of now has been removed. Note “If you have a license of xStand, you can continue using it for free until end of 2018” wasn’t held up in our experience.

As the posting of this article xStand is still available via the Mac App Store, but is no longer supported or in development by the original developers, adnX SARL.

Brave New World


Our kiosk implementation leverages WordPress to provide our content creators, primarily the libraries marketing and communications group, with tools they are already familiar with. As you will see, this approach offers a high degree of control and customization.

With the WordPress site, we implemented the plugin called LayerSlider that allows easily editing pages, animations & effects used on our digital displays and other WordPress sites.

  • LayerSlider – Digital Display Settings Example

  • LayerSlider – Editing Digital Display Slides Example



  • LayerSlider – Digital Display Animation Options Example

Hide Mouse Cursor
We also implemented the following Cascading Style Sheets (CSS) to hide the mouse cursor:

#selector {

cursor: none;

}

Another option on Mac systems is using Cursorcerer Preference Pane which allows you to which allows you to hide the cursor at any time by use of a global hotkey or based on idle time and bring it back as soon as you move the mouse. For our digital displays setup we thought it would be better to go the CSS route vs installing additional software to give us this functionality. But, for other scenarios like a media server, Cursorcerer might meet your needs if you can’t depend on CSS.

Manage Display Settings
To manage the display preferences on our Mac systems, we developed the python script/library named Display Manager and made it available to others on our GitHub repository. It allows users to programmatically manage Mac system display settings like resolution, refresh rate, rotation, brightness, screen mirroring, and HDMI underscan. It’s primarily intended for Mac Admins who need to control displays in specific, predictable ways. Display Manager works in a 2-part package: a library that actually controls Mac displays (display_manager_lib.py), and a script that allows access via the command line, and executes commands in a non-interfering way (display_manager.py).

On our digital display implementation, we set the resolution, rotate the display 90 degrees and set the HDMI underscan to properly fill the display.

For example, we use Jamf Pro and scope our digital display to a policy that runs a shell script with parameter values.

Using the following script:

#!/bin/sh

# Check to make sure the library and command-line API are both installed
if [[ -e /Library/Python/2.7/site-packages/display_manager_lib.py && \
-e /usr/local/bin/display_manager.py ]] ; then

    # Positional parameters. If not mentioned, will stay empty
    res=""
    rotate=""
    brightness=""
    underscan=""
    mirror=""

# Set positional parameters if mentioned
if [[ -n  "$4" ]] ; then
    res="res $4"
fi
if [[ -n  "$5" ]] ; then
    rotate="rotate $5"
fi
if [[ -n  "$6" ]] ; then
    brightness="brightness $6"
fi
if [[ -n  "$7" ]] ; then
    underscan="underscan $7"
fi
if [[ -n  "$8" ]] ; then
    mirror="mirror $8"
fi

/usr/local/bin/display_manager.py $res $rotate $brightness $underscan $mirror

else
    exit 1
fi

And run it ongoing at every login and via custom trigger to allow easy ad-hoc execution to set display settings.

sudo jamf policy -event DigitalDisplay -verbose

If your environment isn’t using Jamf Pro, you could easily use a similar methodology with tools like Outset or custom launch item to run based on your environment needs.

 

Google Chrome Command Line Switches

We decided to use Google Chrome for our browser kiosk implementation using Chromimum command line switches.

Using the this command format:

/path/to/Google Chrome.app/Contents/Google Chrome [SWITCH]

We used the following switches:

  • –kiosk
    Enable kiosk mode with Google Chrome in full screen.

    /path/to/Google Chrome.app/Contents/Google Chrome –kiosk


  • –kiosk [WEB_ADDRESS]
    Enable kiosk mode with Google Chrome in full screen with specific web site.

    /path/to/Google Chrome.app/Contents/Google Chrome –kiosk https://lib.utah.edu

    • –app=[WEB ADDRESS]
      Specifies that Google Chrome should be launched in “application” mode without address bar.

      /path/to/Google Chrome.app/Contents/Google Chrome –kiosk –app=https://lib.utah.edu

 

  • –force-first-run
    Displays the First Run experience when the browser is started, regardless of whether or not it’s actually the First Run (this overrides kNoFirstRun).

Process Overview
Here is an overview of how the various pieces of our kiosk system work together:

Script
Here is the python script we are using with our kiosk implementation. It launches Google Chrome based on command line switched based via configuration property list, then uses AppleScript to verify its the frontmost application, verifies if screen saver is running using pgrep, uses ioreg to verify is display is asleep,  removes Google Chrome profile to cleanup previous user session,  and based on conditions will restart Google Chrome if necessary like if it has crashed, quit by user or if screen saver activated or display asleep, etc.

#!/usr/bin/python
# -*- coding: utf-8 -*-

from __future__ import print_function

import sys
import os
import subprocess
import time
import re
import plistlib
import signal
import shutil
import logging
import json
from datetime import datetime, timedelta

from management_tools import loggers

'''Keep Google Chrome Running in Kiosk Mode
'''

__author__ = "Sam Forester"
__email__ = "sam.forester@utah.edu"
__copyright__ = "Copyright (c) 2018 University of Utah, Marriott Library"
__license__ = "MIT"
__version__ = '1.9.1'
__url__ = None
__description__ = 'Keep Google Chrome Running in Kiosk Mode'

## CHANGELOG:
#   1.8.0: (2018.10.18)
#       - added remove_user_chrome_profile
#       - modified switch processing
#       - added more logging
#       - added logging stream handler and --verbose flag
#       - modified run loop
#       - replaced set_restart to restart_timer
#       - modified restart_timer to allow for null timers
#       - added pgrep
#       - modified screensaver_is_running to use pgrep()
#   1.8.1: (2018.10.18)
#       - fixed bug that would cause endless tabs to be re-opened
#         if Google Chrome was running at RunTime
#       - renamed launch_chrome_with_switches to launch_chrome
#       - fixed bug causing looping to move too fast during screensaver
#   1.8.2: (2018.10.18)
#       - fixed bug that was not allowing user profile to be removed
#       - added faster loop to restore Chrome after screensaver
#   1.9.0: (2018.10.19)
#       - added better mechanism for checking frontmost window
#       - added ability to detect display sleep
#   1.9.1: (2018.10.19)
#       - fixed error with Popen.call()
#       - fixed parameter error

class SignalTrap(object):
    '''Class for trapping interruptions in an attempt to shutdown
    more gracefully
    '''
    def __init__(self, logger):
        self.stopped = False
        self.log = logger
        signal.signal(signal.SIGINT, self.trap)
        signal.signal(signal.SIGQUIT, self.trap)
        signal.signal(signal.SIGTERM, self.trap)
        signal.signal(signal.SIGTSTP, self.trap)

    def trap(self, signum, frame):
        self.log.debug("received signal: {0}".format(signum))
        self.stopped = True


def app_is_frontmost(name):
    '''Uses applescript to see if specified app name running and
    frontmost window.

    Returns True or False
    '''
    # doesn't require Accessibility access
    scpt = ['tell application "System Events"',
                'try',
                    'tell process "{0}"',
                        'if (frontmost is false) then return false',
                    'end tell',
                    'tell application "{0}"',
                        'return (count of windows) is greater than 0',
                    'end tell',
                'on error',
                    'return false',
                'end try',
            'end tell']
    # join all the strings and then format
    applscpt = "\n".join(scpt).format(name)
    cmd = ['osascript', '-e', applscpt]
    try:
        out = subprocess.check_output(cmd).rstrip()
        return True if out == 'true' else False
    except subprocess.CalledProcessError:
        return False

def screensaver_is_running():
    '''Returns True if ScreenSaverEngine is running
    '''
    return pgrep(None, 'ScreenSaverEngine')

def restart_timer(logger=None, **kwargs):
    '''Returns a closure that uses the time of assignment 
    to return True if that amount of time has passed
    if given 

    ::params:: anything that can be used by datetime.timedelta()

    >>> restart = set_restart(seconds=5)
    >>> time.sleep(1)
    >>> restart()
    False
    >>> time.sleep(5)
    >>> restart()
    True
    >>> restart()
    True

    >>> restart = set_restart(hours=2)
    >>> time.sleep(1)
    >>> restart()
    False
    >>> time.sleep(7,200)
    >>> restart()
    True

    >>> restart = set_restart(seconds=0)
    >>> time.sleep(1000)
    >>> restart()
    False
    >>> time.sleep(5)
    >>> restart()
    False

    '''
    now = datetime.now().replace(microsecond=0)
    restart = now + timedelta(**kwargs)
    empty = (now == restart) or (restart < now)
    if logger and not empty:
        logger.debug("restart timer set: {0}".format(restart))
    def _restart():
        if empty:
            return False
        else:
            return datetime.now() > restart
    return _restart

def display_power():
    '''Uses `ioreg` to return values of IODisplayWrangler's 
    IOPowerManagent
    '''
    cmd = ['/usr/sbin/ioreg', '-w', '0', '-n', 'IODisplayWrangler', 
                                         '-r', 'IODisplayWrangler']
    out = subprocess.check_output(cmd)
    m = re.search(r'"IOPowerManagement" = (\{.+\})', out, re.MULTILINE)
    # convert ioreg out put into something more JSON-y
    j = m.group(1).replace('=', ':')
    return json.loads(j)

def display_sleep():
    '''Returns True if display is asleep
    '''
    if display_power()["CurrentPowerState"] < 3:
        return True
    else:
        return False

def pgrep(logger, name):
    '''Uses /usr/bin/pgrep to return list of running PIDs.
    returns empty list if no PIDs are found
    '''
    if not logger:
        logger = logging.getLogger(__name__)
        logger.addHandler(logging.NullHandler())
    cmd = ['/usr/bin/pgrep', name]
    logger.debug("> {0}".format(" ".join(cmd)))
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE)
    out, err = p.communicate()
    if p.returncode == 0:
        pids = [x for x in out.splitlines() if x]
        logger.debug("{0}: pids: {1}".format(name, pids))
        return pids
    else:
        return []
    
def remove_user_chrome_profile(logger):
    '''Removes ~/Library/Application Support/Google
    '''
    user_d = os.path.expanduser('~/Library/Application Support/Google')
    logger.debug("removing user chrome settings: {0}".format(user_d))
    try:
        shutil.rmtree(user_d)
    except OSError as e:
        # skip OSError: [Errno 2] No such file or directory
        logger.error("unable to remove: {0}: {1}".format(user_d, e))
        if e.errno != 2:
            #logger.error("unable to remove: {0}".format(user_d))
            raise
    if os.path.exists(user_d):
        logger.debug("still exists: {0}".format(user_d))

def launch_chrome(logger, switches, app=None, reset=True):
    '''Launches Google Chrome in with specified flags
    '''
    if not app:
        # default Google Chrome.app location
        app = '/Applications/Google Chrome.app'

    if reset:
        remove_user_chrome_profile(logger)

    chromebin = os.path.join(app, 'Contents/MacOS/Google Chrome')
    cmd = [chromebin] + switches
    logger.debug("> {0}".format(" ".join(cmd)))
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, 
                              stderr=subprocess.PIPE)
    logger.debug("chrome PID: {0}".format(p.pid))
    return p

def main(args):
    '''Launch Google Chrome and make sure it's in the foreground
    '''
    script = os.path.basename(sys.argv[0])
    scriptname = os.path.splitext(script)[0]

    level = loggers.INFO
    if '--debug' in args:
        level = loggers.DEBUG

    logger = loggers.FileLogger(name=scriptname, level=level)
    if '--verbose' in args:
        sh = loggers.StreamLogger(level=level)
        logger.addHandler(sh)

    logger.debug("{0} started!".format(script))
    settings = '/Library/Management/edu.utah.mlib.kiosk.settings.plist'
        
    # get settings from file
    try:
        logger.debug("getting settings from: {0}".format(settings))
        config = plistlib.readPlist(settings)
    except Exception as e:
        logger.error(e)
        raise

    try:
        site = config['site']
    except KeyError:    
        logger.error("no site was specified")
        raise SystemExit("no site was specified")

    # add any additionally specified switches
    switches = config.get('switches', [])
    
    switches += ['--kiosk']
    if config.get('isDisplay'):
        switches.append("--app={0}".format(site))
    else:
        switches.append(site)

    # path to chrome app (default: /Applications/Google Chrome.app)
    app = config.get('location', '/Applications/Google Chrome.app')
    logger.debug("location: {0}".format(app))
    
    # seconds to wait between loops
    wait = config.get('wait', 5)    
    logger.debug("wait: {0}".format(wait))

    # timer to restart: (default: def _(): return False)
    restart = config.get('restart', -1)
    logger.debug("restart seconds: {0}".format(restart))
    time_to_restart = restart_timer(seconds=restart)

    # clear user profile between launches (default: True)
    reset = config.get('remove-profile', True)    
    logger.debug("remove profile: {0}".format(reset))

    # Kill any running instances of Google Chrome (or endless tabs)
    pids = pgrep(logger, "Google Chrome")
    if pids:
        logger.debug("Chrome was already running... killing...")
        cmd = ['/usr/bin/killall', "Google Chrome"]
        logger.debug("> {0}".format(" ".join(cmd)))
        subprocess.Popen(cmd, stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE).wait()
    # start Google Chrome
    logger.info("starting Google Chrome")
    chrome = launch_chrome(logger, switches, app, reset)

    sig = SignalTrap(logger)
    while not sig.stopped:
        # if chrome.poll() returns anything but None, it exited
        running = chrome.poll() is None

        if screensaver_is_running() or display_sleep():
            if running:
                logger.debug("closing chrome while display inactive")
                chrome.terminate()
                chrome.wait()
            # TO-DO: would like to keep this from looping every second
            # during screensaver, but that's for another day
            time.sleep(1)
            continue

        # Automatically restart Chrome after a certain amount of time
        if time_to_restart():
            msg = "restarting after {0} seconds".format(restart)
            logger.debug(msg)
            logger.debug("resetting restart timer")
            time_to_restart = set_restart(seconds=restart)
            if running:
                chrome.terminate()
                chrome.wait()
                continue
            else:
                logger.debug("chrome wasn't running... odd")
            logger.debug("resetting restart timer")
            time_to_restart = set_restart(seconds=restart)
        
        # check to see that Chrome is the frontmost process
        if running and not sig.stopped:
            if not app_is_frontmost("Google Chrome"):
                logger.debug("Google Chrome isn't active")
                chrome.terminate()
                chrome.wait()
                running = False

        # Finally, restart chrome if it isn't running
        if not running:
            logger.error("chrome isn't running")
            pid = chrome.pid
            poll = chrome.poll()
            logger.debug("dead chrome: {0} poll: {1}".format(pid,poll))
            # relaunch chrome
            chrome = launch_chrome(logger, switches, app, reset)
        time.sleep(wait)

    chrome.terminate()
    logger.debug("{0} finished!".format(script))
    return 0

if __name__ == '__main__':
    try:
        args = sys.argv[1:]
    except IndexError:
        args = []
    retcode = main(args)
    sys.exit(retcode)

With the following LaunchAgent:

<?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>POSIXSpawnType</key>
	<string>App</string>
	<key>LimitLoadToSessionType</key>
	<string>Aqua</string>
	<key>KeepAlive</key>
	<dict>
		<key>SuccessfulExit</key>
		<false/>
	</dict>
	<key>Label</key>
	<string>edu.utah.mlib.kiosk.chrome.display</string>
	<key>CFBundleIdentifier</key>
	<string>com.google.Chrome</string>
	<key>Program</key>
	<string>/usr/local/bin/chrome_kiosk.py</string>
	<key>StandardOutPath</key>
	<string>/tmp/chrome_kiosk.log</string>
	<key>StandardErrorPath</key>
	<string>/tmp/chrome_kiosk.log</string>
</dict>
</plist>

This LaunchAgent sends standard output to /tmp/chrome_kiosk.log , standard error to /tmp/chrome_kiosk.log , will keep alive if the script fails and is limited to Aqua session.

Apple Technical Note named “Daemons and Agents” that explains most of the session types available for launchd.

  • Aqua – GUI agent which has access to all the GUI services
  • LoginWindow –  Pre-login agent which runs in the login window context
  • Background –  Runs in the parent context of the user
  • StandardIO – Runs only in non-GUI login session (i.e SSH sessions)

A useful GUI application that allows you to create, manage, debug & learning system and user services & options on Mac systems is named, LaunchControl.

The property list that is used to define individual kiosk configurations either can be customized using the defaults command or a configuration profile:

<?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>isDisplay</key>
	<false/>
	<key>site</key>
	<string>https://lib.utah.edu</string>
	<key>location</key>
	<string>/Applications/Web Browsers/Google Chrome.app</string>
	<key>switches</key>
	<array>
    	<string>--force-first-run</string>
	</array>
	<key>wait</key>
	<integer>5</integer>
	<key>restart</key>
	<integer>7200</integer>
	<key>remove-profile</key>
    <true/>
</dict>
</plist>

Which we store on our kiosks at the following location:

/Library/Management/edu.utah.mlib.kiosk.settings.plist

Future


In the future we we might migrate from Privoxy to EZProxy after it has been thoroughly tested and staged. We are currently using EZproxy to provide access from outside our library’s network to restricted-access websites that authenticate users by IP address and if it will give us similar functionality as Privoxy with our kiosk implementation would lessen the amount of proxy services we are supporting.

If there is interest by the community in our kiosk methodology and code, we will clean it up and make it more community friendly and post to our public GitHub repository. Let us know by using our contact us page or post a comment to this blog post.

2 Comments
  • Richard Glaser
    Posted at 17:12h, 05 February Reply

    FYI:

    Received some feedback on this post about if we investigated OpenKiosk, based on Firefox by the Mozilla development group. We didn’t investigate this option, but would be interested in others sharing their experience.

    Here is more information about OpenKiosk…

    OpenKiosk is a cross platform kiosk web browser based on Mozilla Firefox that can be easily installed and used to secure a computer for use as a public terminal. This is a complete solution for any kiosk installation. This software is released under the MPL “as is” with no warranty or support.

    OpenKiosk is currently deployed in schools, universities, libraries, hospitals, airports, hotels, governments, and businesses across the globe.

    http://openkiosk.mozdevgroup.com

  • Pingback:Marriott Library - Apple ITS | Google Chrome for Kiosks Posted to GitHub
    Posted at 14:16h, 29 April Reply

    […] For more detailed information about our Mac kiosk implementation, see our blog post. […]

Leave a Reply to Marriott Library - Apple ITS | Google Chrome for Kiosks Posted to GitHubCancel reply