Skip to content

Commit

Permalink
Add new launcher script and luggage package components
Browse files Browse the repository at this point in the history
- New launcher script, coordinated by LaunchAgents and LaunchDaemons,
  handles delivering the notification in the console user's context even
  if there is no console user (i.e. an admin tool is requesting the
  notification).
  • Loading branch information
sheagcraig committed Sep 29, 2016
1 parent 1d29b92 commit 336ee90
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.pyc
*.app
*.pkg
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased][unreleased]

### Changed
- Updated to Swift 3 syntax.
- Updated to a fork of CommandLine (IngmarStein's PR to update for Xcode8/Swift3)

### Added
- New yo.py launcher (package installer puts it at `/usr/local/bin/yo`).
- New Yo launch system! LaunchDaemon and two LaunchAgents to allow reliable
notification triggering by the management tool of your choice. Thanks to
@chilcote and @grahamgilbert for the inspiration and technical assistance.

### Removed
- Removed casper directory in lieue of new launcher script system.
- Removed yo.sh.

## [1.0.3] - 2015-08-22 - Prince's Hair in Purple Rain
### Fixed
- Yo now properly uses the BundleIdentifier rather than the string literal `org.da.yo`. Thanks for the spot @mcfly1976. (#6)
Expand Down
14 changes: 14 additions & 0 deletions pkg/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
include /usr/local/share/luggage/luggage.make

TITLE=yo
REVERSE_DOMAIN=com.sheagcraig
PAYLOAD=\
pack-utilities-yo.app \
pack-usr-local-bin-yo.py \
pack-Library-LaunchAgents-com.sheagcraig.yo.on_demand.plist \
pack-Library-LaunchAgents-com.sheagcraig.yo.login_once.plist \
pack-Library-LaunchDaemons-com.sheagcraig.yo.cleanup.plist \
pack-script-postinstall \
pack-script-preinstall \

PACKAGE_VERSION=$(shell defaults read "$(PWD)/yo.app/Contents/Info.plist" CFBundleShortVersionString)
23 changes: 23 additions & 0 deletions pkg/com.sheagcraig.yo.cleanup.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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.sheagcraig.yo.cleanup</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/yo</string>
<string>--cleanup</string>
</array>
<key>KeepAlive</key>
<dict>
<key>PathState</key>
<dict>
<key>/private/tmp/.com.sheagcraig.yo.cleanup.launchd</key>
<true/>
</dict>
</dict>
<key>OnDemand</key>
<true/>
</dict>
</plist>
19 changes: 19 additions & 0 deletions pkg/com.sheagcraig.yo.login_once.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?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.sheagcraig.yo.login_once</string>
<key>LimitLoadToSessionType</key>
<array>
<string>Aqua</string>
</array>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/yo</string>
<string>--cached</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
25 changes: 25 additions & 0 deletions pkg/com.sheagcraig.yo.on_demand.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?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.sheagcraig.yo.on_demand</string>
<key>LimitLoadToSessionType</key>
<array>
<string>Aqua</string>
</array>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/yo</string>
<string>--cached</string>
</array>
<key>KeepAlive</key>
<dict>
<key>PathState</key>
<dict>
<key>/private/tmp/.com.sheagcraig.yo.on_demand.launchd</key>
<true/>
</dict>
</dict>
</dict>
</plist>
4 changes: 4 additions & 0 deletions pkg/postinstall
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

# Rename yo.sh to yo so we can call it like a regular cli utility.
mv /usr/local/bin/yo.py /usr/local/bin/yo
7 changes: 7 additions & 0 deletions pkg/preinstall
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

# Weirdly, yo doesn't overwrite it's old app; instead it makes a
# localized folder. Just erase it first.
rm -rf /Applications/Utilities/yo.app
rm -rf /Applications/Utilities/yo
rm -rf /Applications/Utilities/yo.localized
172 changes: 172 additions & 0 deletions pkg/yo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/python


import argparse
import os
from subprocess import call
import sys
import time

from Foundation import (CFPreferencesAppSynchronize, CFPreferencesCopyAppValue,
CFPreferencesSetValue, kCFPreferencesAnyUser,
kCFPreferencesCurrentHost)
from SystemConfiguration import SCDynamicStoreCopyConsoleUser

__version__ == 2.0.0
BUNDLE_ID = "com.sheagcraig.yo"
CLEANUP_PATH = "/private/tmp/.com.sheagcraig.yo.cleanup.launchd"
WATCH_PATH = "/private/tmp/.com.sheagcraig.yo.on_demand.launchd"
YO_BINARY = "/Applications/Utilities/yo.app/Contents/MacOS/yo"
YO_HELP = """\
Yo app notification options:
-t, --title:
Title for notification. REQUIRED.
-s, --subtitle:
Subtitle for notification.
-n, --info:
Informative text.
-b, --action-btn:
Include an action button, with the button label text supplied to this
argument.
-a, --action-path:
Application to open if user selects the action button. Provide the full
path as the argument. This option only does something if
-b/--action-btn is also specified.
-B, --bash-action:
Bash script to run. Be sure to properly escape all reserved characters.
This option only does something if -b/--action-btn is also specified.
Defaults to opening nothing.
-o, --other-btn:
Alternate label for cancel button text.
-i, --icon:
Complete path to an alternate icon to use for the notification.
-c, --content-image:
Path to an image to use for the notification's 'contentImage' property.
-z, --delivery-sound:
The name of the sound to play when delivering or 'None'. The name must
not include the extension, nor any path components, and should be
located in '/Library/Sounds' or '~/Library/Sounds'. (Defaults to the
system's default notification sound). See the README for more info.
-d, --ignores-do-not-disturb:
Set to make your notification appear even if computer is in
do-not-disturb mode.
-l, --lockscreen-only:
Set to make your notification appear only if computer is locked. If
set, no buttons will be available.
-p, --poofs-on-cancel:
Set to make your notification 'poof' when the cancel button is hit.
-m, --banner-mode:
Does not work! Set if you would like to send a non-persistent
notification. No buttons will be available if set.
-v, --version:
Display Yo version information."""


def main():
# Capture commandline args.
parser = get_argument_parser()
launcher_args, yo_args = parser.parse_known_args()

# args = sys.argv
# Replace this script's path with the yo app's path.
# args[0] = "/Applications/Utilities/yo.app/Contents/MacOS/yo"

if any(flag in yo_args for flag in ("--version", "-v")):
# import pdb; pdb.set_trace()
# Skip further checks if version is requested.
args = ["yo.py"] + yo_args
run_yo_with_args(args)

elif launcher_args.cached:
# Yo is being run by a LaunchAgent for the current console user.
cached_args = get_cached_args()
all_args = cached_args + [yo_args]

# Post all of the stored notifications!
for arg_set in all_args:
run_yo_with_args(arg_set)

# Trigger the LaunchDaemon to clean up.
with open(CLEANUP_PATH, "w") as ofile:
ofile.write("Yo!")

elif launcher_args.cleanup:
# Yo is being called by the cleanup LaunchDaemon.
clear_args()

elif not is_console_user() and os.getuid() == 0:
# Only the current console user can trigger a notification.
# So we will cache the required arguments and try to trigger
# an on_demand notification. If there is no console user, the
# notification will trigger on the next login.
cache_args(yo_args)

if get_console_user()[0]:
with open(WATCH_PATH, "w") as ofile:
ofile.write("Yo!")

time.sleep(5)
os.remove(WATCH_PATH)

else:
# Yo has been run by the current user directly
# Non-root users cannot get the cached notifications, so just
# run the one provided on the commandline.
run_yo_with_args(yo_args)


def get_argument_parser():
"""Create our argument parser."""
description = "Yo launcher arguments:"
parser = argparse.ArgumentParser(
description=description,
formatter_class=argparse.RawDescriptionHelpFormatter)
phelp = ("Run cached notifications (must be run as console user). This "
"option is normally run")
parser.add_argument("--cached", help=argparse.SUPPRESS, action="store_true")
phelp = "Clean up cached notifications (must run as root)."
parser.add_argument("--cleanup", help=argparse.SUPPRESS, action="store_true")

parser.epilog = YO_HELP

return parser


def run_yo_with_args(args):
args = [YO_BINARY] + args
call(args)


def is_console_user():
return os.getuid() == get_console_user()[1]


def get_console_user():
return SCDynamicStoreCopyConsoleUser(None, None, None)


def get_cached_args():
notifications = CFPreferencesCopyAppValue("Notifications", BUNDLE_ID)
return notifications or []


def cache_args(args):
notifications = CFPreferencesCopyAppValue("Notifications", BUNDLE_ID)
if not notifications:
notifications = []

notifications = notifications + [args]

CFPreferencesSetValue("Notifications", notifications, BUNDLE_ID,
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
CFPreferencesAppSynchronize(BUNDLE_ID)


def clear_args():
CFPreferencesSetValue("Notifications", [], BUNDLE_ID,
kCFPreferencesAnyUser, kCFPreferencesCurrentHost)
CFPreferencesAppSynchronize(BUNDLE_ID)


if __name__ == "__main__":
main()

0 comments on commit 336ee90

Please sign in to comment.