Skip to content

Commit

Permalink
Add umockdump tool
Browse files Browse the repository at this point in the history
This tool dumps Linux devices and their ancestors from sysfs/udev.

All attributes and properties are included, non-ASCII ones get printed in hex.
The dump is written to the standard output.

This format will be readable/loadable by UMockdevTestbed.
  • Loading branch information
Martin Pitt committed Jul 24, 2012
1 parent d2f10a4 commit 6d1b93b
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 2 deletions.
20 changes: 19 additions & 1 deletion Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ EXTRA_DIST = COPYING
DISTCHECK_CONFIGURE_FLAGS = --enable-gtk-doc
TESTS_ENVIRONMENT = LD_LIBRARY_PATH=.libs:$$LD_LIBRARY_PATH $(top_srcdir)/src/umockdev-wrapper

# use this for running Python tests
PYTEST = env LD_LIBRARY_PATH=$(top_builddir)/.libs GI_TYPELIB_PATH=$(top_builddir)/src/ $(top_srcdir)/src/umockdev-wrapper $(PYTHON) -Wd -Werror::PendingDeprecationWarning -Werror::DeprecationWarning

AM_CFLAGS = -Werror -Wall

# -------------------------------------------------------------
Expand Down Expand Up @@ -143,6 +146,20 @@ CLEANFILES += $(gir_DATA) $(typelib_DATA)

endif

# -------------------------------------------------------------
# tools
if HAVE_PYTHON

bin_SCRIPTS += src/umockdump
EXTRA_DIST += src/umockdump

# adjust shebang line to detected PYTHON
umockdump-install-hook:
sed -i '1 s_[^/]\+$$_$(PYTHON)_' $(DESTDIR)$(bindir)/umockdump

INSTALL_EXEC_HOOKS += umockdump-install-hook
endif

# -------------------------------------------------------------
# tests

Expand Down Expand Up @@ -170,7 +187,8 @@ if HAVE_INTROSPECTION
if HAVE_PYTHON
check-local: $(INTROSPECTION_GIRS)
@echo " Running gobject-introspection test with $(PYTHON)"
LD_LIBRARY_PATH=$(top_builddir)/.libs GI_TYPELIB_PATH=$(top_builddir)/src/ $(top_srcdir)/src/umockdev-wrapper $(PYTHON) -Wd -Werror::PendingDeprecationWarning -Werror::DeprecationWarning $(srcdir)/src/test-umockdev.py
$(PYTEST) $(srcdir)/src/test-umockdev.py
$(PYTEST) $(srcdir)/src/test-umockdump
endif
endif

Expand Down
9 changes: 8 additions & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,12 @@ AC_MSG_RESULT([
CPPFLAGS: ${CPPFLAGS}
CFLAGS: ${CFLAGS}
LDFLAGS: ${LDFLAGS}
])

if test x$PYTHON != x; then
AC_MSG_RESULT([ Python: ${PYTHON}
])
else
AC_MSG_RESULT([ WARNING! No Python interpreter found. Some tools and tests are not available.
])
fi
127 changes: 127 additions & 0 deletions src/test-umockdump
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/python3

'''umockdump tests'''

__copyright__ = 'Copyright (C) 2012 Canonical Ltd.'
__author__ = 'Martin Pitt <martin.pitt@ubuntu.com>'

# umockdev is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# umockdev is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.

import os.path
import sys
import subprocess
import unittest

from gi.repository import UMockdev

umockdump_path = os.path.join(os.path.dirname(__file__), 'umockdump')

def call(args):
'''Call umockdump with given arguments.
Return (code, stdout, stderr) tuple.
'''
umockdump = subprocess.Popen([umockdump_path] + args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
(out, err) = umockdump.communicate()
out = out.strip()
err = err.strip()
return (umockdump.returncode, out, err)

class Testbed(unittest.TestCase):
def setUp(self):
self.testbed = UMockdev.Testbed()

def test_all_empty(self):
'''--all on empty testbed'''

(code, out, err) = call(['--all'])
self.assertEqual(err, '')
self.assertEqual(out, '')
self.assertEqual(code, 0)

def test_one(self):
'''one device'''

syspath = self.testbed.add_device(
'pci', 'dev1', None,
['simple_attr', '1', 'multiline_attr', 'a\\b\nc\\d\nlast'],
['SIMPLE_PROP', '1'])
self.testbed.set_attribute_binary(syspath, 'binary_attr', b'\x41\xFF\x00\x05\xFF\x00')

(code, out, err) = call([syspath])
self.assertEqual(err, '')
self.assertEqual(code, 0)
self.assertEqual(out, '''P: /devices/dev1
E: SIMPLE_PROP=1
E: SUBSYSTEM=pci
H: binary_attr=41FF0005FF00
A: multiline_attr=a\\\\b\\nc\\\\d\\nlast
A: simple_attr=1''')

def test_multiple(self):
'''multiple devices'''

dev1 = self.testbed.add_device(
'pci', 'dev1', None, ['dev1color', 'green'], ['DEV1COLOR', 'GREEN'])
subdev1 = self.testbed.add_device(
'pci', 'subdev1', dev1, ['subdev1color', 'yellow'],
['SUBDEV1COLOR', 'YELLOW'])
dev2 = self.testbed.add_device(
'usb', 'dev2', None, ['dev2color', 'brown'], ['DEV2COLOR', 'BROWN'])

# should grab device and all parents
(code, out, err) = call([subdev1])
self.assertEqual(err, '')
self.assertEqual(code, 0)
self.assertEqual(out, '''P: /devices/dev1/subdev1
E: SUBDEV1COLOR=YELLOW
E: SUBSYSTEM=pci
A: subdev1color=yellow
P: /devices/dev1
E: DEV1COLOR=GREEN
E: SUBSYSTEM=pci
A: dev1color=green''')

(code, out, err) = call([dev1])
self.assertEqual(err, '')
self.assertEqual(code, 0)
self.assertEqual(out, '''P: /devices/dev1
E: DEV1COLOR=GREEN
E: SUBSYSTEM=pci
A: dev1color=green''')

# with --all it should have all three
(code, out, err) = call(['--all'])
self.assertEqual(err, '')
self.assertEqual(code, 0)
self.assertTrue('P: /devices/dev1/subdev1\n' in out, out)
self.assertTrue('P: /devices/dev1\n' in out, out)
self.assertTrue('P: /devices/dev2\n' in out, out)


class System(unittest.TestCase):
def test_all(self):
'''umockdump --all has no errors and some output on system /sys'''

(code, out, err) = call(['--all'])
self.assertEqual(err, '')
self.assertEqual(code, 0)
self.assertTrue(out.startswith('P:'), out[:100] + '[..]')
self.assertGreater(len(out), 100, out[:100] + '[..]')

unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2))
182 changes: 182 additions & 0 deletions src/umockdump
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/usr/bin/python3

'''Dump Linux devices and their ancestors from sysfs/udev.
All attributes and properties are included, non-ASCII ones get printed in hex.
The dump is written to the standard output.
'''

__copyright__ = "Copyright (C) 2012 Canonical Ltd."
__author__ = "Martin Pitt <martin.pitt@ubuntu.com>"

# umockdev is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# umockdev is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.

import argparse
import subprocess
import os
import stat
import sys
import errno
from gettext import gettext as _

_py2 = sys.version_info.major < 3

def fatal(msg):
sys.stderr.write(msg)
sys.stderr.write('\n')
sys.exit(1)

def all_devices():
devs = []
for base, dirs, files in os.walk('/sys/devices'):
if 'uevent' in files:
devs.append(base)
return devs

def parse_args():
ap = argparse.ArgumentParser(
description=_('Dump Linux devices and their ancestors from sysfs/udev.'))
ap.add_argument('devices', metavar='DEVICE', nargs="*",
help=_('Path of a device in /dev or /sys.'))
ap.add_argument('--all', '-a', action='store_true',
help=_('Dump all devices'))

args = ap.parse_args()

if args.all and args.devices:
ap.error(_('Specifying a device list together with --all is invalid.'))
if not args.all and not args.devices:
ap.error(_('Need to specify at least one device or --all.'))

if args.all:
args.devices = all_devices()

return args

def resolve(dev):
'''If dev is a block or character device, convert it to a sysfs path.'''
try:
st = os.stat(dev)
except OSError as e:
fatal(str(e))

# character device?
if stat.S_ISCHR(st.st_mode):
link = '/sys/dev/char/%i:%i' % (os.major(st.st_rdev), os.minor(st.st_rdev))
elif stat.S_ISBLK(st.st_mode):
link = '/sys/dev/block/%i:%i' % (os.major(st.st_rdev), os.minor(st.st_rdev))
else:
link = dev

if not os.path.exists(link):
fatal('Cannot resolve device %s to a sysfs path, %s does not exist' % (dev, link))

dev = os.path.realpath(link)
if not os.path.exists(os.path.join(dev, 'uevent')):
fatal('Invalid device %s, has no uevent attribute' % dev)

return dev

def format_attr(value):
# first, try text
try:
text = value.decode('ASCII')
# escape line breaks and backslashes
if text.endswith('\n'):
text = text[:-1]
text = text.replace('\\', '\\\\')
text = text.replace('\n', '\\n')
return ('A', text)
except UnicodeDecodeError:
pass

# something binary, encode as hex
text = ''
if _py2:
# Python 2 does not consider elements in byte strings as numbers
for byte in value:
text += '%02X' % ord(byte)
else:
for byte in value:
text += '%02X' % byte
return ('H', text)

def dump_device(dev):
'''Dump a single device'''

prop_blacklist = ['DEVPATH', 'UDEV_LOG', 'USEC_INITIALIZED']
attr_blacklist = ['subsystem', 'firmware_node', 'driver', 'uevent']

# we start with udevadm dump of this device, which will include all udev
# properties
udevadm = subprocess.Popen(['udevadm', 'info', '--query=all', '--path', dev],
stdout=subprocess.PIPE, universal_newlines=True)
out = udevadm.communicate()[0]
# filter out redundant/uninteresting properties
for line in out.splitlines():
if not line:
continue
for bl in prop_blacklist:
if line.startswith('E: %s=' % bl):
break
else:
sys.stdout.write(line)
sys.stdout.write('\n')

# now append all attributes
for attr_name in sorted(os.listdir(dev)):
if attr_name in attr_blacklist:
continue
attr_path = os.path.join(dev, attr_name)
# only look at files or symlinks
if not os.path.isfile(attr_path):
continue
if os.path.islink(attr_path):
sys.stdout.write('L: %s=%s\n' % (attr_name, os.readlink(attr_path)))
else:
try:
with open(attr_path, 'rb') as f:
(cls, value) = format_attr(f.read())
except IOError as e:
# some attributes are EACCES, or "no such device", etc.
continue
sys.stdout.write('%s: %s=%s\n' % (cls, attr_name, value))

sys.stdout.write('\n')

def parent(dev):
'''Get a device's parent'''

dev = os.path.dirname(dev)
if not dev.startswith('/sys'):
return None

if os.path.exists(os.path.join(dev, 'uevent')):
return dev

# we might have intermediate directories without uevent, so try the next
# higher one
return parent(dev)

#
# main
#

args = parse_args()

for device in args.devices:
while device:
device = resolve(device)
dump_device(device)
device = parent(device)

0 comments on commit 6d1b93b

Please sign in to comment.