├── .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 |
--------------------------------------------------------------------------------