Index: .gitignore
===================================================================
--- .gitignore	(revision 01e30178eedc635d91562f65f05d7cecc4a171c1)
+++ .gitignore	(revision d17381b325eedd199255b19ae02b7eed37a33d67)
@@ -3,2 +3,5 @@
 debian/
 *.pyc
+*.log
+*.log.*
+tags
Index: queue/lib/build-gutenbach-queue
===================================================================
--- queue/lib/build-gutenbach-queue	(revision 8b676cb958065e5e8e7a6f157160c9cc30b0dd63)
+++ queue/lib/build-gutenbach-queue	(revision 374c558ae95ed914fecf15ccac6209a348fa1dc1)
@@ -7,5 +7,5 @@
 while true; do
     time=$(date)
-    echo "This is a work in progress!  Please send bugs to jhamrick." >> /tmp/gutenbach/current_queue_temp
+    echo "This is a work in progress!  Please send bugs to gutenbach@mit.edu." >> /tmp/gutenbach/current_queue_temp
     echo "" >> /tmp/gutenbach/current_queue_temp
     echo "As of $time:" >> /tmp/gutenbach/current_queue_temp
Index: server/lib/TODO
===================================================================
--- server/lib/TODO	(revision c1cebbcf97fff8e0b0b1edaee02c56cee3397bf7)
+++ server/lib/TODO	(revision 374c558ae95ed914fecf15ccac6209a348fa1dc1)
@@ -4,4 +4,5 @@
 
 - finish implementing a bunch of the IPP handlers
+     [ ] print job
      [ ] validate job
      [ ] pause printer
@@ -15,15 +16,17 @@
 
 - add support in printer.py for:
+     [ ] print job
      [ ] validate job
-     [ ] pause printer
-     [ ] resume printer
-     [ ] set printer attributes
-     [ ] send uri
-     [ ] set job attributes
+     [x] pause printer
+     [x] resume printer
+     [\] set printer attributes
+     [\] send uri
+     [\] set job attributes
      [X] restart job
      [\] promote job
 
 - add support in job.py for:
-     [ ] restart job
+     [X] restart job
+     [x] resume job
 
 - finish test cases for printer.py
@@ -52,15 +55,16 @@
    - local streaming
    - generic streaming
+   - zephyr control (e.g. youtube link)
 
 Tickets that should be fixed by this new version:
-   http://gutenbach.mit.edu/ticket/21
-   http://gutenbach.mit.edu/ticket/2
-   http://gutenbach.mit.edu/ticket/7
-   http://gutenbach.mit.edu/ticket/39
-   http://gutenbach.mit.edu/ticket/47
-   http://gutenbach.mit.edu/ticket/48
-   http://gutenbach.mit.edu/ticket/8
-   http://gutenbach.mit.edu/ticket/34
-   http://gutenbach.mit.edu/ticket/38
-   http://gutenbach.mit.edu/ticket/41
-   http://gutenbach.mit.edu/ticket/18
+   [ ] http://gutenbach.mit.edu/ticket/21
+   [ ] http://gutenbach.mit.edu/ticket/2
+   [ ] http://gutenbach.mit.edu/ticket/7
+   [ ] http://gutenbach.mit.edu/ticket/39
+   [ ] http://gutenbach.mit.edu/ticket/47
+   [ ] http://gutenbach.mit.edu/ticket/48
+   [ ] http://gutenbach.mit.edu/ticket/8
+   [ ] http://gutenbach.mit.edu/ticket/34
+   [x] http://gutenbach.mit.edu/ticket/38
+   [ ] http://gutenbach.mit.edu/ticket/41
+   [ ] http://gutenbach.mit.edu/ticket/18
Index: server/lib/gutenbach/server/__init__.py
===================================================================
--- server/lib/gutenbach/server/__init__.py	(revision a9eb577e4cd2b47f81db5ec3c68805dcb0e6362b)
+++ server/lib/gutenbach/server/__init__.py	(revision cf0d7e841609343938618ddcce617a7fdb26fd9a)
@@ -3,4 +3,18 @@
 __all__ = ['errors']
 __all__.extend(errors.__all__)
+
+def sync(func):
+    """Lock decorator
+
+    Holds a lock (self.lock) for the durration of a method call.
+    """
+
+    def do(self, *args, **kwargs):
+        with self.lock:
+            return func(self, *args, **kwargs)
+
+    return do
+__all__.append('sync')
+
 
 from job import GutenbachJob
Index: server/lib/gutenbach/server/job.py
===================================================================
--- server/lib/gutenbach/server/job.py	(revision c1cebbcf97fff8e0b0b1edaee02c56cee3397bf7)
+++ server/lib/gutenbach/server/job.py	(revision 92253512a365e2c69f963b7b11d480cc2a48133b)
@@ -322,4 +322,18 @@
         self.player.mplayer_pause()
 
+    def resume(self):
+        """Non-blocking resume.  Job must be paused (see
+        'GutenbachJob.is_paused').
+
+        Raises
+        ------
+        InvalidJobStateException
+            If the job is not paused.
+
+        """
+        if not self.is_paused:
+            raise errors.InvalidJobStateException(self.state)
+        self.player.mplayer_pause()
+
     def cancel(self):
         """Blocking cancel. The job must not have been previously
Index: server/lib/gutenbach/server/player.py
===================================================================
--- server/lib/gutenbach/server/player.py	(revision a9eb577e4cd2b47f81db5ec3c68805dcb0e6362b)
+++ server/lib/gutenbach/server/player.py	(revision cf0d7e841609343938618ddcce617a7fdb26fd9a)
@@ -3,4 +3,5 @@
 import subprocess
 import time
+from . import sync
 
 # initialize logger
@@ -24,20 +25,18 @@
 
     @property
+    @sync
     def is_playing(self):
-        with self.lock:
-            if self._dryrun:
-                playing = self.isAlive() and not self.is_done
-            else:
-                playing = self.isAlive() and \
-                          not self.is_done and \
-                          self.player is not None and \
-                          self.player.poll() is None
-        return playing
+        if self._dryrun:
+            return self.isAlive() and not self.is_done
+        else:
+            return self.isAlive() and \
+                      not self.is_done and \
+                      self.player is not None and \
+                      self.player.poll() is None
 
     @property
+    @sync
     def is_paused(self):
-        with self.lock:
-            paused = self.is_playing and self._paused
-        return paused
+        return self.is_playing and self._paused
 
     @property
@@ -49,7 +48,7 @@
         return self._callback
     @callback.setter
+    @sync
     def callback(self, val):
-        with self.lock:
-            self._callback = val
+        self._callback = val
 
     def start(self):
@@ -112,4 +111,5 @@
 
     def mplayer_pause(self):
+        # Note: Inner lock due to sleep.
         with self.lock:
             if self.is_playing:
@@ -123,4 +123,5 @@
                 
     def mplayer_stop(self):
+        # Note: Inner Lock due to join.
         with self.lock:
             if self.is_playing:
Index: server/lib/gutenbach/server/printer.py
===================================================================
--- server/lib/gutenbach/server/printer.py	(revision c1cebbcf97fff8e0b0b1edaee02c56cee3397bf7)
+++ server/lib/gutenbach/server/printer.py	(revision 92253512a365e2c69f963b7b11d480cc2a48133b)
@@ -9,4 +9,5 @@
 import traceback
 import sys
+from . import sync
 
 
@@ -100,5 +101,5 @@
             with self.lock:
                 try:
-                    if self.current_job is None:
+                    if not self.paused and self.current_job is None:
                         self.start_job()
                     elif self.current_job.is_done:
@@ -124,20 +125,19 @@
 
     @property
+    @sync
     def state(self):
-        with self.lock:
-            if self.current_job is not None:
-                val = States.PROCESSING
-            elif len(self.pending_jobs) == 0:
-                val = States.IDLE
-            else:
-                val = States.STOPPED
-        return val
-
-    @property
+        if self.current_job is not None:
+            return States.PROCESSING
+        elif len(self.pending_jobs) == 0:
+            return States.IDLE
+        else:
+            return States.STOPPED
+
+    @property
+    @sync
     def active_jobs(self):
-        with self.lock:
-            jobs = self.pending_jobs[:]
-            if self.current_job is not None:
-                jobs.insert(0, self.current_job.id)
+        jobs = self.pending_jobs[:]
+        if self.current_job is not None:
+            jobs.insert(0, self.current_job.id)
         return jobs
 
@@ -146,35 +146,34 @@
     ######################################################################
 
+    @sync
     def start_job(self):
-        with self.lock:
-            if self.current_job is None:
-                try:
-                    job_id = heapq.heappop(self.pending_jobs)
-                    self.current_job = self.get_job(job_id)
-                    self.current_job.play()
-                except IndexError:
-                    self.current_job = None
-                except InvalidJobStateException:
-                    heapq.heappush(self.pending_jobs, self.current_job.id)
-                    self.current_job = None
+        if self.current_job is None:
+            try:
+                job_id = heapq.heappop(self.pending_jobs)
+                self.current_job = self.get_job(job_id)
+                self.current_job.play()
+            except IndexError:
+                self.current_job = None
+            except InvalidJobStateException:
+                heapq.heappush(self.pending_jobs, self.current_job.id)
+                self.current_job = None
                     
+    @sync
     def complete_job(self):
-        with self.lock:
-            if self.current_job is None:
-                return
-
-            try:
-                if not self.current_job.is_done:
-                    self.current_job.stop()
-            finally:
-                self.finished_jobs.append(self.current_job.id)
-                self.current_job = None
-
+        if self.current_job is None:
+            return
+
+        try:
+            if not self.current_job.is_done:
+                self.current_job.stop()
+        finally:
+            self.finished_jobs.append(self.current_job.id)
+            self.current_job = None
+
+    @sync
     def get_job(self, job_id):
-        with self.lock:
-            if job_id not in self.jobs:
-                raise InvalidJobException(job_id)
-            job = self.jobs[job_id]
-        return job
+        if job_id not in self.jobs:
+            raise InvalidJobException(job_id)
+        return self.jobs[job_id]
 
     ######################################################################
@@ -185,28 +184,49 @@
     def printer_uri_supported(self):
         return ipp.PrinterUriSupported(self.uri)
+    @printer_uri_supported.setter
+    def printer_uri_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("printer-uri-supported")
 
     @property
     def uri_authentication_supported(self):
         return ipp.UriAuthenticationSupported("none")
+    @uri_authentication_supported.setter
+    def uri_authentication_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("uri-authentication-supported")
 
     @property
     def uri_security_supported(self):
         return ipp.UriSecuritySupported("none")
+    @uri_security_supported.setter
+    def uri_security_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("uri-security-supported")
 
     @property
     def printer_name(self):
         return ipp.PrinterName(self.name)
+    @printer_name.setter
+    def printer_name(self, val):
+        raise ipp.errors.AttributesNotSettable("printer-name")
 
     @property
     def printer_state(self):
         return ipp.PrinterState(self.state)
+    @printer_state.setter
+    def printer_state(self, val):
+        raise ipp.errors.AttributesNotSettable("printer-state")
 
     @property
     def printer_state_reasons(self):
         return ipp.PrinterStateReasons("none")
+    @printer_state_reasons.setter
+    def printer_state_reasons(self, val):
+        raise ipp.errors.AttributesNotSettable("printer-state-reasons")
 
     @property
     def ipp_versions_supported(self):
         return ipp.IppVersionsSupported(*self.config['ipp-versions'])
+    @ipp_versions_supported.setter
+    def ipp_versions_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("ipp-versions-supported")
 
     # XXX: We should query ourself for the supported operations
@@ -214,56 +234,98 @@
     def operations_supported(self):
         return ipp.OperationsSupported(ipp.OperationCodes.GET_JOBS)
+    @operations_supported.setter
+    def operations_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("operations-supported")
 
     @property
     def charset_configured(self):
-        return ipp.CharsetConfigured("utf-8")
-
+        return ipp.CharsetConfigured("utf-8") # XXX
+    @charset_configured.setter
+    def charset_configured(self, val):
+        raise ipp.errors.AttributesNotSettable("charset-configured")
+        
     @property
     def charset_supported(self):
-        return ipp.CharsetSupported("utf-8")
+        return ipp.CharsetSupported("utf-8") # XXX
+    @charset_supported.setter
+    def charset_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("charset-supported")
 
     @property
     def natural_language_configured(self):
         return ipp.NaturalLanguageConfigured("en-us")
+    @natural_language_configured.setter
+    def natural_language_configured(self, val):
+        raise ipp.errors.AttributesNotSettable("natural-language-configured")
 
     @property
     def generated_natural_language_supported(self):
         return ipp.GeneratedNaturalLanguageSupported("en-us")
+    @generated_natural_language_supported.setter
+    def generated_natural_language_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("generated-natural-language-supported")
 
     @property
     def document_format_default(self):
         return ipp.DocumentFormatDefault("application/octet-stream")
+    @document_format_default.setter
+    def document_format_default(self, val):
+        raise ipp.errors.AttributesNotSettable("document-format-default")
 
     @property
     def document_format_supported(self):
         return ipp.DocumentFormatSupported("application/octet-stream", "audio/mp3")
+    @document_format_supported.setter
+    def document_format_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("document-format-supported")
 
     @property
     def printer_is_accepting_jobs(self):
         return ipp.PrinterIsAcceptingJobs(True)
+    @printer_is_accepting_jobs.setter
+    def printer_is_accepting_jobs(self, val):
+        raise ipp.errors.AttributesNotSettable("printer-is-accepting-jobs")
 
     @property
     def queued_job_count(self):
         return ipp.QueuedJobCount(len(self.active_jobs))
+    @queued_job_count.setter
+    def queued_job_count(self, val):
+        raise ipp.errors.AttributesNotSettable("queued-job-count")
 
     @property
     def pdl_override_supported(self):
         return ipp.PdlOverrideSupported("not-attempted")
+    @pdl_override_supported.setter
+    def pdl_override_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("pdl-override-supported")
 
     @property
     def printer_up_time(self):
         return ipp.PrinterUpTime(int(time.time()) - self.time_created)
+    @printer_up_time.setter
+    def printer_up_time(self, val):
+        raise ipp.errors.AttributesNotSettable("printer-up-time")
 
     @property
     def compression_supported(self):
         return ipp.CompressionSupported("none")
+    @compression_supported.setter
+    def compression_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("compression-supported")
 
     @property
     def multiple_operation_time_out(self):
         return ipp.MultipleOperationTimeOut(240)
+    @multiple_operation_time_out.setter
+    def multiple_operation_time_out(self, val):
+        raise ipp.errors.AttributesNotSettable("multiple-operation-time-out")
 
     @property
     def multiple_document_jobs_supported(self):
         return ipp.MultipleDocumentJobsSupported(False)
+    @multiple_document_jobs_supported.setter
+    def multiple_document_jobs_supported(self, val):
+        raise ipp.errors.AttributesNotSettable("multiple-document-jobs-supported")
 
     ######################################################################
@@ -349,9 +411,33 @@
         return job_id
 
+    @sync
     def pause_printer(self):
-        pass
-
+        """Pause the printer.
+
+        Does nothing if the printer is already paused.
+        """
+        if self.paused:
+            return
+
+        if self.current_job is not None and self.current_job.is_playing:
+            self.current_job.pause()
+
+        self.paused = True
+
+
+
+    @sync
     def resume_printer(self):
-        pass
+        """Resume the printer.
+
+        Does nothing if the printer is not paused.
+        """
+        if not self.paused:
+            return
+
+        if self.current_job is not None:
+            self.current_job.resume()
+
+        self.paused = False
 
     def get_printer_attributes(self, requested_attributes=None):
@@ -366,6 +452,10 @@
         return attributes
 
-    def set_printer_attributes(self):
-        pass
+    def set_printer_attributes(self, job_id, attributes):
+        for attr in attributes:
+            try:
+                setattr(self, attr, attributes[attr])
+            except AttributeError:
+                raise ipp.errors.ClientErrorAttributes
 
     def cancel_job(self, job_id, requesting_user_name=None):
@@ -385,6 +475,12 @@
         job.spool(document)
 
-    def send_uri(self):
-        pass
+    def send_uri(self, job_id, document_uri, document_name=None,
+                 document_format=None, document_natural_language=None,
+                 requesting_user_name=None, compression=None,
+                 last_document=None):
+        job = self.get_job(job_id)
+        # XXX: need to validate URI
+        # XXX: need to deal with the URI stream?
+        #job.spool_uri(document_uri)
 
     def get_job_attributes(self, job_id, requested_attributes=None):
@@ -399,7 +495,14 @@
         return attributes
 
-    def set_job_attributes(self):
-        pass
-
+    def set_job_attributes(self, job_id, attributes):
+        job = self.get_job(job_id)
+        for attr in attributes:
+            if attr in ("job-id", "job-k-octets", "job-state", "job-printer-uri"):
+                raise ipp.errors.ClientErrorAttributesNotSettable(attr)
+            elif attr == "job-name":
+                job.name = attributes[attr]
+            elif attr == "job-originating-user-name":
+                job.creator = attributes[attr] # XXX: do we want this?
+                
     def restart_job(self, job_id, requesting_user_name=None):
         job = self.get_job(job_id)
Index: server/lib/gutenbach/server/requests.py
===================================================================
--- server/lib/gutenbach/server/requests.py	(revision 4914b47659fd66f986adbaaf448c4cfde76b730e)
+++ server/lib/gutenbach/server/requests.py	(revision f70792f1bc122fe2ab67699f8946a0fd72426ceb)
@@ -985,5 +985,5 @@
             REQUIRED 'job-id' (integer(1:MAX)) and 'printer-uri' (uri)
             REQUIRED 'document-uri' (uri)
-              -or-   'job-uri' (uri)
+            OPTIONAL 'job-uri' (uri)
             OPTIONAL 'requesting-user-name' (name(MAX))
             OPTIONAL 'document-name' (name(MAX))
@@ -991,5 +991,4 @@
             OPTIONAL 'document-format' (mimeMediaType)
             OPTIONAL 'document-natural-language' (naturalLanguage)
-        Group 2: Document Content
             
         Response
@@ -1069,5 +1068,5 @@
             self.printer.send_uri(
                 job_id,
-                document_uri=document_uri,
+                document_uri,
                 document_name=document_name,
                 document_format=document_format,
