├── .gitignore ├── Install Guide.md ├── LICENSE ├── Library ├── LaunchAgents │ └── com.github.wardspardox.dock-maintainer.plist └── LaunchDaemons │ └── com.github.wardsparadox.dock-maintainer.updater.plist ├── README.md ├── dock-maintainer-updater.py ├── dock-maintainer.py └── outset_dock-maintainer ├── .gitignore ├── build-info.plist └── payload ├── boot-every └── dock-maintainer-updater.py └── login-every └── dock-maintainer.py /.gitignore: -------------------------------------------------------------------------------- 1 | # .DS_Store files! 2 | .DS_Store 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | -------------------------------------------------------------------------------- /Install Guide.md: -------------------------------------------------------------------------------- 1 | # Install Guide: 2 | 1. Deploy via either method mentioned in the README.MD document. 3 | 2. On a server create an accessible folder (I used my pre-existing Munki repo server as it was already configured as needed.) on a web server. 4 | ie: `http://example.com/munki_repo/docksetups` 5 | 3. Create a plist in that directory that is structured with two arrays. One named `Apps` and one named `Others`. Inside of those array's put the **full path** to the applications (~ shortcut for home also works in the `Others` directory only!) you want to be put in the dock. Something like so: 6 | ```xml 7 | 8 | 9 | 10 | 11 | Apps 12 | 13 | /Applications/Google Chrome.app 14 | ~/Applications/Slack.app 15 | /Applications/Atom.app 16 | /Applications/iTerm.app 17 | /Applications/Boostnote.app 18 | /Applications/Microsoft Remote Desktop Beta.app 19 | 20 | Others 21 | 22 | /Applications 23 | ~/Downloads 24 | 25 | 26 | 27 | ``` 28 | 4. On the computer that you want an account's dock to be maintained, run the following, where username is a short name of the user you want maintained and ServerURL is the **url to the folder containing the dock plists, not the plists itself**: 29 | `/usr/bin/defaults write /Library/Preferences/com.github.wardsparadox.dock-maintainer ManagedUser "username"` 30 | `/usr/bin/defaults write /Library/Preferences/com.github.wardsparadox.dock-maintainer ServerURL "http://example.com/munki_repo/docksetups"` 31 | 5. You can then reboot the machine or run `/usr/local/outset/outset --boot` to update or download the cached plist on the machine. 32 | 6. Login as the user you wish to see and badaboom, dock should be the one you wish to see (note: outset can take a few seconds to startup after login.) 33 | 7. Logs for the updater are in `/Library/Logs` and the maintainer has logs in `~/Library/Logs` as it runs as the user. 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zack McCauley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Library/LaunchAgents/com.github.wardspardox.dock-maintainer.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.github.wardsparadox.dock-maintainer.maintainer 7 | Program 8 | /usr/local/bin/dock-maintainer/maintainer.py 9 | RunAtLoad 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Library/LaunchDaemons/com.github.wardsparadox.dock-maintainer.updater.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.github.wardsparadox.dock-maintainer.updater 7 | Program 8 | /usr/local/bin/dock-maintainer/dock-maintainer-updater.py 9 | RunAtLoad 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dock-maintainer 2 | 3 | ## 2022-05-11 4 | 5 | Repo archived. If you need something similar: https://github.com/joncrain/dynamic_dock 6 | 7 | 8 | 9 | 10 | 11 | Manages dock for a specified account. I recommend using this with [Outset](https://github.com/chilcote/outset) and [Munki](https://github.com/munki/munki) (Outset for launching files as needed, Munki for hosting the `docksetup` files as it's web server requirements mirror the ones here) 12 | 13 | Article explaining this tool: https://wardsparadox.github.io/2017/05/dock-maintainer-defined/ 14 | 15 | SSTP Install Guide: [Install Guide.md](https://github.com/WardsParadox/dock-maintainer/blob/master/Install%20Guide.md) 16 | 17 | **Please file an issue if you have issues. I am not the best of programmers. 18 | 19 | ***Edge Case: 20 | When running this utility if you update the plist on the computer or manually modify the dock. Things may not work as intended if either of those options are used.*** 21 | 22 | ## Two Methods of Deployment: 23 | `dock-maintainer.py` needs to run as a user. I recommend using Outset but I have included `launchctl` files for the appropriate files in the right zones. 24 | 25 | `dock-maintainer-updater.py` needs to run as root. This is due to storing the server copy file in the AllUsers Library (`/Library`) as well as logs. 26 | 27 | ### Use Outset: 28 | 1. Deploy `dock-maintainer.py` to `/usr/local/outset/login-every` 29 | 2. Deploy `dock-maintainer-updater.py` to `/usr/local/outset/boot-every` 30 | 3. `chmod 755 /path/to/files/above && chown root:wheel /path/to/files/above` 31 | 4. Clap your hands with a job well done! 👏🏻 32 | 33 | ### Deploy it w/o Outset 34 | 1. Deploy both files to: `/usr/local/bin/dock-maintainer` 35 | 2. `chmod -R 755 /usr/local/bin/dock-maintainer && chown -R root:wheel /usr/local/bin/dock-maintainer` 36 | 3. Deploy the LaunchAgent and LaunchDaemon included or write your own 37 | 4. Go contemplate why you don't use Outset. 😡 **(I jest!😛)** 38 | 39 | I recommend using [Munki-Pkg](https://github.com/munki/munki-pkg) with both scenarios above! 40 | 41 | ## Preferences Needing to be set on the machine: 42 | domain: com.github.wardsparadox.dock-maintainer 43 | ManagedUser - Which user to run for 44 | ServerURL - path to web server folder holding all plists for user 45 | ``` 46 | { 47 | ManagedUser = "student"; 48 | ServerURL = "http://example.com/munki_repo/docksetups"; 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /dock-maintainer-updater.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | Downloads file as needed for dock-maintainer. Sets extended attribute to be 4 | the last mod date for the plist according to the server. Suggested work around 5 | by elios on macadmins.org 6 | ''' 7 | import urllib2 8 | import datetime 9 | from time import mktime 10 | import os 11 | import logging 12 | import xattr 13 | from Foundation import CFPreferencesCopyAppValue 14 | 15 | logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s', 16 | datefmt='%Y-%m-%d %I:%M:%S %p', 17 | level=logging.INFO, 18 | filename=os.path.normpath("/Library/Logs/dock-maintainer.log")) 19 | stdout_logging = logging.StreamHandler() 20 | stdout_logging.setFormatter(logging.Formatter()) 21 | logging.getLogger().addHandler(stdout_logging) 22 | 23 | 24 | def downloadFile(url, filepath, attributedate): 25 | ''' 26 | Downloads url to filepath 27 | ''' 28 | with open(filepath, "wb") as code: 29 | code.write(url.read()) 30 | xattr.setxattr(filepath, 'dock-maintainer.Last-Modified-date', str(attributedate)) 31 | logging.info("Downloaded File to %s", filepath) 32 | return filepath 33 | 34 | def wait_for_internet_connection(): 35 | ''' 36 | Shameless swipe from stackoverflow. 37 | https://stackoverflow.com/a/10609378 38 | ''' 39 | while True: 40 | try: 41 | response = urllib2.urlopen('https://www.google.com/', timeout=1) 42 | return 43 | except urllib2.URLError: 44 | pass 45 | 46 | def main(): 47 | ''' 48 | Main Controlling Module: 49 | - Checks preferences set 50 | - Checks for Path, if not creates 51 | - Downloads plist if needed 52 | ''' 53 | keys = {} 54 | keys["ManagedUser"] = CFPreferencesCopyAppValue("ManagedUser", 55 | "com.github.wardsparadox.dock-maintainer") 56 | 57 | keys["ServerURL"] = CFPreferencesCopyAppValue("ServerURL", 58 | "com.github.wardsparadox.dock-maintainer") 59 | keys["FileName"] = CFPreferencesCopyAppValue("FileName", 60 | "com.github.wardsparadox.dock-maintainer") 61 | 62 | path = os.path.realpath("/Library/Application Support/com.github.wardsparadox.dock-maintainer") 63 | if os.path.exists(path): 64 | logging.info("Path exists at %s", path) 65 | else: 66 | logging.info("Path not found, creating at %s", path) 67 | os.mkdir(path, 0755) 68 | 69 | if keys["ManagedUser"] is None: 70 | logging.error("No ManagedUser Preference set") 71 | exit(2) 72 | else: 73 | plistfilepath = os.path.join(path, keys["ManagedUser"]) 74 | completeurl = os.path.join(keys["ServerURL"], keys["FileName"]) 75 | try: 76 | fileurl = urllib2.urlopen(completeurl) 77 | meta = fileurl.info().getheaders("Last-Modified")[0] 78 | servermod = datetime.datetime.fromtimestamp(mktime( 79 | datetime.datetime.strptime( 80 | meta, "%a, %d %b %Y %X GMT").timetuple())) 81 | except urllib2.HTTPError: 82 | logging.error("Can not connect to url") 83 | exit(1) 84 | 85 | if not os.path.isfile(plistfilepath): 86 | logging.info("File not found! Downloading") 87 | downloadFile(fileurl, plistfilepath, servermod) 88 | exit(0) 89 | 90 | if xattr.listxattr(plistfilepath): 91 | logging.info("Got File Attributes") 92 | else: 93 | logging.info("No Attributes found! Downloading and setting them") 94 | downloadFile(fileurl, plistfilepath, servermod) 95 | exit(0) 96 | filexattrdate = xattr.getxattr(plistfilepath, 97 | 'dock-maintainer.Last-Modified-date') 98 | if str(servermod) != filexattrdate: 99 | logging.info("File is out of date") 100 | downloadFile(fileurl, plistfilepath, servermod) 101 | else: 102 | logging.info("File is synced.") 103 | exit(0) 104 | 105 | if __name__ == '__main__': 106 | wait_for_internet_connection() 107 | main() 108 | -------------------------------------------------------------------------------- /dock-maintainer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | A wrapper for dockutil that manages a specified users dock with a server based plist for the list. 4 | ''' 5 | import subprocess 6 | import plistlib 7 | import os 8 | import logging 9 | from Foundation import kCFPreferencesCurrentHost, \ 10 | CFPreferencesCopyAppValue, \ 11 | CFPreferencesSetAppValue, \ 12 | CFPreferencesSetMultiple, \ 13 | kCFPreferencesCurrentUser, \ 14 | CFPreferencesAppSynchronize, \ 15 | NSURL 16 | 17 | from SystemConfiguration import SCDynamicStoreCopyConsoleUser 18 | 19 | logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s', 20 | datefmt='%Y-%m-%d %I:%M:%S %p', 21 | level=logging.INFO, 22 | filename=os.path.expanduser('~/Library/Logs/dock-maintainer.log')) 23 | stdout_logging = logging.StreamHandler() 24 | stdout_logging.setFormatter(logging.Formatter()) 25 | logging.getLogger().addHandler(stdout_logging) 26 | 27 | class DockError(Exception): 28 | '''Basic exception''' 29 | pass 30 | 31 | class Dock(): 32 | '''Class to handle Dock operations''' 33 | _DOMAIN = 'com.apple.dock' 34 | _DOCK_PLIST = os.path.expanduser( 35 | '~/Library/Preferences/com.apple.dock.plist') 36 | _DOCK_LAUNCHAGENT_ID = 'com.apple.Dock.agent' 37 | _DOCK_LAUNCHAGENT_FILE = '/System/Library/LaunchAgents/com.apple.Dock.plist' 38 | _SECTIONS = ['persistent-apps', 'persistent-others'] 39 | items = {} 40 | 41 | def __init__(self): 42 | for key in self._SECTIONS: 43 | try: 44 | section = CFPreferencesCopyAppValue(key, self._DOMAIN) 45 | self.items[key] = section.mutableCopy() 46 | except Exception: 47 | raise 48 | 49 | def save(self): 50 | '''saves our (modified) Dock preferences''' 51 | # unload Dock launchd job so we can make our changes unmolested 52 | subprocess.call( 53 | ['/bin/launchctl', 'unload', self._DOCK_LAUNCHAGENT_FILE]) 54 | 55 | for key in self._SECTIONS: 56 | try: 57 | CFPreferencesSetAppValue(key, self.items[key], self._DOMAIN) 58 | except Exception: 59 | raise DockError 60 | if not CFPreferencesAppSynchronize(self._DOMAIN): 61 | raise DockError 62 | 63 | # restart the Dock 64 | subprocess.call(['/bin/launchctl', 'load', self._DOCK_LAUNCHAGENT_FILE]) 65 | subprocess.call(['/bin/launchctl', 'start', self._DOCK_LAUNCHAGENT_ID]) 66 | 67 | def findExistingLabel(self, test_label, section='persistent-apps'): 68 | '''returns index of item with label matching test_label 69 | or -1 if not found''' 70 | for index in range(len(self.items[section])): 71 | if (self.items[section][index]['tile-data'].get('file-label') == 72 | test_label): 73 | return index 74 | return -1 75 | 76 | def removeDockEntry(self, label, section=None): 77 | '''Removes a Dock entry with matching label, if any''' 78 | if section: 79 | sections = [section] 80 | else: 81 | sections = self._SECTIONS 82 | for section in sections: 83 | found_index = self.findExistingLabel(label, section=section) 84 | if found_index > -1: 85 | del self.items[section][found_index] 86 | 87 | def replaceDockEntry(self, thePath, label=None, section='persistent-apps'): 88 | '''Replaces a Dock entry. If label is None, then a label is derived 89 | from the item path. The new entry replaces an entry with the given 90 | or derived label''' 91 | if section == 'persistent-apps': 92 | new_item = self.makeDockAppEntry(thePath) 93 | else: 94 | new_item = self.makeDockOtherEntry(thePath) 95 | if new_item: 96 | if not label: 97 | label = os.path.splitext(os.path.basename(thePath))[0] 98 | found_index = self.findExistingLabel(label, section=section) 99 | if found_index > -1: 100 | self.items[section][found_index] = new_item 101 | 102 | def makeDockAppEntry(self, thePath): 103 | '''returns a dictionary corresponding to a Dock application item''' 104 | label_name = os.path.splitext(os.path.basename(thePath))[0] 105 | ns_url = NSURL.fileURLWithPath_(thePath).absoluteString() 106 | return {'tile-data': {'file-data': {'_CFURLString': ns_url, 107 | '_CFURLStringType': 15}, 108 | 'file-label': label_name, 109 | 'file-type': 41}, 110 | 'tile-type': 'file-tile'} 111 | 112 | def makeDockOtherEntry(self, thePath, 113 | arrangement=0, displayas=1, showas=0): 114 | '''returns a dictionary corresponding to a Dock folder or file item''' 115 | # arrangement values: 116 | # 1: sort by name 117 | # 2: sort by date added 118 | # 3: sort by modification date 119 | # 4: sort by creation date 120 | # 5: sort by kind 121 | # 122 | # displayas values: 123 | # 0: display as stack 124 | # 1: display as folder 125 | # 126 | # showas values: 127 | # 0: auto 128 | # 1: fan 129 | # 2: grid 130 | # 3: list 131 | 132 | label_name = os.path.splitext(os.path.basename(thePath))[0] 133 | if arrangement == 0: 134 | if label_name == 'Downloads': 135 | # set to sort by date added 136 | arrangement = 2 137 | else: 138 | # set to sort by name 139 | arrangement = 1 140 | ns_url = NSURL.fileURLWithPath_(thePath).absoluteString() 141 | if os.path.isdir(thePath): 142 | return {'tile-data':{'arrangement': arrangement, 143 | 'displayas': displayas, 144 | 'file-data':{'_CFURLString': ns_url, 145 | '_CFURLStringType': 15}, 146 | 'file-label': label_name, 147 | 'dock-extra': False, 148 | 'showas': showas 149 | }, 150 | 'tile-type':'directory-tile'} 151 | else: 152 | return {'tile-data':{'file-data':{'_CFURLString': ns_url, 153 | '_CFURLStringType': 15}, 154 | 'file-label': label_name, 155 | 'dock-extra': False}, 156 | 'tile-type':'file-tile'} 157 | 158 | dock = Dock() 159 | 160 | def setPreferences(): 161 | ''' 162 | Sets dock preferences to keep dock as is. 163 | ''' 164 | preferences = {} 165 | preferences["contents-immutable"] = True 166 | preferences["size-immutable"] = True 167 | preferences["orientation"] = "bottom" 168 | preferences["position-immutable"] = True 169 | preferences["magnify-immutable"] = True 170 | preferences["autohide"] = False 171 | preferences["autohide-immutable"] = True 172 | preferences["tilesize"] = int(60) 173 | CFPreferencesSetMultiple(preferences, None, 174 | "com.apple.dock", 175 | kCFPreferencesCurrentUser, 176 | kCFPreferencesCurrentHost) 177 | logging.info("dock-maintainer: Setting secure preferences") 178 | 179 | def main(): 180 | ''' 181 | Main Stuff 182 | ''' 183 | keys = {} 184 | keys["ManagedUser"] = \ 185 | CFPreferencesCopyAppValue("ManagedUser", 186 | "com.github.wardsparadox.dock-maintainer") 187 | if keys["ManagedUser"] is None: 188 | logging.error("No ManagedUser Preference set!" 189 | "Please set that via defaults write" 190 | "com.github.wardsparadox.dock-maintainer ManagedUser nameofuser") 191 | exit(2) 192 | configPath = os.path.realpath( 193 | "/Library/Application Support/com.github.wardsparadox.dock-maintainer/") 194 | try: 195 | configPlist = plistlib.readPlist(os.path.join(configPath, keys["ManagedUser"])) 196 | logging.info("dock-maintainer: Input plist found. Matching docks.") 197 | except IOError: 198 | logging.error("dock-maintainer: No input found! Make sure the updater is functioning!") 199 | exit(3) 200 | username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0] 201 | username = [username, ""][username in [u"loginwindow", None, u""]] 202 | if username != keys["ManagedUser"]: 203 | logging.info("dock-maintainer: Exiting as user is not the right user") 204 | exit(0) 205 | else: 206 | persistent_apps = dock.items['persistent-apps'] 207 | persistent_others = dock.items['persistent-others'] 208 | dock_apps = [] 209 | dock_others = [] 210 | config_apps = [] 211 | config_others = [] 212 | for dockapp in persistent_apps: 213 | dock_apps.append(str(dockapp["tile-data"]["file-label"])) 214 | for configapp in configPlist['Apps']: 215 | config_apps.append(os.path.splitext(os.path.basename(configapp))[0]) 216 | for dockother in persistent_others: 217 | dock_others.append(str(dockother["tile-data"]["file-label"])) 218 | for configother in configPlist['Others']: 219 | config_others.append(os.path.splitext(os.path.basename(configother))[0]) 220 | 221 | final_apps = [] 222 | final_others = [] 223 | if len(list(set(dock_apps) ^ set(config_apps))) > 0: 224 | logging.info("dock-maintainer: App missing from dock, setting dock to config") 225 | for app in configPlist['Apps']: 226 | final_apps.append(dock.makeDockAppEntry(app)) 227 | dock.items['persistent-apps'] = final_apps 228 | else: 229 | logging.info("dock-maintainer: Dock Apps match Config Apps, nothing to change") 230 | if len(list(set(dock_others) ^ set(config_others))) > 0: 231 | logging.info("dock-maintainer: Other Item missing from dock, setting dock to config") 232 | for item in configPlist['Others']: 233 | final_others.append(dock.makeDockOtherEntry(os.path.expanduser(item), 0, 1, 3)) 234 | print item 235 | dock.items['persistent-others'] = final_others 236 | else: 237 | logging.info("dock-maintainer: Dock Other Items match Config Other Items, nothing to change") 238 | 239 | if len(final_apps) or len(final_others) > 0: 240 | dock.save() 241 | else: 242 | print "dock does not need to be reloaded" 243 | logging.info("dock-maintainer: Killing Dock to finalize") 244 | #setPreferences() 245 | 246 | if __name__ == '__main__': 247 | main() 248 | -------------------------------------------------------------------------------- /outset_dock-maintainer/.gitignore: -------------------------------------------------------------------------------- 1 | # .DS_Store files! 2 | .DS_Store 3 | 4 | # our build directory 5 | build/ 6 | -------------------------------------------------------------------------------- /outset_dock-maintainer/build-info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | distribution_style 6 | 7 | identifier 8 | com.github.wardsparadox.dock-maintainer 9 | install_location 10 | /usr/local/outset 11 | name 12 | dock-maintainer.pkg 13 | ownership 14 | recommended 15 | postinstall_action 16 | none 17 | suppress_bundle_relocation 18 | 19 | version 20 | 1.4.0 21 | 22 | 23 | -------------------------------------------------------------------------------- /outset_dock-maintainer/payload/boot-every/dock-maintainer-updater.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | Downloads file as needed for dock-maintainer. Sets extended attribute to be 4 | the last mod date for the plist according to the server. Suggested work around 5 | by elios on macadmins.org 6 | ''' 7 | import urllib2 8 | import datetime 9 | from time import mktime 10 | import os 11 | import logging 12 | import xattr 13 | from Foundation import CFPreferencesCopyAppValue 14 | 15 | logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s', 16 | datefmt='%Y-%m-%d %I:%M:%S %p', 17 | level=logging.INFO, 18 | filename=os.path.normpath("/Library/Logs/dock-maintainer.log")) 19 | stdout_logging = logging.StreamHandler() 20 | stdout_logging.setFormatter(logging.Formatter()) 21 | logging.getLogger().addHandler(stdout_logging) 22 | 23 | 24 | def downloadFile(url, filepath, attributedate): 25 | ''' 26 | Downloads url to filepath 27 | ''' 28 | with open(filepath, "wb") as code: 29 | code.write(url.read()) 30 | xattr.setxattr(filepath, 'dock-maintainer.Last-Modified-date', str(attributedate)) 31 | logging.info("Downloaded File to %s", filepath) 32 | return filepath 33 | 34 | def wait_for_internet_connection(): 35 | ''' 36 | Shameless swipe from stackoverflow. 37 | https://stackoverflow.com/a/10609378 38 | ''' 39 | while True: 40 | try: 41 | response = urllib2.urlopen('https://www.google.com/', timeout=1) 42 | return 43 | except urllib2.URLError: 44 | pass 45 | 46 | def main(): 47 | ''' 48 | Main Controlling Module: 49 | - Checks preferences set 50 | - Checks for Path, if not creates 51 | - Downloads plist if needed 52 | ''' 53 | keys = {} 54 | keys["ManagedUser"] = CFPreferencesCopyAppValue("ManagedUser", 55 | "com.github.wardsparadox.dock-maintainer") 56 | 57 | keys["ServerURL"] = CFPreferencesCopyAppValue("ServerURL", 58 | "com.github.wardsparadox.dock-maintainer") 59 | keys["FileName"] = CFPreferencesCopyAppValue("FileName", 60 | "com.github.wardsparadox.dock-maintainer") 61 | 62 | path = os.path.realpath("/Library/Application Support/com.github.wardsparadox.dock-maintainer") 63 | if os.path.exists(path): 64 | logging.info("Path exists at %s", path) 65 | else: 66 | logging.info("Path not found, creating at %s", path) 67 | os.mkdir(path, 0755) 68 | 69 | if keys["ManagedUser"] is None: 70 | logging.error("No ManagedUser Preference set") 71 | exit(2) 72 | else: 73 | plistfilepath = os.path.join(path, keys["ManagedUser"]) 74 | completeurl = os.path.join(keys["ServerURL"], keys["FileName"]) 75 | try: 76 | fileurl = urllib2.urlopen(completeurl) 77 | meta = fileurl.info().getheaders("Last-Modified")[0] 78 | servermod = datetime.datetime.fromtimestamp(mktime( 79 | datetime.datetime.strptime( 80 | meta, "%a, %d %b %Y %X GMT").timetuple())) 81 | except urllib2.HTTPError: 82 | logging.error("Can not connect to url") 83 | exit(1) 84 | 85 | if not os.path.isfile(plistfilepath): 86 | logging.info("File not found! Downloading") 87 | downloadFile(fileurl, plistfilepath, servermod) 88 | exit(0) 89 | 90 | if xattr.listxattr(plistfilepath): 91 | logging.info("Got File Attributes") 92 | else: 93 | logging.info("No Attributes found! Downloading and setting them") 94 | downloadFile(fileurl, plistfilepath, servermod) 95 | exit(0) 96 | filexattrdate = xattr.getxattr(plistfilepath, 97 | 'dock-maintainer.Last-Modified-date') 98 | if str(servermod) != filexattrdate: 99 | logging.info("File is out of date") 100 | downloadFile(fileurl, plistfilepath, servermod) 101 | else: 102 | logging.info("File is synced.") 103 | exit(0) 104 | 105 | if __name__ == '__main__': 106 | wait_for_internet_connection() 107 | main() 108 | -------------------------------------------------------------------------------- /outset_dock-maintainer/payload/login-every/dock-maintainer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | A wrapper for dockutil that manages a specified users dock with a server based plist for the list. 4 | ''' 5 | import subprocess 6 | import plistlib 7 | import os 8 | import logging 9 | from Foundation import kCFPreferencesCurrentHost, \ 10 | CFPreferencesCopyAppValue, \ 11 | CFPreferencesSetAppValue, \ 12 | CFPreferencesSetMultiple, \ 13 | kCFPreferencesCurrentUser, \ 14 | CFPreferencesAppSynchronize, \ 15 | NSURL 16 | 17 | from SystemConfiguration import SCDynamicStoreCopyConsoleUser 18 | 19 | logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s', 20 | datefmt='%Y-%m-%d %I:%M:%S %p', 21 | level=logging.INFO, 22 | filename=os.path.expanduser('~/Library/Logs/dock-maintainer.log')) 23 | stdout_logging = logging.StreamHandler() 24 | stdout_logging.setFormatter(logging.Formatter()) 25 | logging.getLogger().addHandler(stdout_logging) 26 | 27 | class DockError(Exception): 28 | '''Basic exception''' 29 | pass 30 | 31 | class Dock(): 32 | '''Class to handle Dock operations''' 33 | _DOMAIN = 'com.apple.dock' 34 | _DOCK_PLIST = os.path.expanduser( 35 | '~/Library/Preferences/com.apple.dock.plist') 36 | _DOCK_LAUNCHAGENT_ID = 'com.apple.Dock.agent' 37 | _DOCK_LAUNCHAGENT_FILE = '/System/Library/LaunchAgents/com.apple.Dock.plist' 38 | _SECTIONS = ['persistent-apps', 'persistent-others'] 39 | items = {} 40 | 41 | def __init__(self): 42 | for key in self._SECTIONS: 43 | try: 44 | section = CFPreferencesCopyAppValue(key, self._DOMAIN) 45 | self.items[key] = section.mutableCopy() 46 | except Exception: 47 | raise 48 | 49 | def save(self): 50 | '''saves our (modified) Dock preferences''' 51 | # unload Dock launchd job so we can make our changes unmolested 52 | subprocess.call( 53 | ['/bin/launchctl', 'unload', self._DOCK_LAUNCHAGENT_FILE]) 54 | 55 | for key in self._SECTIONS: 56 | try: 57 | CFPreferencesSetAppValue(key, self.items[key], self._DOMAIN) 58 | except Exception: 59 | raise DockError 60 | if not CFPreferencesAppSynchronize(self._DOMAIN): 61 | raise DockError 62 | 63 | # restart the Dock 64 | subprocess.call(['/bin/launchctl', 'load', self._DOCK_LAUNCHAGENT_FILE]) 65 | subprocess.call(['/bin/launchctl', 'start', self._DOCK_LAUNCHAGENT_ID]) 66 | 67 | def findExistingLabel(self, test_label, section='persistent-apps'): 68 | '''returns index of item with label matching test_label 69 | or -1 if not found''' 70 | for index in range(len(self.items[section])): 71 | if (self.items[section][index]['tile-data'].get('file-label') == 72 | test_label): 73 | return index 74 | return -1 75 | 76 | def removeDockEntry(self, label, section=None): 77 | '''Removes a Dock entry with matching label, if any''' 78 | if section: 79 | sections = [section] 80 | else: 81 | sections = self._SECTIONS 82 | for section in sections: 83 | found_index = self.findExistingLabel(label, section=section) 84 | if found_index > -1: 85 | del self.items[section][found_index] 86 | 87 | def replaceDockEntry(self, thePath, label=None, section='persistent-apps'): 88 | '''Replaces a Dock entry. If label is None, then a label is derived 89 | from the item path. The new entry replaces an entry with the given 90 | or derived label''' 91 | if section == 'persistent-apps': 92 | new_item = self.makeDockAppEntry(thePath) 93 | else: 94 | new_item = self.makeDockOtherEntry(thePath) 95 | if new_item: 96 | if not label: 97 | label = os.path.splitext(os.path.basename(thePath))[0] 98 | found_index = self.findExistingLabel(label, section=section) 99 | if found_index > -1: 100 | self.items[section][found_index] = new_item 101 | 102 | def makeDockAppEntry(self, thePath): 103 | '''returns a dictionary corresponding to a Dock application item''' 104 | label_name = os.path.splitext(os.path.basename(thePath))[0] 105 | ns_url = NSURL.fileURLWithPath_(thePath).absoluteString() 106 | return {'tile-data': {'file-data': {'_CFURLString': ns_url, 107 | '_CFURLStringType': 15}, 108 | 'file-label': label_name, 109 | 'file-type': 41}, 110 | 'tile-type': 'file-tile'} 111 | 112 | def makeDockOtherEntry(self, thePath, 113 | arrangement=0, displayas=1, showas=0): 114 | '''returns a dictionary corresponding to a Dock folder or file item''' 115 | # arrangement values: 116 | # 1: sort by name 117 | # 2: sort by date added 118 | # 3: sort by modification date 119 | # 4: sort by creation date 120 | # 5: sort by kind 121 | # 122 | # displayas values: 123 | # 0: display as stack 124 | # 1: display as folder 125 | # 126 | # showas values: 127 | # 0: auto 128 | # 1: fan 129 | # 2: grid 130 | # 3: list 131 | 132 | label_name = os.path.splitext(os.path.basename(thePath))[0] 133 | if arrangement == 0: 134 | if label_name == 'Downloads': 135 | # set to sort by date added 136 | arrangement = 2 137 | else: 138 | # set to sort by name 139 | arrangement = 1 140 | ns_url = NSURL.fileURLWithPath_(thePath).absoluteString() 141 | if os.path.isdir(thePath): 142 | return {'tile-data':{'arrangement': arrangement, 143 | 'displayas': displayas, 144 | 'file-data':{'_CFURLString': ns_url, 145 | '_CFURLStringType': 15}, 146 | 'file-label': label_name, 147 | 'dock-extra': False, 148 | 'showas': showas 149 | }, 150 | 'tile-type':'directory-tile'} 151 | else: 152 | return {'tile-data':{'file-data':{'_CFURLString': ns_url, 153 | '_CFURLStringType': 15}, 154 | 'file-label': label_name, 155 | 'dock-extra': False}, 156 | 'tile-type':'file-tile'} 157 | 158 | dock = Dock() 159 | 160 | def setPreferences(): 161 | ''' 162 | Sets dock preferences to keep dock as is. 163 | ''' 164 | preferences = {} 165 | preferences["contents-immutable"] = True 166 | preferences["size-immutable"] = True 167 | preferences["orientation"] = "bottom" 168 | preferences["position-immutable"] = True 169 | preferences["magnify-immutable"] = True 170 | preferences["autohide"] = False 171 | preferences["autohide-immutable"] = True 172 | preferences["tilesize"] = int(60) 173 | CFPreferencesSetMultiple(preferences, None, 174 | "com.apple.dock", 175 | kCFPreferencesCurrentUser, 176 | kCFPreferencesCurrentHost) 177 | logging.info("dock-maintainer: Setting secure preferences") 178 | 179 | def main(): 180 | ''' 181 | Main Stuff 182 | ''' 183 | keys = {} 184 | keys["ManagedUser"] = \ 185 | CFPreferencesCopyAppValue("ManagedUser", 186 | "com.github.wardsparadox.dock-maintainer") 187 | if keys["ManagedUser"] is None: 188 | logging.error("No ManagedUser Preference set!" 189 | "Please set that via defaults write" 190 | "com.github.wardsparadox.dock-maintainer ManagedUser nameofuser") 191 | exit(2) 192 | configPath = os.path.realpath( 193 | "/Library/Application Support/com.github.wardsparadox.dock-maintainer/") 194 | try: 195 | configPlist = plistlib.readPlist(os.path.join(configPath, keys["ManagedUser"])) 196 | logging.info("dock-maintainer: Input plist found. Matching docks.") 197 | except IOError: 198 | logging.error("dock-maintainer: No input found! Make sure the updater is functioning!") 199 | exit(3) 200 | username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0] 201 | username = [username, ""][username in [u"loginwindow", None, u""]] 202 | if username != keys["ManagedUser"]: 203 | logging.info("dock-maintainer: Exiting as user is not the right user") 204 | exit(0) 205 | else: 206 | persistent_apps = dock.items['persistent-apps'] 207 | persistent_others = dock.items['persistent-others'] 208 | dock_apps = [] 209 | dock_others = [] 210 | config_apps = [] 211 | config_others = [] 212 | for dockapp in persistent_apps: 213 | dock_apps.append(str(dockapp["tile-data"]["file-label"])) 214 | for configapp in configPlist['Apps']: 215 | config_apps.append(os.path.splitext(os.path.basename(configapp))[0]) 216 | for dockother in persistent_others: 217 | dock_others.append(str(dockother["tile-data"]["file-label"])) 218 | for configother in configPlist['Others']: 219 | config_others.append(os.path.splitext(os.path.basename(configother))[0]) 220 | 221 | final_apps = [] 222 | final_others = [] 223 | if len(list(set(dock_apps) ^ set(config_apps))) > 0: 224 | logging.info("dock-maintainer: App missing from dock, setting dock to config") 225 | for app in configPlist['Apps']: 226 | final_apps.append(dock.makeDockAppEntry(app)) 227 | dock.items['persistent-apps'] = final_apps 228 | else: 229 | logging.info("dock-maintainer: Dock Apps match Config Apps, nothing to change") 230 | if len(list(set(dock_others) ^ set(config_others))) > 0: 231 | logging.info("dock-maintainer: Other Item missing from dock, setting dock to config") 232 | for item in configPlist['Others']: 233 | final_others.append(dock.makeDockOtherEntry(os.path.expanduser(item), 0, 1, 3)) 234 | print item 235 | dock.items['persistent-others'] = final_others 236 | else: 237 | logging.info("dock-maintainer: Dock Other Items match Config Other Items, nothing to change") 238 | 239 | if len(final_apps) or len(final_others) > 0: 240 | dock.save() 241 | else: 242 | print "dock does not need to be reloaded" 243 | logging.info("dock-maintainer: Killing Dock to finalize") 244 | #setPreferences() 245 | 246 | if __name__ == '__main__': 247 | main() 248 | --------------------------------------------------------------------------------