utils.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311
  1. #
  2. # utils module - common functions for reportbug UIs
  3. # Written by Chris Lawrence <lawrencc@debian.org>
  4. # Copyright (C) 1999-2008 Chris Lawrence
  5. # Copyright (C) 2008-2016 Sandro Tosi <morph@debian.org>
  6. #
  7. # This program is freely distributable per the following license:
  8. #
  9. # Permission to use, copy, modify, and distribute this software and its
  10. # documentation for any purpose and without fee is hereby granted,
  11. # provided that the above copyright notice appears in all copies and that
  12. # both that copyright notice and this permission notice appear in
  13. # supporting documentation.
  14. #
  15. # I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
  16. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I
  17. # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  18. # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  19. # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  20. # ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  21. # SOFTWARE.
  22. import sys
  23. import os
  24. import re
  25. import platform
  26. try:
  27. import pwd
  28. from tempfiles import TempFile, tempfile_prefix, cleanup_temp_file
  29. except ImportError, e:
  30. if platform.system() == 'Windows':
  31. pass
  32. else:
  33. print e
  34. sys.exit(1)
  35. import commands
  36. import shlex
  37. import rfc822
  38. import socket
  39. import subprocess
  40. from urlutils import open_url
  41. from string import ascii_letters, digits
  42. # Paths for dpkg
  43. DPKGLIB = '/var/lib/dpkg'
  44. AVAILDB = os.path.join(DPKGLIB, 'available')
  45. STATUSDB = os.path.join(DPKGLIB, 'status')
  46. # Headers other than these become email headers for debbugs servers
  47. PSEUDOHEADERS = ('Package', 'Source', 'Version', 'Severity', 'File', 'Tags',
  48. 'Justification', 'Followup-For', 'Owner', 'User', 'Usertags',
  49. 'Forwarded', 'Control')
  50. # These pseudo-headers can be repeated in the report
  51. REPEATABLE_PSEUDOHEADERS = ['Control',]
  52. MODES = {'novice': 'Offer simple prompts, bypassing technical questions.',
  53. 'standard': 'Offer more extensive prompts, including asking about '
  54. 'things that a moderately sophisticated user would be expected to '
  55. 'know about Debian.',
  56. 'advanced': 'Like standard, but assumes you know a bit more about '
  57. 'Debian, including "incoming".',
  58. 'expert': 'Bypass most handholding measures and preliminary triage '
  59. 'routines. This mode should not be used by people unfamiliar with '
  60. 'Debian\'s policies and operating procedures.'}
  61. MODELIST = ['novice', 'standard', 'advanced', 'expert']
  62. for mode in MODELIST:
  63. exec 'MODE_%s=%d' % (mode.upper(), MODELIST.index(mode))
  64. del mode
  65. # moved here since it needs the MODE_* vars to be defined
  66. import debbugs
  67. # it needs to be imported after debbugs
  68. import ui.text_ui as ui
  69. from reportbug.ui import AVAILABLE_UIS
  70. NEWBIELINE = """Dear Maintainer,
  71. *** Reporter, please consider answering these questions, where appropriate ***
  72. * What led up to the situation?
  73. * What exactly did you do (or not do) that was effective (or
  74. ineffective)?
  75. * What was the outcome of this action?
  76. * What outcome did you expect instead?
  77. *** End of the template - remove these template lines ***"""
  78. fhs_directories = ['/', '/usr', '/usr/share', '/var', '/usr/X11R6',
  79. '/usr/man', '/usr/doc', '/usr/bin']
  80. # A map between codenames and suites
  81. CODENAME2SUITE = {'squeeze': 'oldoldstable',
  82. 'wheezy': 'oldstable',
  83. 'jessie': 'stable',
  84. 'stretch': 'testing',
  85. 'buster': 'next-testing',
  86. 'sid': 'unstable',
  87. 'experimental': 'experimental'}
  88. SUITE2CODENAME = dict([(suite, codename) for codename, suite in CODENAME2SUITE.items()])
  89. def realpath(filename):
  90. filename = os.path.abspath(filename)
  91. bits = filename.split('/')
  92. for i in range(2, len(bits) + 1):
  93. component = '/'.join(bits[0:i])
  94. if component in fhs_directories:
  95. continue
  96. if os.path.islink(component):
  97. resolved = os.readlink(component)
  98. (dir, file) = os.path.split(component)
  99. resolved = os.path.normpath(os.path.join(dir, resolved))
  100. newpath = apply(os.path.join, [resolved] + bits[i:])
  101. return realpath(newpath)
  102. return filename
  103. pathdirs = ['/usr/sbin', '/usr/bin', '/sbin', '/bin', '/usr/X11R6/bin',
  104. '/usr/games']
  105. def search_path_for(filename):
  106. d, f = os.path.split(filename)
  107. if d:
  108. return realpath(filename)
  109. path = os.environ.get("PATH", os.defpath).split('/')
  110. for d in pathdirs:
  111. if d not in path:
  112. path.append(d)
  113. for d in path:
  114. fullname = os.path.join(d, f)
  115. if os.path.exists(fullname):
  116. return realpath(fullname)
  117. return None
  118. def which_editor(specified_editor=None):
  119. """ Determine which editor program to use.
  120. :parameters:
  121. `specified_editor`
  122. Specified editor for reportbug, to be used in preference
  123. to other settings.
  124. :return value:
  125. Command to invoke for selected editor program.
  126. """
  127. debian_default_editor = "/usr/bin/sensible-editor"
  128. for editor in [specified_editor,
  129. os.environ.get("VISUAL"),
  130. os.environ.get("EDITOR"),
  131. debian_default_editor]:
  132. if editor:
  133. break
  134. return editor
  135. def glob_escape(filename):
  136. filename = re.sub(r'([*?\[\]])', r'\\\1', filename)
  137. return filename
  138. def search_pipe(searchfile, use_dlocate=True):
  139. arg = commands.mkarg(searchfile)
  140. if use_dlocate and os.path.exists('/usr/bin/dlocate'):
  141. pipe = os.popen('COLUMNS=79 dlocate -S %s 2>/dev/null' % arg)
  142. else:
  143. use_dlocate = False
  144. pipe = os.popen('COLUMNS=79 dpkg --search %s 2>/dev/null' % arg)
  145. return (pipe, use_dlocate)
  146. def query_dpkg_for(filename, use_dlocate=True):
  147. try:
  148. x = os.getcwd()
  149. except OSError:
  150. os.chdir('/')
  151. searchfilename = glob_escape(filename)
  152. (pipe, dlocate_used) = search_pipe(searchfilename, use_dlocate)
  153. packages = {}
  154. for line in pipe:
  155. line = line.strip()
  156. # Ignore diversions
  157. if 'diversion by' in line:
  158. continue
  159. (package, path) = line.split(': ', 1)
  160. path = path.strip()
  161. packlist = package.split(', ')
  162. for package in packlist:
  163. if package in packages:
  164. packages[package].append(path)
  165. else:
  166. packages[package] = [path]
  167. pipe.close()
  168. # Try again without dlocate if no packages found
  169. if not packages and dlocate_used:
  170. return query_dpkg_for(filename, use_dlocate=False)
  171. return filename, packages
  172. def find_package_for(filename, pathonly=False):
  173. """Find the package(s) containing this file."""
  174. packages = {}
  175. # tries to match also files in /var/lib/dpkg/info/
  176. if filename.startswith('/var/lib/dpkg/info/'):
  177. dpkg_info = re.compile('/var/lib/dpkg/info/(.+)\.[^.]+')
  178. m = dpkg_info.match(filename)
  179. # callee want a dict as second pair element...
  180. packages[m.group(1)] = ''
  181. return (filename, packages)
  182. if filename[0] == '/':
  183. fn, pkglist = query_dpkg_for(filename)
  184. if pkglist:
  185. return fn, pkglist
  186. newfilename = search_path_for(filename)
  187. if pathonly and not newfilename:
  188. return (filename, None)
  189. return query_dpkg_for(newfilename or filename)
  190. def find_rewritten(username):
  191. for filename in ['/etc/email-addresses']:
  192. if os.path.exists(filename):
  193. try:
  194. fp = file(filename)
  195. except IOError:
  196. continue
  197. for line in fp:
  198. line = line.strip().split('#')[0]
  199. if not line:
  200. continue
  201. try:
  202. name, alias = line.split(':')
  203. if name.strip() == username:
  204. return alias.strip()
  205. except ValueError:
  206. print 'Invalid entry in %s' % filename
  207. return None
  208. def check_email_addr(addr):
  209. """Simple check for email validity"""
  210. if '@' not in addr:
  211. return False
  212. if addr.count('@') != 1:
  213. return False
  214. localpart, domainpart = addr.split('@')
  215. if localpart.startswith('.') or localpart.endswith('.'):
  216. return False
  217. if '.' not in domainpart:
  218. return False
  219. if domainpart.startswith('.') or domainpart.endswith('.'):
  220. return False
  221. return True
  222. def get_email_addr(addr):
  223. addr = rfc822.AddressList(addr)
  224. return addr.addresslist[0]
  225. def get_email(email='', realname=''):
  226. return get_email_addr(get_user_id(email, realname))
  227. def get_user_id(email='', realname='', charset='utf-8'):
  228. uid = os.getuid()
  229. info = pwd.getpwuid(uid)
  230. email = (os.environ.get('REPORTBUGEMAIL', email) or
  231. os.environ.get('DEBEMAIL') or os.environ.get('EMAIL'))
  232. email = email or find_rewritten(info[0]) or info[0]
  233. if '@' not in email:
  234. if os.path.exists('/etc/mailname'):
  235. domainname = file('/etc/mailname').readline().strip()
  236. else:
  237. domainname = socket.getfqdn()
  238. email = email + '@' + domainname
  239. # Handle EMAIL if it's formatted as 'Bob <bob@host>'.
  240. if '<' in email or '(' in email:
  241. realname, email = get_email_addr(email)
  242. if not realname:
  243. realname = (os.environ.get('DEBFULLNAME') or os.environ.get('DEBNAME')
  244. or os.environ.get('NAME'))
  245. if not realname:
  246. realname = info[4].split(',', 1)[0]
  247. # Convert & in gecos field 4 to capitalized logname: #224231
  248. realname = realname.replace('&', info[0].upper())
  249. if not realname:
  250. return email
  251. # Decode the realname from the charset -
  252. # but only if it is not already in Unicode
  253. if isinstance(realname, str):
  254. realname = realname.decode(charset, 'replace')
  255. if re.match(r'[\w\s]+$', realname):
  256. return u'%s <%s>' % (realname, email)
  257. addr = rfc822.dump_address_pair((realname, email))
  258. if isinstance(addr, str):
  259. addr = addr.decode('utf-8', 'replace')
  260. return addr
  261. statuscache = {}
  262. def get_package_status(package, avail=False):
  263. if not avail and package in statuscache:
  264. return statuscache[package]
  265. versionre = re.compile('Version: ')
  266. packagere = re.compile('Package: ')
  267. priorityre = re.compile('Priority: ')
  268. dependsre = re.compile('(Pre-)?Depends: ')
  269. recsre = re.compile('Recommends: ')
  270. suggestsre = re.compile('Suggests: ')
  271. conffilesre = re.compile('Conffiles:')
  272. maintre = re.compile('Maintainer: ')
  273. statusre = re.compile('Status: ')
  274. originre = re.compile('Origin: ')
  275. bugsre = re.compile('Bugs: ')
  276. descre = re.compile('Description(?:-.*)?: ')
  277. fullre = re.compile(' ')
  278. srcre = re.compile('Source: ')
  279. sectionre = re.compile('Section: ')
  280. pkgversion = pkgavail = maintainer = status = origin = None
  281. bugs = vendor = priority = desc = src_name = section = None
  282. conffiles = []
  283. fulldesc = []
  284. depends = []
  285. recommends = []
  286. suggests = []
  287. confmode = False
  288. state = ''
  289. try:
  290. x = os.getcwd()
  291. except OSError:
  292. os.chdir('/')
  293. packarg = commands.mkarg(package)
  294. if avail:
  295. output = commands.getoutput(
  296. "COLUMNS=79 dpkg --print-avail %s 2>/dev/null" % packarg)
  297. else:
  298. output = commands.getoutput(
  299. "COLUMNS=79 dpkg --status %s 2>/dev/null" % packarg)
  300. # dpkg output is in UTF-8 format
  301. output = output.decode('utf-8', 'replace')
  302. for line in output.split(os.linesep):
  303. line = line.rstrip()
  304. if not line:
  305. continue
  306. if confmode:
  307. if line[:2] != ' /':
  308. confmode = False
  309. else:
  310. # re is used to identify also conffiles with spaces in the name
  311. conffiles = conffiles + [re.findall(r' (.+) ([0-9a-f]+).*$', line)[0]]
  312. if versionre.match(line):
  313. (crud, pkgversion) = line.split(": ", 1)
  314. elif statusre.match(line):
  315. (crud, status) = line.split(": ", 1)
  316. elif priorityre.match(line):
  317. (crud, priority) = line.split(": ", 1)
  318. elif packagere.match(line):
  319. (crud, pkgavail) = line.split(": ", 1)
  320. elif originre.match(line):
  321. (crud, origin) = line.split(": ", 1)
  322. elif bugsre.match(line):
  323. (crud, bugs) = line.split(": ", 1)
  324. elif descre.match(line):
  325. (crud, desc) = line.split(": ", 1)
  326. elif dependsre.match(line):
  327. (crud, thisdepends) = line.split(": ", 1)
  328. # Remove versioning crud
  329. thisdepends = [[y.split()[0] for y in x.split('|')]
  330. for x in (thisdepends.split(', '))]
  331. depends.extend(thisdepends)
  332. elif recsre.match(line):
  333. (crud, thisdepends) = line.split(": ", 1)
  334. # Remove versioning crud
  335. thisdepends = [[y.split()[0] for y in x.split('|')]
  336. for x in (thisdepends.split(', '))]
  337. recommends.extend(thisdepends)
  338. elif suggestsre.match(line):
  339. (crud, thisdepends) = line.split(": ", 1)
  340. # Remove versioning crud
  341. thisdepends = [[y.split()[0] for y in x.split('|')]
  342. for x in (thisdepends.split(', '))]
  343. suggests.extend(thisdepends)
  344. elif conffilesre.match(line):
  345. confmode = True
  346. elif maintre.match(line):
  347. crud, maintainer = line.split(": ", 1)
  348. elif srcre.match(line):
  349. crud, src_name = line.split(": ", 1)
  350. src_name = src_name.split()[0]
  351. elif sectionre.match(line):
  352. crud, section = line.split(": ", 1)
  353. elif desc and line[0] == ' ':
  354. fulldesc.append(line)
  355. installed = False
  356. if status:
  357. state = status.split()[2]
  358. installed = (state not in ('config-files', 'not-installed'))
  359. reportinfo = None
  360. if bugs:
  361. reportinfo = debbugs.parse_bts_url(bugs)
  362. elif origin:
  363. if origin in debbugs.SYSTEMS:
  364. vendor = debbugs.SYSTEMS[origin]['name']
  365. reportinfo = (debbugs.SYSTEMS[origin].get('type', 'debbugs'),
  366. debbugs.SYSTEMS[origin]['btsroot'])
  367. else:
  368. vendor = origin.capitalize()
  369. else:
  370. vendor = ''
  371. info = (pkgversion, pkgavail, tuple(depends), tuple(recommends),
  372. tuple(conffiles),
  373. maintainer, installed, origin, vendor, reportinfo, priority,
  374. desc, src_name, os.linesep.join(fulldesc), state, tuple(suggests),
  375. section)
  376. if not avail:
  377. statuscache[package] = info
  378. return info
  379. # dbase = []
  380. # avail = []
  381. # Object that essentially chunkifies the output of apt-cache dumpavail
  382. class AvailDB(object):
  383. def __init__(self, fp=None, popenob=None):
  384. self.popenob = popenob
  385. if fp:
  386. self.fp = fp
  387. elif popenob:
  388. self.fp = popenob.stdout
  389. def __iter__(self):
  390. return self
  391. def next(self):
  392. chunk = u''
  393. while True:
  394. if self.popenob:
  395. if self.popenob.returncode:
  396. break
  397. line = self.fp.readline()
  398. if not line:
  399. break
  400. if line == '\n':
  401. return chunk
  402. chunk += line.decode('utf-8', 'replace')
  403. if chunk:
  404. return chunk
  405. raise StopIteration
  406. def __del__(self):
  407. # print >> sys.stderr, 'availdb cleanup', repr(self.popenob), repr(self.fp)
  408. if self.popenob:
  409. # Clear the pipe before shutting it down
  410. while True:
  411. if self.popenob.returncode:
  412. break
  413. stuff = self.fp.read(65536)
  414. if not stuff:
  415. break
  416. self.popenob.wait()
  417. if self.fp:
  418. self.fp.close()
  419. def get_dpkg_database():
  420. try:
  421. fp = open(STATUSDB)
  422. if fp:
  423. return AvailDB(fp=fp)
  424. except IOError:
  425. print >> sys.stderr, 'Unable to open', STATUSDB
  426. sys.exit(1)
  427. def get_avail_database():
  428. # print >> sys.stderr, 'Searching available database'
  429. subp = subprocess.Popen(('apt-cache', 'dumpavail'), stdout=subprocess.PIPE)
  430. return AvailDB(popenob=subp)
  431. def available_package_description(package):
  432. data = commands.getoutput('apt-cache show' + commands.mkarg(package))
  433. data = data.decode('utf-8', 'replace')
  434. descre = re.compile('^Description(?:-.*)?: (.*)$')
  435. for line in data.split('\n'):
  436. m = descre.match(line)
  437. if m:
  438. return m.group(1)
  439. return None
  440. def get_source_name(package):
  441. packages = []
  442. data = commands.getoutput('apt-cache showsrc' + commands.mkarg(package))
  443. data = data.decode('utf-8', 'replace')
  444. packre = re.compile(r'^Package: (.*)$')
  445. for line in data.split('\n'):
  446. m = packre.match(line)
  447. if m:
  448. return m.group(1)
  449. return None
  450. def get_source_package(package):
  451. packages = []
  452. retlist = []
  453. found = {}
  454. data = commands.getoutput('apt-cache showsrc' + commands.mkarg(package))
  455. data = data.decode('utf-8', 'replace')
  456. binre = re.compile(r'^Binary: (.*)$')
  457. for line in data.split('\n'):
  458. m = binre.match(line)
  459. if m:
  460. packs = m.group(1)
  461. packlist = re.split(r',\s*', packs)
  462. packages += packlist
  463. for p in packages:
  464. desc = available_package_description(p)
  465. if desc and (p not in found):
  466. retlist += [(p, desc)]
  467. found[p] = desc
  468. retlist.sort()
  469. return retlist
  470. def get_package_info(packages, skip_notfound=False):
  471. if not packages:
  472. return []
  473. packinfo = get_dpkg_database()
  474. pkgname = r'(?:[\S]+(?:$|,\s+))'
  475. groupfor = {}
  476. searchpkgs = []
  477. searchbits = []
  478. for (group, package) in packages:
  479. groupfor[package] = group
  480. escpkg = re.escape(package)
  481. searchpkgs.append(escpkg)
  482. searchbits = [
  483. # Package regular expression
  484. r'^(?P<hdr>Package):\s+(' + '|'.join(searchpkgs) + ')$',
  485. # Provides regular expression
  486. r'^(?P<hdr>Provides):\s+' + pkgname + r'*(?P<pkg>' + '|'.join(searchpkgs) +
  487. r')(?:$|,\s+)' + pkgname + '*$'
  488. ]
  489. groups = groupfor.values()
  490. found = {}
  491. searchobs = [re.compile(x, re.MULTILINE) for x in searchbits]
  492. packob = re.compile('^Package: (?P<pkg>.*)$', re.MULTILINE)
  493. statob = re.compile('^Status: (?P<stat>.*)$', re.MULTILINE)
  494. versob = re.compile('^Version: (?P<vers>.*)$', re.MULTILINE)
  495. descob = re.compile('^Description(?:-.*)?: (?P<desc>.*)$', re.MULTILINE)
  496. ret = []
  497. for p in packinfo:
  498. for ob in searchobs:
  499. m = ob.search(p)
  500. if m:
  501. pack = packob.search(p).group('pkg')
  502. stat = statob.search(p).group('stat')
  503. sinfo = stat.split()
  504. stat = sinfo[0][0] + sinfo[2][0]
  505. # check if the package is installed, and in that case, retrieve
  506. # its information; if the first char is not 'i' (installed) or
  507. # the second is 'n' (not-installed), then skip data retrieval
  508. if stat[0] != 'i' or stat[1] == 'n':
  509. continue
  510. if m.group('hdr') == 'Provides':
  511. provides = m.group('pkg')
  512. else:
  513. provides = None
  514. vers = versob.search(p).group('vers')
  515. desc = descob.search(p).group('desc')
  516. info = (pack, stat, vers, desc, provides)
  517. ret.append(info)
  518. group = groupfor.get(pack)
  519. if group:
  520. for item in group:
  521. found[item] = True
  522. if provides not in found:
  523. found[provides] = True
  524. if skip_notfound:
  525. return ret
  526. for group in groups:
  527. notfound = [x for x in group if x not in found]
  528. if len(notfound) == len(group):
  529. if group not in found:
  530. ret.append((' | '.join(group), 'pn', '<none>',
  531. '(no description available)', None))
  532. return ret
  533. def packages_providing(package):
  534. aret = get_package_info([((package,), package)], skip_notfound=True)
  535. ret = []
  536. for pkg in aret:
  537. ret.append((pkg[0], pkg[3]))
  538. return ret
  539. def get_dependency_info(package, depends, rel="depends on"):
  540. if not depends:
  541. return ('\n%s %s no packages.\n' % (package, rel))
  542. dependencies = []
  543. for dep in depends:
  544. for bit in dep:
  545. dependencies.append((tuple(dep), bit))
  546. depinfo = "\nVersions of packages %s %s:\n" % (package, rel)
  547. packs = {}
  548. for info in get_package_info(dependencies):
  549. pkg = info[0]
  550. if pkg not in packs:
  551. packs[pkg] = info
  552. elif info[4]:
  553. if not packs[pkg][4]:
  554. packs[pkg] = info
  555. deplist = packs.values()
  556. deplist.sort()
  557. deplist2 = []
  558. # extract the info we need, also add provides where it fits
  559. for (pack, status, vers, desc, provides) in deplist:
  560. if provides:
  561. pack += ' [' + provides + ']'
  562. deplist2.append((pack, vers, status))
  563. deplist = deplist2
  564. # now we can compute the max possible value for each column, that can be the
  565. # max of all its values, or the space left from the other column; this way,
  566. # the sum of the 2 fields is never > 73 (hence the resulting line is <80
  567. # columns)
  568. maxp = max([len(x[0]) for x in deplist])
  569. maxv = max([len(x[1]) for x in deplist])
  570. widthp = min(maxp, 73 - maxv)
  571. widthv = min(maxv, 73 - widthp)
  572. for (pack, vers, status) in deplist:
  573. # we format the string specifying to align it in a field of a given
  574. # dimension (the first {width*}) but also limit its size (the second
  575. # {width*}
  576. info = '{0:3.3} {1:{widthp}.{widthp}} {2:{widthv}.{widthv}}'.format(
  577. status, pack, vers, widthp=widthp, widthv=widthv)
  578. # remove tailing white spaces
  579. depinfo += info.rstrip() + '\n'
  580. return depinfo
  581. def get_changed_config_files(conffiles, nocompress=False):
  582. confinfo = {}
  583. changed = []
  584. for (filename, md5sum) in conffiles:
  585. try:
  586. fp = file(filename)
  587. except IOError, msg:
  588. confinfo[filename] = msg
  589. continue
  590. filemd5 = commands.getoutput('md5sum ' + commands.mkarg(filename)).split()[0]
  591. if filemd5 == md5sum:
  592. continue
  593. changed.append(filename)
  594. thisinfo = 'changed:\n'
  595. for line in fp:
  596. if not line:
  597. continue
  598. if line == '\n' and not nocompress:
  599. continue
  600. if line[0] == '#' and not nocompress:
  601. continue
  602. thisinfo += line
  603. confinfo[filename] = thisinfo.decode('utf-8', 'replace')
  604. return confinfo, changed
  605. DISTORDER = ['oldstable', 'stable', 'testing', 'unstable', 'experimental']
  606. def get_debian_release_info():
  607. debvers = debinfo = verfile = warn = ''
  608. dists = []
  609. output = commands.getoutput('apt-cache policy 2>/dev/null')
  610. if output:
  611. mre = re.compile('\s+(\d+)\s+.*$\s+release\s.*o=(Ubuntu|Debian|Debian Ports),a=([^,]+),', re.MULTILINE)
  612. found = {}
  613. # XXX: When Python 2.4 rolls around, rewrite this
  614. for match in mre.finditer(output):
  615. pword, distname = match.group(1, 3)
  616. if distname in DISTORDER:
  617. pri, dist = int(pword), DISTORDER.index(distname)
  618. else:
  619. pri, dist = int(pword), len(DISTORDER)
  620. found[(pri, dist, distname)] = True
  621. if found:
  622. dists = found.keys()
  623. dists.sort()
  624. dists.reverse()
  625. dists = [(x[0], x[2]) for x in dists]
  626. debvers = dists[0][1]
  627. try:
  628. fob = open('/etc/debian_version')
  629. verfile = fob.readline().strip()
  630. fob.close()
  631. except IOError:
  632. print >> sys.stderr, 'Unable to open /etc/debian_version'
  633. if verfile:
  634. debinfo += 'Debian Release: ' + verfile + '\n'
  635. if debvers:
  636. debinfo += ' APT prefers ' + debvers + '\n'
  637. if dists:
  638. # Should wrap this eventually...
  639. # policystr = pprint.pformat(dists)
  640. policystr = ', '.join([str(x) for x in dists])
  641. debinfo += ' APT policy: %s\n' % policystr
  642. if warn:
  643. debinfo += warn
  644. return debinfo
  645. def lsb_release_info():
  646. return commands.getoutput('lsb_release -a 2>/dev/null') + '\n'
  647. def get_arch():
  648. arch = commands.getoutput('COLUMNS=79 dpkg --print-architecture 2>/dev/null')
  649. if not arch:
  650. un = os.uname()
  651. arch = un[4]
  652. arch = re.sub(r'i[456]86', 'i386', arch)
  653. arch = re.sub(r's390x', 's390', arch)
  654. arch = re.sub(r'ppc', 'powerpc', arch)
  655. return arch
  656. def get_multiarch():
  657. out = commands.getoutput('COLUMNS=79 dpkg --print-foreign-architectures 2>/dev/null')
  658. return ', '.join(out.splitlines())
  659. def generate_blank_report(package, pkgversion, severity, justification,
  660. depinfo, confinfo, foundfile='', incfiles='',
  661. system='debian', exinfo=None, type=None, klass='',
  662. subject='', tags='', body='', mode=MODE_EXPERT,
  663. pseudos=None, debsumsoutput=None, issource=False):
  664. # For now...
  665. import bugreport
  666. sysinfo = (package not in ('wnpp', 'ftp.debian.org'))
  667. # followup is where bugreport expects the notification of the bug reportbug
  668. # to follow-up, but reportbug pass this information with 'exinfo'
  669. rep = bugreport.bugreport(package, version=pkgversion, severity=severity,
  670. justification=justification, filename=foundfile,
  671. mode=mode, subject=subject, tags=tags, body=body,
  672. pseudoheaders=pseudos, followup=exinfo, type=type,
  673. system=system, depinfo=depinfo, sysinfo=sysinfo,
  674. confinfo=confinfo, incfiles=incfiles,
  675. debsumsoutput=debsumsoutput, issource=issource)
  676. return unicode(rep)
  677. def get_cpu_cores():
  678. cpucount = 0
  679. try:
  680. fob = open('/proc/cpuinfo')
  681. except IOError:
  682. print >> sys.stderr, 'Unable to open /proc/cpuinfo'
  683. return 0
  684. for line in fob:
  685. if line.startswith('processor'):
  686. cpucount += 1
  687. # Alpha platform
  688. if line.startswith('cpus detected'):
  689. cpucount = int(line.split()[-1])
  690. fob.close()
  691. return max(cpucount, 1)
  692. class our_lex(shlex.shlex):
  693. def get_token(self):
  694. token = shlex.shlex.get_token(self)
  695. if token is None or not len(token):
  696. return token
  697. if (token[0] == token[-1]) and token[0] in self.quotes:
  698. token = token[1:-1]
  699. return token
  700. USERFILE = os.path.expanduser('~/.reportbugrc')
  701. FILES = ('/etc/reportbug.conf', USERFILE)
  702. CONFIG_ARGS = (
  703. 'sendto', 'severity', 'mua', 'mta', 'email', 'realname', 'bts', 'verify',
  704. 'replyto', 'http_proxy', 'smtphost', 'editor', 'debconf', 'justification',
  705. 'sign', 'nocc', 'nocompress', 'dontquery', 'noconf', 'mirrors', 'keyid',
  706. 'headers', 'interface', 'template', 'mode', 'check_available', 'query_src',
  707. 'printonly', 'offline', 'check_uid', 'smtptls', 'smtpuser', 'smtppasswd',
  708. 'paranoid', 'mbox_reader_cmd', 'max_attachment_size')
  709. class Mua:
  710. command = ""
  711. name = ""
  712. def __init__(self, command):
  713. self.command = command
  714. self.name = command.split()[0]
  715. def send(self, filename):
  716. mua = self.command
  717. if '%s' not in mua:
  718. mua += ' %s'
  719. return ui.system(mua % commands.mkarg(filename)[1:])
  720. def get_name(self):
  721. return self.name
  722. class Gnus(Mua):
  723. name = "gnus"
  724. def __init__(self):
  725. pass
  726. def send(self, filename):
  727. elisp = """(progn
  728. (load-file "/usr/share/reportbug/reportbug.el")
  729. (tfheen-reportbug-insert-template "%s"))"""
  730. filename = re.sub("[\"\\\\]", "\\\\\\g<0>", filename)
  731. elisp = commands.mkarg(elisp % filename)
  732. return ui.system("emacsclient --no-wait --eval %s 2>/dev/null"
  733. " || emacs --eval %s" % (elisp, elisp))
  734. MUA = {
  735. 'mutt': Mua('mutt -H'),
  736. 'mh': Mua('/usr/bin/mh/comp -use -file'),
  737. 'gnus': Gnus(),
  738. 'claws-mail': Mua('claws-mail --compose-from-file'),
  739. }
  740. MUA['nmh'] = MUA['mh']
  741. # TODO: convert them to class methods
  742. MUAVERSION = {
  743. MUA['mutt']: 'mutt -v',
  744. MUA['mh']: '/usr/bin/mh/comp -use -file',
  745. MUA['gnus']: 'emacs --version',
  746. MUA['claws-mail']: 'claws-mail --version',
  747. }
  748. def mua_is_supported(mua):
  749. # check if the mua is supported by reportbug
  750. if mua == 'mh' or mua == MUA['mh']:
  751. mua_tmp = 'mh'
  752. elif mua == 'nmh' or mua == MUA['nmh']:
  753. mua_tmp = 'mh'
  754. elif mua == 'gnus' or mua == MUA['gnus']:
  755. mua_tmp = 'gnus'
  756. elif mua == 'mutt' or mua == MUA['mutt']:
  757. mua_tmp = 'mutt'
  758. elif mua == 'claws-mail' or mua == MUA['claws-mail']:
  759. mua_tmp = 'claws-mail'
  760. else:
  761. mua_tmp = mua
  762. if mua_tmp not in MUA:
  763. return False
  764. else:
  765. return True
  766. def mua_exists(mua):
  767. # check if the mua is available on the system
  768. if mua == 'mh' or mua == MUA['mh']:
  769. mua_tmp = MUA['mh']
  770. elif mua == 'nmh' or mua == MUA['nmh']:
  771. mua_tmp = MUA['mh']
  772. elif mua == 'gnus' or mua == MUA['gnus']:
  773. mua_tmp = MUA['gnus']
  774. elif mua == 'mutt' or mua == MUA['mutt']:
  775. mua_tmp = MUA['mutt']
  776. elif mua == 'claws-mail' or mua == MUA['claws-mail']:
  777. mua_tmp = MUA['claws-mail']
  778. else:
  779. mua_tmp = MUA[mua]
  780. output = '/dev/null'
  781. if os.path.exists(output):
  782. try:
  783. returnvalue = subprocess.call(MUAVERSION[mua_tmp], stdout=open(output, 'w'), stderr=subprocess.STDOUT,
  784. shell=True)
  785. except (IOError, OSError):
  786. returnvalue = subprocess.call(MUAVERSION[mua_tmp], shell=True)
  787. else:
  788. returnvalue = subprocess.call(MUAVERSION[mua_tmp], shell=True)
  789. # 127 is the shell standard return value to indicate a 'command not found' result
  790. if returnvalue == 127:
  791. return False
  792. else:
  793. return True
  794. def mua_name(mua):
  795. # in case the user specifies only the mua name in --mua, returns the default options
  796. if mua in MUA:
  797. return MUA[mua]
  798. else:
  799. return mua
  800. def first_run():
  801. return not os.path.exists(USERFILE)
  802. def parse_config_files():
  803. args = {}
  804. for filename in FILES:
  805. if os.path.exists(filename):
  806. try:
  807. lex = our_lex(file(filename), posix=True)
  808. except IOError, msg:
  809. continue
  810. lex.wordchars = lex.wordchars + '-.@/:<>'
  811. token = lex.get_token()
  812. while token:
  813. token = token.lower()
  814. if token in ('quiet', 'maintonly', 'submit'):
  815. args['sendto'] = token
  816. elif token == 'severity':
  817. token = lex.get_token().lower()
  818. if token in debbugs.SEVERITIES.keys():
  819. args['severity'] = token
  820. elif token == 'header':
  821. args['headers'] = args.get('headers', []) + [lex.get_token()]
  822. elif token in ('no-cc', 'cc'):
  823. args['nocc'] = (token == 'no-cc')
  824. elif token in ('no-compress', 'compress'):
  825. args['nocompress'] = (token == 'no-compress')
  826. elif token in ('no-query-bts', 'query-bts'):
  827. args['dontquery'] = (token == 'no-query-bts')
  828. elif token in ('config-files', 'no-config-files'):
  829. args['noconf'] = (token == 'no-config-files')
  830. elif token in ('printonly', 'template', 'offline'):
  831. args[token] = True
  832. elif token in ('email', 'realname', 'replyto', 'http_proxy',
  833. 'smtphost', 'editor', 'mua', 'mta', 'smtpuser',
  834. 'smtppasswd', 'justification', 'keyid',
  835. 'mbox_reader_cmd'):
  836. bit = lex.get_token()
  837. args[token] = bit.decode('utf-8', 'replace')
  838. elif token in ('no-smtptls', 'smtptls'):
  839. args['smtptls'] = (token == 'smtptls')
  840. elif token == 'sign':
  841. token = lex.get_token().lower()
  842. if token in ('pgp', 'gpg'):
  843. args['sign'] = token
  844. elif token == 'gnupg':
  845. args['sign'] = 'gpg'
  846. elif token == 'none':
  847. args['sign'] = ''
  848. elif token == 'ui':
  849. token = lex.get_token().lower()
  850. if token in AVAILABLE_UIS.keys():
  851. args['interface'] = token
  852. elif token == 'mode':
  853. arg = lex.get_token().lower()
  854. if arg in MODES.keys():
  855. args[token] = arg
  856. elif token == 'bts':
  857. token = lex.get_token().lower()
  858. if token in debbugs.SYSTEMS.keys():
  859. args['bts'] = token
  860. elif token == 'mirror':
  861. args['mirrors'] = args.get('mirrors', []) + [lex.get_token()]
  862. elif token in ('no-check-available', 'check-available'):
  863. args['check_available'] = (token == 'check-available')
  864. elif token == 'reportbug_version':
  865. # Currently ignored; might be used for compat purposes
  866. # eventually
  867. w_version = lex.get_token().lower()
  868. elif token in MUA:
  869. args['mua'] = MUA[token]
  870. elif token in ('query-source', 'no-query-source'):
  871. args['query_src'] = (token == 'query-source')
  872. elif token in ('debconf', 'no-debconf'):
  873. args['debconf'] = (token == 'debconf')
  874. elif token in ('verify', 'no-verify'):
  875. args['verify'] = (token == 'verify')
  876. elif token in ('check-uid', 'no-check-uid'):
  877. args['check_uid'] = (token == 'check-uid')
  878. elif token in ('paranoid', 'no-paranoid'):
  879. args['paranoid'] = (token == 'paranoid')
  880. elif token == 'max_attachment_size':
  881. arg = lex.get_token()
  882. args['max_attachment_size'] = int(arg)
  883. elif token == 'envelopefrom':
  884. token = lex.get_token().lower()
  885. args['envelopefrom'] = token
  886. else:
  887. sys.stderr.write('Unrecognized token: %s\n' % token)
  888. token = lex.get_token()
  889. return args
  890. def parse_bug_control_file(filename):
  891. submitas = submitto = None
  892. reportwith = []
  893. supplemental = []
  894. fh = file(filename)
  895. for line in fh:
  896. line = line.strip()
  897. parts = line.split(': ')
  898. if len(parts) != 2:
  899. continue
  900. header, data = parts[0].lower(), parts[1]
  901. if header == 'submit-as':
  902. submitas = data
  903. elif header == 'send-to':
  904. submitto = data
  905. elif header == 'report-with':
  906. reportwith += data.split(' ')
  907. elif header == 'package-status':
  908. supplemental += data.split(' ')
  909. return submitas, submitto, reportwith, supplemental
  910. def cleanup_msg(dmessage, headers, pseudos, type):
  911. pseudoheaders = []
  912. # Handle non-pseduo-headers
  913. headerre = re.compile(r'^([^:]+):\s*(.*)$', re.I)
  914. newsubject = message = ''
  915. parsing = lastpseudo = True
  916. # Include the headers that were passed in too!
  917. newheaders = []
  918. for header in headers:
  919. mob = headerre.match(header)
  920. if mob:
  921. newheaders.append(mob.groups())
  922. # Get the pseudo-headers fields
  923. PSEUDOS = []
  924. for ph in pseudos:
  925. mob = headerre.match(ph)
  926. if mob:
  927. PSEUDOS.append(mob.group(1))
  928. for line in dmessage.split(os.linesep):
  929. if not line and parsing:
  930. parsing = False
  931. elif parsing:
  932. mob = headerre.match(line)
  933. # GNATS and debbugs have different ideas of what a pseudoheader
  934. # is...
  935. if mob and ((type == 'debbugs' and
  936. mob.group(1) not in PSEUDOHEADERS and
  937. mob.group(1) not in PSEUDOS) or
  938. (type == 'gnats' and mob.group(1)[0] != '>')):
  939. newheaders.append(mob.groups())
  940. lastpseudo = False
  941. continue
  942. elif mob:
  943. # Normalize pseudo-header
  944. lastpseudo = False
  945. key, value = mob.groups()
  946. if key[0] != '>':
  947. # Normalize hyphenated headers to capitalize each word
  948. key = '-'.join([x.capitalize() for x in key.split('-')])
  949. pseudoheaders.append((key, value))
  950. elif not lastpseudo and len(newheaders):
  951. # Assume we have a continuation line
  952. lastheader = newheaders[-1]
  953. newheaders[-1] = (lastheader[0], lastheader[1] + '\n' + line)
  954. continue
  955. else:
  956. # Permit bogus headers in the pseudoheader section
  957. headers.append(re.split(':\s+', line, 1))
  958. elif line.strip() != NEWBIELINE:
  959. message += line + '\n'
  960. ph = []
  961. if type == 'gnats':
  962. for header, content in pseudoheaders:
  963. if content:
  964. ph += ["%s: %s" % (header, content)]
  965. else:
  966. ph += [header]
  967. else:
  968. ph2 = {}
  969. repeatable_ph = []
  970. # generate a list of pseudoheaders, but without duplicates
  971. # we take the list of pseudoheaders defined in reportbug and add
  972. # the ones passed by the user (if not already present). We are not using
  973. # set(..) because we want to preserve the item order of PSEUDOHEADERS
  974. pseudo_list = list(PSEUDOHEADERS)
  975. for p in PSEUDOS:
  976. if p not in pseudo_list:
  977. pseudo_list.append(p)
  978. for header, content in pseudoheaders:
  979. # if either in the canonical pseudo-headers list or in those passed on the command line
  980. if header in pseudo_list:
  981. if header not in REPEATABLE_PSEUDOHEADERS:
  982. ph2[header] = content
  983. else:
  984. repeatable_ph += ['%s: %s' % (header, content)]
  985. else:
  986. newheaders.append((header, content))
  987. for header in pseudo_list:
  988. if header in ph2:
  989. ph += ['%s: %s' % (header, ph2[header])]
  990. ph.extend(repeatable_ph)
  991. return message, newheaders, ph
  992. def launch_mbox_reader(cmd, url, http_proxy, timeout):
  993. """Runs the command specified by cmd passing the mbox file
  994. downloaded from url as a parameter. If cmd is None or fails, then
  995. fallback to mail program."""
  996. mbox = open_url(url, http_proxy, timeout)
  997. if mbox is None:
  998. return
  999. (fd, fname) = TempFile()
  1000. try:
  1001. for line in mbox:
  1002. fd.write(line)
  1003. fd.close()
  1004. if cmd is not None:
  1005. try:
  1006. cmd = cmd % fname
  1007. except TypeError:
  1008. cmd = "%s %s" % (cmd, fname)
  1009. error = os.system(cmd)
  1010. if not error:
  1011. return
  1012. # fallback
  1013. os.system('mail -f ' + fname)
  1014. finally:
  1015. os.unlink(fname)
  1016. def get_running_kernel_pkg():
  1017. """Return the package of the currently running kernel, needed to force
  1018. assignment for 'kernel' package to a real one"""
  1019. system = platform.system()
  1020. release = platform.release()
  1021. if system == 'Linux':
  1022. return 'linux-image-' + release
  1023. elif system == 'GNU/kFreeBSD':
  1024. return 'kfreebsd-image-' + release
  1025. else:
  1026. return None
  1027. def exec_and_parse_bugscript(handler, bugscript):
  1028. """Execute and parse the output of the package bugscript, in particular
  1029. identifying the headers and pseudo-headers blocks, if present"""
  1030. fh, filename = TempFile()
  1031. fh.close()
  1032. rc = os.system('LC_ALL=C %s %s %s' % (handler, commands.mkarg(bugscript),
  1033. commands.mkarg(filename)))
  1034. isheaders = False
  1035. ispseudoheaders = False
  1036. isattachments = False
  1037. headers = pseudoheaders = text = ''
  1038. attachments = []
  1039. fp = open(filename)
  1040. for line in fp.readlines():
  1041. # we identify the blocks for headers and pseudo-h
  1042. if line == '-- BEGIN HEADERS --\n':
  1043. isheaders = True
  1044. elif line == '-- END HEADERS --\n':
  1045. isheaders = False
  1046. elif line == '-- BEGIN PSEUDOHEADERS --\n':
  1047. ispseudoheaders = True
  1048. elif line == '-- END PSEUDOHEADERS --\n':
  1049. ispseudoheaders = False
  1050. elif line == '-- BEGIN ATTACHMENTS --\n':
  1051. isattachments = True
  1052. elif line == '-- END ATTACHMENTS --\n':
  1053. isattachments = False
  1054. else:
  1055. if isheaders:
  1056. headers += line
  1057. elif ispseudoheaders:
  1058. pseudoheaders += line
  1059. elif isattachments:
  1060. attachments.append(line.strip())
  1061. else:
  1062. text += line
  1063. fp.close()
  1064. cleanup_temp_file(filename)
  1065. text = text.decode('utf-8', 'replace')
  1066. return (rc, headers, pseudoheaders, text, attachments)
  1067. def check_package_name(pkg):
  1068. """Check the package name against Debian Policy:
  1069. https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source
  1070. Returns True if the package name is valid."""
  1071. pkg_re = re.compile('^[a-z0-9][a-z0-9+-\.]+$')
  1072. return True if pkg_re.match(pkg) else False
  1073. def get_init_system():
  1074. """Determines the init system on the current machine"""
  1075. init = 'unable to detect'
  1076. if os.path.isdir('/run/systemd/system'):
  1077. init = 'systemd (via /run/systemd/system)'
  1078. if not subprocess.call('. /lib/lsb/init-functions ; init_is_upstart', shell=True):
  1079. init = 'upstart (via init_is_upstart())'
  1080. elif os.path.isfile('/sbin/init') and not os.path.islink('/sbin/init'):
  1081. init = 'sysvinit (via /sbin/init)'
  1082. return init