blob: 8af941eb11681ed654d1fc1cd8ce4265483affe9 [file] [log] [blame]
cmticeb70801a2014-12-11 14:29:34 -08001# Copyright 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Wrapper for running gdb.
6
7This handles the fun details like running against the right sysroot, via
8qemu, bind mounts, etc...
9"""
10
11from __future__ import print_function
12
13import argparse
14import contextlib
15import errno
16import os
17import sys
18import tempfile
19
20from chromite.lib import commandline
21from chromite.lib import cros_build_lib
22from chromite.lib import namespaces
23from chromite.lib import osutils
24from chromite.lib import retry_util
25
26GDB = '/usr/bin/gdb'
27
28class BoardSpecificGdb(object):
29 """Framework for running gdb."""
30
31 _BIND_MOUNT_PATHS = ('dev', 'dev/pts', 'proc', 'mnt/host/source', 'sys')
32
33 def __init__(self, board, argv):
34 self.board = board
35 self.sysroot = cros_build_lib.GetSysroot(board=self.board)
36 self.prompt = '(%s-gdb) ' % self.board
37 self.host = False
38 self.run_as_root = False # May add an option to change this later.
39 self.args = argv
40
41 if not os.path.isdir(self.sysroot):
42 raise AssertionError('Sysroot does not exist: %s' % self.sysroot)
43
44 def removeSysrootPrefix(self, path):
45 """Returns the given path with any sysroot prefix removed."""
46 # If the sysroot is /, then the paths are already normalized.
47 if self.sysroot != '/' and path.startswith(self.sysroot):
48 path = path.replace(self.sysroot, '', 1)
49
50 return path
51
52 @staticmethod
53 def GetNonRootAccount():
54 """Return details about the non-root account we want to use.
55
56 Returns:
57 A tuple of (username, uid, gid, home).
58 """
59 return (
60 os.environ.get('SUDO_USER', 'nobody'),
61 int(os.environ.get('SUDO_UID', '65534')),
62 int(os.environ.get('SUDO_GID', '65534')),
63 # Should we find a better home?
64 '/tmp/portage',
65 )
66
67 @staticmethod
68 @contextlib.contextmanager
69 def LockDb(db):
70 """Lock an account database.
71
72 We use the same algorithm as shadow/user.eclass. This way we don't race
73 and corrupt things in parallel.
74 """
75 lock = '%s.lock' % db
76 _, tmplock = tempfile.mkstemp(prefix='%s.platform.' % lock)
77
78 # First try forever to grab the lock.
79 retry = lambda e: e.errno == errno.EEXIST
80 # Retry quickly at first, but slow down over time.
81 try:
82 retry_util.GenericRetry(retry, 60, os.link, tmplock, lock, sleep=0.1)
83 except Exception:
84 print('error: could not grab lock %s' % lock)
85 raise
86
87 # Yield while holding the lock, but try to clean it no matter what.
88 try:
89 os.unlink(tmplock)
90 yield lock
91 finally:
92 os.unlink(lock)
93
94 def SetupUser(self):
95 """Propogate the user name<->id mapping from outside the chroot.
96
97 Some unittests use getpwnam($USER), as does bash. If the account
98 is not registered in the sysroot, they get back errors.
99 """
100 MAGIC_GECOS = 'Added by your friendly platform test helper; do not modify'
101 # This is kept in sync with what sdk_lib/make_chroot.sh generates.
102 SDK_GECOS = 'ChromeOS Developer'
103
104 user, uid, gid, home = self.GetNonRootAccount()
105 if user == 'nobody':
106 return
107
108 passwd_db = os.path.join(self.sysroot, 'etc', 'passwd')
109 with self.LockDb(passwd_db):
110 data = osutils.ReadFile(passwd_db)
111 accts = data.splitlines()
112 for acct in accts:
113 passwd = acct.split(':')
114 if passwd[0] == user:
115 # Did the sdk make this account?
116 if passwd[4] == SDK_GECOS:
117 # Don't modify it (see below) since we didn't create it.
118 return
119
120 # Did we make this account?
121 if passwd[4] != MAGIC_GECOS:
122 raise RuntimeError('your passwd db (%s) has unmanaged acct %s' %
123 (passwd_db, user))
124
125 # Maybe we should see if it needs to be updated? Like if they
126 # changed UIDs? But we don't really check that elsewhere ...
127 return
128
129 acct = '%(name)s:x:%(uid)s:%(gid)s:%(gecos)s:%(homedir)s:%(shell)s' % {
130 'name': user,
131 'uid': uid,
132 'gid': gid,
133 'gecos': MAGIC_GECOS,
134 'homedir': home,
135 'shell': '/bin/bash',
136 }
137 with open(passwd_db, 'a') as f:
138 if data[-1] != '\n':
139 f.write('\n')
140 f.write('%s\n' % acct)
141
142 def run(self):
143 """Runs the debugger in a proper environment (e.g. qemu)."""
144 self.SetupUser()
145
146 for mount in self._BIND_MOUNT_PATHS:
147 path = os.path.join(self.sysroot, mount)
148 osutils.SafeMakedirs(path)
149 osutils.Mount('/' + mount, path, 'none', osutils.MS_BIND)
150
151 cmd = GDB
152 argv = self.args[:]
153 if argv:
154 argv[0] = self.removeSysrootPrefix(argv[0])
155
156 # Some programs expect to find data files via $CWD, so doing a chroot
157 # and dropping them into / would make them fail.
158 cwd = self.removeSysrootPrefix(os.getcwd())
159
160 print('chroot: %s' % self.sysroot)
161 print('cwd: %s' % cwd)
162 if argv:
163 print('cmd: {%s} %s' % (cmd, ' '.join(map(repr, argv))))
164 os.chroot(self.sysroot)
165 os.chdir(cwd)
166 # The TERM the user is leveraging might not exist in the sysroot.
167 # Force a sane default that supports standard color sequences.
168 os.environ['TERM'] = 'ansi'
169 # Some progs want this like bash else they get super confused.
170 os.environ['PWD'] = cwd
171 if not self.run_as_root:
172 _, uid, gid, home = self.GetNonRootAccount()
173 os.setgid(gid)
174 os.setuid(uid)
175 os.environ['HOME'] = home
176
177 gdb_commands = (
178 'set sysroot /',
179 'set solib-absolute-prefix /',
180 'set solib-search-path /',
181 'set debug-file-directory /usr/lib/debug',
182 'set prompt %s' % self.prompt
183 )
184
185 args = [cmd] + ['--eval-command=%s' % x for x in gdb_commands] + self.args
186 sys.exit(os.execvp(cmd, args))
187
188
189def _ReExecuteIfNeeded(argv):
190 """Re-execute gdb as root.
191
192 We often need to do things as root, so make sure we're that. Like chroot
193 for proper library environment or do bind mounts.
194
195 Also unshare the mount namespace so as to ensure that doing bind mounts for
196 tests don't leak out to the normal chroot. Also unshare the UTS namespace
197 so changes to `hostname` do not impact the host.
198 """
199 if os.geteuid() != 0:
200 cmd = ['sudo', '-E', '--'] + argv
201 os.execvp(cmd[0], cmd)
202 else:
203 namespaces.SimpleUnshare(net=True, pid=True)
204
205
206def main(argv):
207
208 parser = commandline.ArgumentParser(description=__doc__)
209
210 parser.add_argument('--board', required=True,
211 help='board to debug for')
212 parser.add_argument('gdb_args', nargs=argparse.REMAINDER)
213
214 options = parser.parse_args(argv)
215 options.Freeze()
216
217 # Once we've finished sanity checking args, make sure we're root.
218 _ReExecuteIfNeeded([sys.argv[0]] + argv)
219
220 gdb = BoardSpecificGdb(options.board, options.gdb_args)
221
222 gdb.run()