From d7f8557426798b69dff52e07205d3f75eb427dbd Mon Sep 17 00:00:00 2001 From: Samantha Baldwin Date: Sat, 22 Oct 2016 04:32:18 -0400 Subject: [PATCH] replace cdrdao.py with much simpler version (#52) * replace cdrdao.py with much simpler version * more pythonic syntax for cdrdao.read_toc(fast_toc=) switching * fix silly typo --- morituri/common/program.py | 21 +- morituri/program/cdrdao.py | 555 +++------------------------ morituri/rip/main.py | 8 - morituri/rip/offset.py | 9 +- morituri/test/test_program_cdrdao.py | 31 +- 5 files changed, 59 insertions(+), 565 deletions(-) diff --git a/morituri/common/program.py b/morituri/common/program.py index 83fa1abb..3bc8400d 100644 --- a/morituri/common/program.py +++ b/morituri/common/program.py @@ -126,26 +126,14 @@ def function(r, t): ptoc = cache.Persister(toc_pickle or None) if not ptoc.object: - tries = 0 - while True: - tries += 1 - t = cdrdao.ReadTOCTask(device=device) - try: - function(runner, t) - break - except: - if tries > 3: - raise - self.debug('failed to read TOC after %d tries, retrying' % tries) - - version = t.tasks[1].parser.version from pkg_resources import parse_version as V - # we've built a cdrdao 1.2.3rc2 modified package with the patch - if V(version) < V('1.2.3rc2p1'): + version = cdrdao.getCDRDAOVersion() + if V(version) < V('1.2.3rc2'): self.stdout.write('Warning: cdrdao older than 1.2.3 has a ' 'pre-gap length bug.\n' 'See http://sourceforge.net/tracker/?func=detail' '&aid=604751&group_id=2171&atid=102171\n') + t = cdrdao.ReadTOCTask(device) ptoc.persist(t.table) toc = ptoc.object assert toc.hasTOC() @@ -173,8 +161,7 @@ def getTable(self, runner, cddbdiscid, mbdiscid, device, offset): self.debug('getTable: cddbdiscid %s, mbdiscid %s not in cache for offset %s, ' 'reading table' % ( cddbdiscid, mbdiscid, offset)) - t = cdrdao.ReadTableTask(device=device) - runner.run(t) + t = cdrdao.ReadTableTask(device) itable = t.table tdict[offset] = itable ptable.persist(tdict) diff --git a/morituri/program/cdrdao.py b/morituri/program/cdrdao.py index c6fba644..95ba4f69 100644 --- a/morituri/program/cdrdao.py +++ b/morituri/program/cdrdao.py @@ -1,522 +1,73 @@ -# -*- Mode: Python; test-case-name:morituri.test.test_program_cdrdao -*- -# vi:si:et:sw=4:sts=4:ts=4 - -# Morituri - for those about to RIP - -# Copyright (C) 2009 Thomas Vander Stichele - -# This file is part of morituri. -# -# morituri is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# morituri 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with morituri. If not, see . - - -import re +import logging import os +import re import tempfile +from subprocess import check_call, Popen, PIPE, CalledProcessError -from morituri.common import log, common -from morituri.image import toc, table -from morituri.common import task as ctask - -from morituri.extern.task import task +from morituri.image.toc import TocFile +CDRDAO = 'cdrdao' -class ProgramError(Exception): +def read_toc(device, fast_toc=False): """ - The program had a fatal error. + Return cdrdao-generated table of contents for 'device'. """ + # cdrdao MUST be passed a non-existing filename as its last argument + # to write the TOC to; it does not support writing to stdout or + # overwriting an existing file, nor does linux seem to support + # locking a non-existant file. Thus, this race-condition introducing + # hack is carried from morituri to whipper and will be removed when + # cdrdao is fixed. + fd, tocfile = tempfile.mkstemp(suffix=u'.cdrdao.read-toc.whipper') + os.close(fd) + os.unlink(tocfile) - def __init__(self, errorMessage): - self.args = (errorMessage, ) - self.errorMessage = errorMessage - -states = ['START', 'TRACK', 'LEADOUT', 'DONE'] - -_VERSION_RE = re.compile(r'^Cdrdao version (?P.*) - \(C\)') - -_ANALYZING_RE = re.compile(r'^Analyzing track (?P\d+).*') - -_TRACK_RE = re.compile(r""" - ^(?P[\d\s]{2})\s+ # Track - (?P\w+)\s+ # Mode; AUDIO - \d\s+ # Flags - \d\d:\d\d:\d\d # Start in HH:MM:FF - \((?P.+)\)\s+ # Start in frames - \d\d:\d\d:\d\d # Length in HH:MM:FF - \((?P.+)\) # Length in frames -""", re.VERBOSE) - -_LEADOUT_RE = re.compile(r""" - ^Leadout\s - \w+\s+ # Mode - \d\s+ # Flags - \d\d:\d\d:\d\d # Start in HH:MM:FF - \((?P.+)\) # Start in frames -""", re.VERBOSE) - -_POSITION_RE = re.compile(r""" - ^(?P\d\d): # HH - (?P\d\d): # MM - (?P\d\d) # SS -""", re.VERBOSE) - -_ERROR_RE = re.compile(r"""^ERROR: (?P.*)""") - + cmd = [CDRDAO, 'read-toc'] + (['--fast-toc'] if fast_toc else []) + [ + '--device', device, tocfile] + # PIPE is the closest to >/dev/null we can get + try: + check_call(cmd, stdout=PIPE, stderr=PIPE) + except CalledProcessError, e: + logging.warning('cdrdao read-toc failed: return code is non-zero: ' + + str(e.returncode)) + raise e + toc = TocFile(tocfile) + toc.parse() + os.unlink(tocfile) + return toc -class LineParser(object, log.Loggable): +def version(): """ - Parse incoming bytes into lines - Calls 'parse' on owner for each parsed line. + Return cdrdao version as a string. """ + cdrdao = Popen(CDRDAO, stderr=PIPE) + out, err = cdrdao.communicate() + if cdrdao.returncode != 1: + logging.warning("cdrdao version detection failed: " + "return code is " + str(cdrdao.returncode)) + return None + m = re.compile(r'^Cdrdao version (?P.*) - \(C\)').search( + err.decode('utf-8')) + if not m: + logging.warning("cdrdao version detection failed: " + + "could not find version") + return None + return m.group('version') - def __init__(self, owner): - self._buffer = "" # accumulate characters - self._lines = [] # accumulate lines - self._owner = owner - - def read(self, bytes): - self.log('received %d bytes', len(bytes)) - self._buffer += bytes - - # parse buffer into lines if possible, and parse them - if "\n" in self._buffer: - self.log('buffer has newline, splitting') - lines = self._buffer.split('\n') - if lines[-1] != "\n": - # last line didn't end yet - self.log('last line still in progress') - self._buffer = lines[-1] - del lines[-1] - else: - self.log('last line finished, resetting buffer') - self._buffer = "" - - for line in lines: - self.log('Parsing %s', line) - self._owner.parse(line) - - self._lines.extend(lines) - - -class OutputParser(object, log.Loggable): - - def __init__(self, taskk, session=None): - self._buffer = "" # accumulate characters - self._lines = [] # accumulate lines - self._state = 'START' - self._frames = None # number of frames - self.track = 0 # which track are we analyzing? - self._task = taskk - self.tracks = 0 # count of tracks, relative to session - self._session = session - - - self.table = table.Table() # the index table for the TOC - self.version = None # cdrdao version - - def read(self, bytes): - self.log('received %d bytes in state %s', len(bytes), self._state) - self._buffer += bytes - - # find counter in LEADOUT state; only when we read full toc - self.log('state: %s, buffer bytes: %d', self._state, len(self._buffer)) - if self._buffer and self._state == 'LEADOUT': - # split on lines that end in \r, which reset cursor to counter - # start - # this misses the first one, but that's ok: - # length 03:40:71...\n00:01:00 - times = self._buffer.split('\r') - # counter ends in \r, so the last one would be empty - if not times[-1]: - del times[-1] - - position = "" - m = None - while times and not m: - position = times.pop() - m = _POSITION_RE.search(position) - - # we need both a position reported and an Analyzing line - # to have been parsed to report progress - if m and self.track is not None: - track = self.table.tracks[self.track - 1] - frame = (track.getIndex(1).absolute or 0) \ - + int(m.group('hh')) * 60 * common.FRAMES_PER_SECOND \ - + int(m.group('mm')) * common.FRAMES_PER_SECOND \ - + int(m.group('ss')) - self.log('at frame %d of %d', frame, self._frames) - self._task.setProgress(float(frame) / self._frames) - - # parse buffer into lines if possible, and parse them - if "\n" in self._buffer: - self.log('buffer has newline, splitting') - lines = self._buffer.split('\n') - if lines[-1] != "\n": - # last line didn't end yet - self.log('last line still in progress') - self._buffer = lines[-1] - del lines[-1] - else: - self.log('last line finished, resetting buffer') - self._buffer = "" - for line in lines: - self.log('Parsing %s', line) - m = _ERROR_RE.search(line) - if m: - error = m.group('error') - self._task.errors.append(error) - self.debug('Found ERROR: output: %s', error) - self._task.exception = ProgramError(error) - self._task.abort() - return - - self._parse(lines) - self._lines.extend(lines) - - def _parse(self, lines): - for line in lines: - #print 'parsing', len(line), line - methodName = "_parse_" + self._state - getattr(self, methodName)(line) - - def _parse_START(self, line): - if line.startswith('Cdrdao version'): - m = _VERSION_RE.search(line) - self.version = m.group('version') - - if line.startswith('Track'): - self.debug('Found possible track line') - if line == "Track Mode Flags Start Length": - self.debug('Found track line, moving to TRACK state') - self._state = 'TRACK' - return - - m = _ERROR_RE.search(line) - if m: - error = m.group('error') - self._task.errors.append(error) - - def _parse_TRACK(self, line): - if line.startswith('---'): - return - - m = _TRACK_RE.search(line) - if m: - t = int(m.group('track')) - self.tracks += 1 - track = table.Track(self.tracks, session=self._session) - track.index(1, absolute=int(m.group('start'))) - self.table.tracks.append(track) - self.debug('Found absolute track %d, session-relative %d', t, - self.tracks) - - m = _LEADOUT_RE.search(line) - if m: - self.debug('Found leadout line, moving to LEADOUT state') - self._state = 'LEADOUT' - self._frames = int(m.group('start')) - self.debug('Found absolute leadout at offset %r', self._frames) - self.info('%d tracks found for this session', self.tracks) - return - - def _parse_LEADOUT(self, line): - m = _ANALYZING_RE.search(line) - if m: - self.debug('Found analyzing line') - track = int(m.group('track')) - self.description = 'Analyzing track %d...' % track - self.track = track - - -# FIXME: handle errors - - -class CDRDAOTask(ctask.PopenTask): +def ReadTOCTask(device): """ - I am a task base class that runs CDRDAO. + stopgap morituri-insanity compatibility layer """ + return read_toc(device, fast_toc=True) - logCategory = 'CDRDAOTask' - description = "Reading TOC..." - options = None - - def __init__(self): - self.errors = [] - self.debug('creating CDRDAOTask') - - def start(self, runner): - self.debug('Starting cdrdao with options %r', self.options) - self.command = ['cdrdao', ] + self.options - - ctask.PopenTask.start(self, runner) - - def commandMissing(self): - raise common.MissingDependencyException('cdrdao') - - - def failed(self): - if self.errors: - raise DeviceOpenException("\n".join(self.errors)) - else: - raise ProgramFailedException(self._popen.returncode) - - -class DiscInfoTask(CDRDAOTask): +def ReadTableTask(device): """ - I am a task that reads information about a disc. - - @ivar sessions: the number of sessions - @type sessions: int + stopgap morituri-insanity compatibility layer """ + return read_toc(device) - logCategory = 'DiscInfoTask' - description = "Scanning disc..." - table = None - sessions = None - - def __init__(self, device=None): - """ - @param device: the device to rip from - @type device: str - """ - self.debug('creating DiscInfoTask for device %r', device) - CDRDAOTask.__init__(self) - - self.options = ['disk-info', ] - if device: - self.options.extend(['--device', device, ]) - - self.parser = LineParser(self) - - def readbytesout(self, bytes): - self.parser.read(bytes) - - def readbyteserr(self, bytes): - self.parser.read(bytes) - - def parse(self, line): - # called by parser - if line.startswith('Sessions'): - self.sessions = int(line[line.find(':') + 1:]) - self.debug('Found %d sessions', self.sessions) - m = _ERROR_RE.search(line) - if m: - error = m.group('error') - self.errors.append(error) - - def done(self): - pass - - -# Read stuff for one session - - -class ReadSessionTask(CDRDAOTask): - """ - I am a task that reads things for one session. - - @ivar table: the index table - @type table: L{table.Table} - """ - - logCategory = 'ReadSessionTask' - description = "Reading session" - table = None - extraOptions = None - - def __init__(self, session=None, device=None): - """ - @param session: the session to read - @type session: int - @param device: the device to rip from - @type device: str - """ - self.debug('Creating ReadSessionTask for session %d on device %r', - session, device) - CDRDAOTask.__init__(self) - self.parser = OutputParser(self) - (fd, self._tocfilepath) = tempfile.mkstemp( - suffix=u'.readtablesession.morituri') - os.close(fd) - os.unlink(self._tocfilepath) - - self.options = ['read-toc', ] - if device: - self.options.extend(['--device', device, ]) - if session: - self.options.extend(['--session', str(session)]) - self.description = "%s of session %d..." % ( - self.description, session) - if self.extraOptions: - self.options.extend(self.extraOptions) - - self.options.extend([self._tocfilepath, ]) - - def readbyteserr(self, bytes): - self.parser.read(bytes) - - if self.parser.tracks > 0: - self.setProgress(float(self.parser.track - 1) / self.parser.tracks) - - def done(self): - # by merging the TOC info. - self._tocfile = toc.TocFile(self._tocfilepath) - self._tocfile.parse() - os.unlink(self._tocfilepath) - self.table = self._tocfile.table - - # we know the .toc file represents a single wav rip, so all offsets - # are absolute since beginning of disc - self.table.absolutize() - # we unset relative since there is no real file backing this toc - for t in self.table.tracks: - for i in t.indexes.values(): - #i.absolute = i.relative - i.relative = None - - # copy the leadout from the parser's table - # FIXME: how do we get the length of the last audio track in the case - # of a data track ? - # self.table.leadout = self.parser.table.leadout - - # we should have parsed it from the initial output - assert self.table.leadout is not None - - -class ReadTableSessionTask(ReadSessionTask): - """ - I am a task that reads all indexes of a CD for a session. - - @ivar table: the index table - @type table: L{table.Table} - """ - - logCategory = 'ReadTableSessionTask' - description = "Scanning indexes" - - -class ReadTOCSessionTask(ReadSessionTask): - """ - I am a task that reads the TOC of a CD, without pregaps. - - @ivar table: the index table that matches the TOC. - @type table: L{table.Table} - """ - - logCategory = 'ReadTOCSessTask' - description = "Reading TOC" - extraOptions = ['--fast-toc', ] - - def done(self): - ReadSessionTask.done(self) - - assert self.table.hasTOC(), "This Table Index should be a TOC" - -# read all sessions - - -class ReadAllSessionsTask(task.MultiSeparateTask): - """ - I am a base class for tasks that need to read all sessions. - - @ivar table: the index table - @type table: L{table.Table} - """ - - logCategory = 'ReadAllSessionsTask' - table = None - _readClass = None - - def __init__(self, device=None): - """ - @param device: the device to rip from - @type device: str - """ - task.MultiSeparateTask.__init__(self) - - self._device = device - - self.debug('Starting ReadAllSessionsTask') - self.tasks = [DiscInfoTask(device=device), ] - - def stopped(self, taskk): - if not taskk.exception: - # After first task, schedule additional ones - if taskk == self.tasks[0]: - for i in range(taskk.sessions): - self.tasks.append(self._readClass(session=i + 1, - device=self._device)) - - if self._task == len(self.tasks): - self.table = self.tasks[1].table - if len(self.tasks) > 2: - for i, t in enumerate(self.tasks[2:]): - self.table.merge(t.table, i + 2) - - assert self.table.leadout is not None - - task.MultiSeparateTask.stopped(self, taskk) - - -class ReadTableTask(ReadAllSessionsTask): - """ - I am a task that reads all indexes of a CD for all sessions. - - @ivar table: the index table - @type table: L{table.Table} - """ - - logCategory = 'ReadTableTask' - description = "Scanning indexes..." - _readClass = ReadTableSessionTask - - -class ReadTOCTask(ReadAllSessionsTask): +def getCDRDAOVersion(): """ - I am a task that reads the TOC of a CD, without pregaps. - - @ivar table: the index table that matches the TOC. - @type table: L{table.Table} + stopgap morituri-insanity compatibility layer """ - - logCategory = 'ReadTOCTask' - description = "Reading TOC..." - _readClass = ReadTOCSessionTask - - -class DeviceOpenException(Exception): - - def __init__(self, msg): - self.msg = msg - self.args = (msg, ) - - -class ProgramFailedException(Exception): - - def __init__(self, code): - self.code = code - self.args = (code, ) - - -_VERSION_RE = re.compile( - "^Cdrdao version (?P.+) -") - - -def getCDRDAOVersion(): - getter = common.VersionGetter('cdrdao', - ["cdrdao"], - _VERSION_RE, - "%(version)s") - - return getter.get() + return version() diff --git a/morituri/rip/main.py b/morituri/rip/main.py index ad41134f..667cdbe3 100644 --- a/morituri/rip/main.py +++ b/morituri/rip/main.py @@ -10,7 +10,6 @@ from morituri.configure import configure from morituri.extern.command import command from morituri.extern.task import task -from morituri.program import cdrdao from morituri.rip import cd, offset, drive, image, accurip, debug @@ -33,13 +32,6 @@ def main(): sys.stderr.write('rip: error: missing dependency "%s"\n' % e.exception.dependency) return 255 - # FIXME: move this exception - if isinstance(e.exception, cdrdao.DeviceOpenException): - sys.stderr.write("""rip: error: cannot read CD from drive. -cdrdao says: -%s -""" % e.exception.msg) - return 255 if isinstance(e.exception, common.EmptyError): log.debug('main', diff --git a/morituri/rip/offset.py b/morituri/rip/offset.py index 4869b3d5..a9b5982c 100644 --- a/morituri/rip/offset.py +++ b/morituri/rip/offset.py @@ -100,14 +100,7 @@ def do(self, args): prog.unmountDevice(device) # first get the Table Of Contents of the CD - t = cdrdao.ReadTOCTask(device=device) - - try: - runner.run(t) - except cdrdao.DeviceOpenException, e: - self.error(e.msg) - return 3 - + t = cdrdao.ReadTOCTask(device) table = t.table self.debug("CDDB disc id: %r", table.getCDDBDiscId()) diff --git a/morituri/test/test_program_cdrdao.py b/morituri/test/test_program_cdrdao.py index 37b49868..eeebb093 100644 --- a/morituri/test/test_program_cdrdao.py +++ b/morituri/test/test_program_cdrdao.py @@ -7,38 +7,9 @@ from morituri.test import common - -class FakeTask: - - def setProgress(self, value): - pass - - -class ParseTestCase(common.TestCase): - - def setUp(self): - path = os.path.join(os.path.dirname(__file__), - 'cdrdao.readtoc.progress') - self._parser = cdrdao.OutputParser(FakeTask()) - - self._handle = open(path) - - def testParse(self): - # FIXME: we should be testing splitting in byte blocks, not lines - for line in self._handle.readlines(): - self._parser.read(line) - - for i, offset in enumerate( - [0, 13864, 31921, 48332, 61733, 80961, - 100219, 116291, 136188, 157504, 175275]): - track = self._parser.table.tracks[i] - self.assertEquals(track.getIndex(1).absolute, offset) - - self.assertEquals(self._parser.version, '1.2.2') - +# TODO: Current test architecture makes testing cdrdao difficult. Revisit. class VersionTestCase(common.TestCase): - def testGetVersion(self): v = cdrdao.getCDRDAOVersion() self.failUnless(v)