Skip to content

Commit 1678c25

Browse files
committed
cleanups and doc
1 parent d047a05 commit 1678c25

8 files changed

Lines changed: 108 additions & 94 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
dist
21
*.egg-info
32
__pycache__
3+
.coverage
4+
dist
5+
htmlcov

CHANGELOG.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Release history
22
---------------
33

4-
0.1 (October 2013)
5-
++++++++++++++++++
4+
0.1 (November 2013)
5+
+++++++++++++++++++
66
- Focus: initial public release

README.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Detect if a job exists:
5959
>>> launchd.LaunchdJob("com.example.fubar").exists()
6060
False
6161
62-
launchd properties:
62+
launchd job properties (these come directly from launchd and NOT the .plist files):
6363

6464
.. code-block:: python
6565
@@ -79,6 +79,15 @@ Find all plist filenames of currently running jobs:
7979
continue
8080
print(job.plistfilename)
8181
82+
Job properties of a given job (this uses the actual .plist file):
83+
84+
.. code-block:: python
85+
>>> launchd.plist.read("com.apple.kextd")
86+
{'ProgramArguments': ['/usr/libexec/kextd'], 'KeepAlive': {'SuccessfulExit': False},
87+
'POSIXSpawnType': 'Interactive', 'MachServices': {'com.apple.KernelExtensionServer':
88+
{'HostSpecialPort': 15}}, 'Label': 'com.apple.kextd'}
89+
90+
8291
8392
Installation
8493
============

example.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,48 @@
88

99
import launchd
1010

11-
myplist = dict(
12-
Disabled=False,
13-
Label="testlaunchdwrapper_python",
14-
Nice=-15,
15-
OnDemand=True,
16-
ProgramArguments=["/bin/bash", "-c", "sleep 1 && echo 'Hello World' && exit 0"],
17-
RunAtLoad=True,
18-
ServiceDescription="runs a sample command",
19-
ServiceIPC=False,
20-
)
2111

12+
def install(label, plist):
13+
'''
14+
Utility function to store a new .plist file and load it
2215
23-
def install():
24-
fname = launchd.plist.write(myplist, myplist['Label'], launchd.plist.USER)
16+
:param label: job label
17+
:param plist: a property list dictionary
18+
'''
19+
fname = launchd.plist.write(label, plist)
2520
launchd.load(fname)
2621

2722

28-
def uninstall():
29-
fname = launchd.plist.discover_filename(myplist['Label'])
30-
job = launchd.LaunchdJob(myplist['Label'])
31-
try:
32-
job.refresh()
33-
except ValueError:
34-
pass
35-
else:
23+
def uninstall(label):
24+
'''
25+
Utility function to remove a .plist file and unload it
26+
27+
:param label: job label
28+
'''
29+
if launchd.LaunchdJob(label).exists():
30+
fname = launchd.plist.discover_filename(label)
3631
launchd.unload(fname)
3732
os.unlink(fname)
3833

3934

4035
def main():
36+
myplist = dict(
37+
Disabled=False,
38+
Label="testlaunchdwrapper_python",
39+
Nice=-15,
40+
OnDemand=True,
41+
ProgramArguments=["/bin/bash", "-c", "sleep 1 && echo 'Hello World' && exit 0"],
42+
RunAtLoad=True,
43+
ServiceDescription="runs a sample command",
44+
ServiceIPC=False,
45+
)
46+
4147
import time
4248
label = myplist['Label']
4349
job = launchd.LaunchdJob(label)
4450
if not job.exists():
4551
print("'%s' is not loaded in launchd. Installing..." % (label))
46-
install()
47-
job.refresh()
52+
install(label, myplist)
4853
while job.pid is not None:
4954
print("Alive! PID = %s" % job.pid)
5055
job.refresh()
@@ -60,7 +65,7 @@ def main():
6065
time.sleep(0.2)
6166

6267
print("Uninstalling again...")
63-
uninstall()
68+
uninstall(label)
6469
return 0
6570

6671

launchd/cmd.py

Lines changed: 11 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,27 @@
11
# -*- coding: utf-8 -*-
22

33
import subprocess
4-
import plistlib
5-
import six
6-
7-
_LAUNCHCTL_CMD = "launchctl"
8-
9-
10-
def jobs_cmd():
11-
'''
12-
Wrapper for `launchctl list`
13-
14-
Returns a generator for LaunchdJob
15-
'''
16-
if six.PY2:
17-
stdout = launchctl("list")
18-
else:
19-
stdout = launchctl("list").decode("utf-8")
20-
# PID, Status, Label
21-
lines = iter(stdout.splitlines())
22-
# skip first line
23-
next(lines)
24-
sep = "\t"
25-
Ox = "0x"
26-
for line in lines:
27-
pid, _, last = line.strip().partition(sep)
28-
status, _, label = last.strip().partition(sep)
29-
if label.startswith(Ox):
30-
continue
31-
if pid.isdigit():
32-
pid = int(pid)
33-
else:
34-
pid = None
35-
if status.isdigit():
36-
status = int(status)
37-
else:
38-
status = None
39-
yield LaunchdJob(label, pid, status)
404

41-
42-
def job_properties_cmd(joblabel):
43-
'''
44-
Wrapper for `launchctl -x LABEL`
45-
46-
Returns dictionary
47-
:param job: string label or LaunchdJob
48-
'''
49-
if six.PY2:
50-
return dict(plistlib.readPlistFromString(launchctl("list", "-x", joblabel.encode("utf-8"))))
51-
else:
52-
return dict(plistlib.readPlistFromBytes(launchctl("list", "-x", joblabel)))
5+
import six
536

547

558
def launchctl(subcommand, *args):
569
'''
5710
A minimal wrapper to call the launchctl binary and capture the output
5811
:param subcommand: string
5912
'''
13+
if not isinstance(subcommand, six.string_types):
14+
raise ValueError("Argument is invalid: %r" % repr(subcommand))
6015
if isinstance(subcommand, six.text_type):
6116
subcommand = subcommand.encode('utf-8')
62-
else:
63-
raise ValueError("Argument is invalid: %r" % repr(subcommand))
64-
cmd = [_LAUNCHCTL_CMD, subcommand]
17+
18+
cmd = ["launchctl", subcommand]
6519
for arg in args:
66-
if isinstance(arg, six.text_type):
67-
cmd.append(arg.encode('utf-8'))
20+
if isinstance(arg, six.string_types):
21+
if isinstance(arg, six.text_type):
22+
cmd.append(arg.encode('utf-8'))
23+
else:
24+
cmd.append(arg)
6825
else:
6926
raise ValueError("Argument is invalid: %r" % repr(arg))
70-
output = subprocess.check_output(cmd, stdin=None, stderr=subprocess.STDOUT, shell=False)
71-
return output
27+
return subprocess.check_output(cmd, stdin=None, stderr=subprocess.STDOUT, shell=False)

launchd/launchctl.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ def jobs():
109109
except KeyError:
110110
pid = None
111111
try:
112-
status = int(entry['LastExitStatus'])
112+
laststatus = int(entry['LastExitStatus'])
113113
except KeyError:
114-
status = None
115-
job = LaunchdJob(label, pid, status)
114+
laststatus = None
115+
job = LaunchdJob(label, pid, laststatus)
116116
job._nsproperties = entry
117117
yield job
118118

@@ -128,10 +128,8 @@ def stop(*args):
128128

129129

130130
def load(*args):
131-
raise NotImplementedError()
132131
return launchctl("load", *args)
133132

134133

135134
def unload(*args):
136-
raise NotImplementedError()
137135
return launchctl("unload", *args)

launchd/plist.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import os
44
import plistlib
55

6-
76
USER = 1
87
USER_ADMIN = 2
98
DAEMON_ADMIN = 3
@@ -23,11 +22,18 @@ def compute_filename(label, scope):
2322
return os.path.expanduser(os.path.join(PLIST_LOCATIONS[scope], label + ".plist"))
2423

2524

26-
def discover_filename(label, scope=None):
27-
if scope is not None:
28-
scopes = [scope]
29-
else:
25+
def discover_filename(label, scopes=None):
26+
'''
27+
Check the filesystem for the existence of a .plist file matching the job label.
28+
Optionally specify one or more scopes to search (default all).
29+
30+
:param label: string
31+
:param scope: tuple or list or oneOf(USER, USER_ADMIN, DAEMON_ADMIN, USER_OS, DAEMON_OS)
32+
'''
33+
if scopes is None:
3034
scopes = [k for k in PLIST_LOCATIONS]
35+
elif not isinstance(scopes, (list, tuple)):
36+
scopes = (scopes, )
3137
for thisscope in scopes:
3238
plistfilename = compute_filename(label, thisscope)
3339
if os.path.isfile(plistfilename):
@@ -36,11 +42,11 @@ def discover_filename(label, scope=None):
3642

3743

3844
def read(label, scope=None):
39-
with open(discover_filename(label, scope)) as f:
45+
with open(discover_filename(label, (scope,)), 'rb') as f:
4046
return plistlib.readPlist(f)
4147

4248

43-
def write(plist, label, scope):
49+
def write(label, plist, scope=USER):
4450
'''
4551
Writes the given property list to the appropriate file on disk and returns
4652
the absolute filename.

launchd/tests/plist.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,41 @@ def test_compute_filename(self):
4141
def test_discover_filename(self):
4242
fname = plist.discover_filename("com.apple.kextd", plist.DAEMON_OS)
4343
self.assertTrue(os.path.isfile(fname))
44+
45+
# no scope specified
46+
fname2 = plist.discover_filename("com.apple.kextd")
47+
self.assertTrue(os.path.isfile(fname2))
48+
49+
fname = plist.discover_filename("com.apple.kextd", plist.USER)
50+
self.assertEqual(None, fname)
51+
52+
53+
class PlistToolPersistencyTest(unittest.TestCase):
54+
def setUp(self):
55+
self.sample_label = "com.example.unittest"
56+
self.sample_props = dict(Label="testlaunchdwrapper_python")
57+
fname = plist.discover_filename(self.sample_label, plist.USER)
58+
if fname is not None:
59+
os.unlink(fname)
60+
unittest.TestCase.setUp(self)
61+
62+
def tearDown(self):
63+
fname = plist.discover_filename(self.sample_label, plist.USER)
64+
if fname is not None:
65+
os.unlink(fname)
66+
unittest.TestCase.tearDown(self)
67+
68+
@unittest.skipUnless(sys.platform.startswith("darwin"), "requires OS X")
69+
def test_read_write(self):
70+
sample_label = "com.example.unittest"
71+
sample_props = dict(Label="testlaunchdwrapper_python")
72+
73+
fname = plist.discover_filename(sample_label, plist.USER)
74+
self.assertEqual(None, fname)
75+
plist.write(sample_label, sample_props, plist.USER)
76+
77+
fname = plist.discover_filename(sample_label, plist.USER)
78+
self.assertTrue(os.path.isfile(fname))
79+
props = plist.read(sample_label, plist.USER)
80+
self.assertEqual(sample_props, props)
81+
os.unlink(fname)

0 commit comments

Comments
 (0)