]> git.ipfire.org Git - thirdparty/cups.git/blobdiff - cups/testclient.c
Support <hex> strings in IPP files.
[thirdparty/cups.git] / cups / testclient.c
index aa3e3e039c32efd6964a6f8f84e8ae3743f7ba7d..cf945df98a6d3d24fb1051c0ff344911e1cb0a8d 100644 (file)
@@ -1,15 +1,17 @@
 /*
  * Simulated client test program for CUPS.
  *
- * Copyright 2017 by Apple Inc.
+ * Copyright © 2017-2019 by Apple Inc.
  *
- * Licensed under Apache License v2.0.  See the file "LICENSE" for more information.
+ * Licensed under Apache License v2.0.  See the file "LICENSE" for more
+ * information.
  */
 
 /*
  * Include necessary headers...
  */
 
+#include <config.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <cups/cups.h>
 #include <cups/thread-private.h>
 
 
+/*
+ * Constants...
+ */
+
+#define MAX_CLIENTS    16              /* Maximum number of client threads */
+
+
 /*
  * Local types...
  */
 
-typedef struct _client_monitor_s
+typedef struct _client_data_s
 {
   const char           *uri,           /* Printer URI */
                        *hostname,      /* Hostname */
@@ -30,6 +39,10 @@ typedef struct _client_monitor_s
                        *resource;      /* Resource path */
   int                  port;           /* Port number */
   http_encryption_t    encryption;     /* Use encryption? */
+  const char           *docfile,       /* Document file */
+                       *docformat;     /* Document format */
+  int                  grayscale,      /* Force grayscale? */
+                       keepfile;       /* Keep temporary file? */
   ipp_pstate_t         printer_state;  /* Current printer state */
   char                  printer_state_reasons[1024];
                                         /* Current printer-state-reasons */
@@ -37,7 +50,16 @@ typedef struct _client_monitor_s
   ipp_jstate_t         job_state;      /* Current job state */
   char                  job_state_reasons[1024];
                                         /* Current job-state-reasons */
-} _client_monitor_t;
+} _client_data_t;
+
+
+/*
+ * Local globals...
+ */
+
+static int             client_count = 0;
+static _cups_mutex_t   client_mutex = _CUPS_MUTEX_INITIALIZER;
+static int             verbosity = 0;
 
 
 /*
@@ -45,7 +67,8 @@ typedef struct _client_monitor_s
  */
 
 static const char       *make_raster_file(ipp_t *response, int grayscale, char *tempname, size_t tempsize, const char **format);
-static void            *monitor_printer(_client_monitor_t *monitor);
+static void            *monitor_printer(_client_data_t *data);
+static void            *run_client(_client_data_t *data);
 static void             show_attributes(const char *title, int request, ipp_t *ipp);
 static void             show_capabilities(ipp_t *response);
 static void             usage(void);
@@ -59,43 +82,27 @@ int                                 /* O - Exit status */
 main(int  argc,                                /* I - Number of command-line arguments */
      char *argv[])                     /* I - Command-line arguments */
 {
-  int                   i;              /* Looping var */
-  const char            *opt,           /* Current option */
-                        *uri = NULL,    /* Printer URI */
-                        *printfile = NULL,
-                                        /* Print file */
-                        *printformat = NULL;
-                                        /* Print format */
-  int                   keepfile = 0,   /* Keep temp file? */
-                        grayscale = 0,  /* Force grayscale? */
-                        verbosity = 0;  /* Verbosity */
-  char                  tempfile[1024] = "",
-                                        /* Temporary file (if any) */
-                        scheme[32],     /* URI scheme */
-                        userpass[256],  /* Username:password */
-                        hostname[256],  /* Hostname */
-                        resource[256];  /* Resource path */
-  int                   port;           /* Port number */
-  http_encryption_t     encryption;     /* Encryption mode */
-  _client_monitor_t     monitor;        /* Monitoring data */
-  http_t                *http;          /* HTTP connection */
-  ipp_t                 *request,       /* IPP request */
-                        *response;      /* IPP response */
-  ipp_attribute_t       *attr;          /* IPP attribute */
-  static const char * const pattrs[] =  /* Printer attributes we are interested in */
-  {
-    "job-template",
-    "printer-defaults",
-    "printer-description",
-    "media-col-database",
-    "media-col-ready"
-  };
+  int                  i;              /* Looping var */
+  const char           *opt;           /* Current option */
+  int                  num_clients = 0,/* Number of clients to simulate */
+                       clients_started = 0;
+                                       /* Number of clients that have been started */
+  char                 scheme[32],     /* URI scheme */
+                       userpass[256],  /* Username:password */
+                       hostname[256],  /* Hostname */
+                       resource[256];  /* Resource path */
+  _client_data_t       data;           /* Client data */
 
 
  /*
   * Parse command-line options...
   */
 
+  if (argc == 1)
+    return (0);
+
+  memset(&data, 0, sizeof(data));
+
   for (i = 1; i < argc; i ++)
   {
     if (argv[i][0] == '-')
@@ -104,8 +111,32 @@ main(int  argc,                            /* I - Number of command-line arguments */
       {
         switch (*opt)
         {
+          case 'c' : /* -c num-clients */
+              if (num_clients)
+              {
+                puts("Number of clients can only be specified once.");
+                usage();
+                return (1);
+              }
+
+              i ++;
+              if (i >= argc)
+              {
+                puts("Expected client count after '-c'.");
+                usage();
+                return (1);
+              }
+
+              if ((num_clients = atoi(argv[i])) < 1)
+              {
+                puts("Number of clients must be one or more.");
+                usage();
+                return (1);
+              }
+              break;
+
           case 'd' : /* -d document-format */
-              if (printformat)
+              if (data.docformat)
               {
                 puts("Document format can only be specified once.");
                 usage();
@@ -120,11 +151,11 @@ main(int  argc,                           /* I - Number of command-line arguments */
                 return (1);
               }
 
-              printformat = argv[i];
+              data.docformat = argv[i];
               break;
 
           case 'f' : /* -f print-file */
-              if (printfile)
+              if (data.docfile)
               {
                 puts("Print file can only be specified once.");
                 usage();
@@ -139,15 +170,15 @@ main(int  argc,                           /* I - Number of command-line arguments */
                 return (1);
               }
 
-              printfile = argv[i];
+              data.docfile = argv[i];
               break;
 
           case 'g' :
-              grayscale = 1;
+              data.grayscale = 1;
               break;
 
           case 'k' :
-              keepfile = 1;
+              data.keepfile = 1;
               break;
 
           case 'v' :
@@ -161,217 +192,83 @@ main(int  argc,                          /* I - Number of command-line arguments */
         }
       }
     }
-    else if (uri || (strncmp(argv[i], "ipp://", 6) && strncmp(argv[i], "ipps://", 7)))
+    else if (data.uri || (strncmp(argv[i], "ipp://", 6) && strncmp(argv[i], "ipps://", 7)))
     {
       printf("Unknown command-line argument '%s'.\n", argv[i]);
       usage();
       return (1);
     }
     else
-      uri = argv[i];
+      data.uri = argv[i];
   }
 
  /*
   * Make sure we have everything we need.
   */
 
-  if (!uri)
+  if (!data.uri)
   {
     puts("Expected printer URI.");
     usage();
     return (1);
   }
 
+  if (num_clients < 1)
+    num_clients = 1;
+
  /*
   * Connect to the printer...
   */
 
-  if (httpSeparateURI(HTTP_URI_CODING_ALL, uri, scheme, sizeof(scheme), userpass, sizeof(userpass), hostname, sizeof(hostname), &port, resource, sizeof(resource)) < HTTP_URI_STATUS_OK)
+  if (httpSeparateURI(HTTP_URI_CODING_ALL, data.uri, scheme, sizeof(scheme), userpass, sizeof(userpass), hostname, sizeof(hostname), &data.port, resource, sizeof(resource)) < HTTP_URI_STATUS_OK)
   {
-    printf("Bad printer URI '%s'.\n", uri);
+    printf("Bad printer URI '%s'.\n", data.uri);
     return (1);
   }
 
-  if (!port)
-    port = IPP_PORT;
+  if (!data.port)
+    data.port = IPP_PORT;
 
   if (!strcmp(scheme, "https") || !strcmp(scheme, "ipps"))
-    encryption = HTTP_ENCRYPTION_ALWAYS;
+    data.encryption = HTTP_ENCRYPTION_ALWAYS;
   else
-    encryption = HTTP_ENCRYPTION_IF_REQUESTED;
-
-  if ((http = httpConnect2(hostname, port, NULL, AF_UNSPEC, encryption, 1, 0, NULL)) == NULL)
-  {
-    printf("Unable to connect to '%s' on port %d: %s\n", hostname, port, cupsLastErrorString());
-    return (1);
-  }
+    data.encryption = HTTP_ENCRYPTION_IF_REQUESTED;
 
  /*
-  * Query printer status and capabilities...
+  * Start the client threads...
   */
 
-  request = ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES);
-  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri);
-  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, cupsUser());
-  ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, "requested-attributes", (int)(sizeof(pattrs) / sizeof(pattrs[0])), NULL, pattrs);
+  data.hostname = hostname;
+  data.resource = resource;
 
-  response = cupsDoRequest(http, request, resource);
-
-  if (verbosity)
-    show_capabilities(response);
-
- /*
-  * Now figure out what we will be printing...
-  */
-
-  if (printfile)
+  while (clients_started < num_clients)
   {
-   /*
-    * User specified a print file, figure out the format...
-    */
-
-    if ((opt = strrchr(printfile, '.')) != NULL)
+    _cupsMutexLock(&client_mutex);
+    if (client_count < MAX_CLIENTS)
     {
-     /*
-      * Guess the format from the extension...
-      */
+      _cups_thread_t   tid;            /* New thread */
 
-      if (!strcmp(opt, ".jpg"))
-        printformat = "image/jpeg";
-      else if (!strcmp(opt, ".pdf"))
-        printformat = "application/pdf";
-      else if (!strcmp(opt, ".ps"))
-        printformat = "application/postscript";
-      else if (!strcmp(opt, ".pwg"))
-        printformat = "image/pwg-raster";
-      else if (!strcmp(opt, ".urf"))
-        printformat = "image/urf";
-      else
-        printformat = "application/octet-stream";
+      client_count ++;
+      _cupsMutexUnlock(&client_mutex);
+      tid = _cupsThreadCreate((_cups_thread_func_t)run_client, &data);
+      _cupsThreadDetach(tid);
     }
     else
     {
-     /*
-      * Tell the printer to auto-detect...
-      */
-
-      printformat = "application/octet-stream";
+      _cupsMutexUnlock(&client_mutex);
+      sleep(1);
     }
   }
-  else
-  {
-   /*
-    * No file specified, make something to test with...
-    */
-
-    if ((printfile = make_raster_file(response, grayscale, tempfile, sizeof(tempfile), &printformat)) == NULL)
-      return (1);
-  }
 
-  ippDelete(response);
-
- /*
-  * Start monitoring the printer in the background...
-  */
-
-  memset(&monitor, 0, sizeof(monitor));
-
-  monitor.uri           = uri;
-  monitor.hostname      = hostname;
-  monitor.resource      = resource;
-  monitor.port          = port;
-  monitor.encryption    = encryption;
-
-  _cupsThreadCreate((_cups_thread_func_t)monitor_printer, &monitor);
-
- /*
-  * Create the job and wait for completion...
-  */
-
-  request = ippNewRequest(IPP_OP_CREATE_JOB);
-  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri);
-  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, cupsUser());
-
-  if ((opt = strrchr(printfile, '/')) != NULL)
-    opt ++;
-  else
-    opt = printfile;
-
-  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "job-name", NULL, opt);
-
-  if (verbosity)
-    show_attributes("Create-Job request", 1, request);
-
-  response = cupsDoRequest(http, request, resource);
-
-  if (verbosity)
-    show_attributes("Create-Job response", 0, response);
-
-  if (cupsLastError() >= IPP_STATUS_REDIRECTION_OTHER_SITE)
-  {
-    printf("Unable to create print job: %s\n", cupsLastErrorString());
-
-    monitor.job_state = IPP_JSTATE_ABORTED;
-
-    goto cleanup;
-  }
-
-  if ((attr = ippFindAttribute(response, "job-id", IPP_TAG_INTEGER)) == NULL)
-  {
-    puts("No job-id returned in Create-Job request.");
-
-    monitor.job_state = IPP_JSTATE_ABORTED;
-
-    goto cleanup;
-  }
-
-  monitor.job_id = ippGetInteger(attr, 0);
-
-  printf("CREATED JOB %d, sending %s of type %s\n", monitor.job_id, printfile, printformat);
-
-  ippDelete(response);
-
-  request = ippNewRequest(IPP_OP_SEND_DOCUMENT);
-  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri);
-  ippAddInteger(request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, "job-id", monitor.job_id);
-  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, cupsUser());
-  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, printformat);
-  ippAddBoolean(request, IPP_TAG_OPERATION, "last-document", 1);
-
-  if (verbosity)
-    show_attributes("Send-Document request", 1, request);
-
-  response = cupsDoFileRequest(http, request, resource, printfile);
-
-  if (verbosity)
-    show_attributes("Send-Document response", 0, response);
-
-  if (cupsLastError() >= IPP_STATUS_REDIRECTION_OTHER_SITE)
+  while (client_count > 0)
   {
-    printf("Unable to print file: %s\n", cupsLastErrorString());
-
-    monitor.job_state = IPP_JSTATE_ABORTED;
-
-    goto cleanup;
-  }
-
-  puts("WAITING FOR JOB TO COMPLETE");
-
-  while (monitor.job_state < IPP_JSTATE_CANCELED)
+    _cupsMutexLock(&client_mutex);
+    printf("%d RUNNING CLIENTS\n", client_count);
+    _cupsMutexUnlock(&client_mutex);
     sleep(1);
+  }
 
- /*
-  * Cleanup after ourselves...
-  */
-
-  cleanup:
-
-  httpClose(http);
-
-  if (tempfile[0] && !keepfile)
-    unlink(tempfile);
-
-  return (monitor.job_state == IPP_JSTATE_COMPLETED);
+  return (0);
 }
 
 
@@ -617,6 +514,7 @@ make_raster_file(ipp_t      *response,  /* I - Printer attributes */
   if ((fd = cupsTempFd(tempname, (int)tempsize)) < 0)
   {
     printf("Unable to create temporary print file: %s\n", strerror(errno));
+    free(line);
     return (NULL);
   }
 
@@ -624,6 +522,7 @@ make_raster_file(ipp_t      *response,  /* I - Printer attributes */
   {
     printf("Unable to open raster stream: %s\n", cupsRasterErrorString());
     close(fd);
+    free(line);
     return (NULL);
   }
 
@@ -720,7 +619,7 @@ make_raster_file(ipp_t      *response,  /* I - Printer attributes */
 
 static void *                          /* O - Thread exit code */
 monitor_printer(
-    _client_monitor_t *monitor)                /* I - Monitoring data */
+    _client_data_t *data)              /* I - Client data */
 {
   http_t       *http;                  /* Connection to printer */
   ipp_t                *request,               /* IPP request */
@@ -747,7 +646,7 @@ monitor_printer(
   * Open a connection to the printer...
   */
 
-  http = httpConnect2(monitor->hostname, monitor->port, NULL, AF_UNSPEC, monitor->encryption, 1, 0, NULL);
+  http = httpConnect2(data->hostname, data->port, NULL, AF_UNSPEC, data->encryption, 1, 0, NULL);
 
  /*
   * Loop until the job is canceled, aborted, or completed.
@@ -759,7 +658,7 @@ monitor_printer(
   job_state            = (ipp_jstate_t)0;
   job_state_reasons[0] = '\0';
 
-  while (monitor->job_state < IPP_JSTATE_CANCELED)
+  while (data->job_state < IPP_JSTATE_CANCELED)
   {
    /*
     * Reconnect to the printer as needed...
@@ -775,11 +674,11 @@ monitor_printer(
       */
 
       request = ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES);
-      ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, monitor->uri);
+      ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, data->uri);
       ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, cupsUser());
       ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, "requested-attributes", (int)(sizeof(pattrs) / sizeof(pattrs[0])), NULL, pattrs);
 
-      response = cupsDoRequest(http, request, monitor->resource);
+      response = cupsDoRequest(http, request, data->resource);
 
       if ((attr = ippFindAttribute(response, "printer-state", IPP_TAG_ENUM)) != NULL)
         printer_state = (ipp_pstate_t)ippGetInteger(attr, 0);
@@ -787,29 +686,29 @@ monitor_printer(
       if ((attr = ippFindAttribute(response, "printer-state-reasons", IPP_TAG_KEYWORD)) != NULL)
         ippAttributeString(attr, printer_state_reasons, sizeof(printer_state_reasons));
 
-      if (printer_state != monitor->printer_state || strcmp(printer_state_reasons, monitor->printer_state_reasons))
+      if (printer_state != data->printer_state || strcmp(printer_state_reasons, data->printer_state_reasons))
       {
-        printf("PRINTER: %s (%s)\n", ippEnumString("printer-state", printer_state), printer_state_reasons);
+        printf("PRINTER: %s (%s)\n", ippEnumString("printer-state", (int)printer_state), printer_state_reasons);
 
-        monitor->printer_state = printer_state;
-        strlcpy(monitor->printer_state_reasons, printer_state_reasons, sizeof(monitor->printer_state_reasons));
+        data->printer_state = printer_state;
+        strlcpy(data->printer_state_reasons, printer_state_reasons, sizeof(data->printer_state_reasons));
       }
 
       ippDelete(response);
 
-      if (monitor->job_id > 0)
+      if (data->job_id > 0)
       {
        /*
         * Check the status of the job itself...
         */
 
         request = ippNewRequest(IPP_OP_GET_JOB_ATTRIBUTES);
-        ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, monitor->uri);
-        ippAddInteger(request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, "job-id", monitor->job_id);
+        ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, data->uri);
+        ippAddInteger(request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, "job-id", data->job_id);
         ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, cupsUser());
         ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, "requested-attributes", (int)(sizeof(jattrs) / sizeof(jattrs[0])), NULL, jattrs);
 
-        response = cupsDoRequest(http, request, monitor->resource);
+        response = cupsDoRequest(http, request, data->resource);
 
         if ((attr = ippFindAttribute(response, "job-state", IPP_TAG_ENUM)) != NULL)
           job_state = (ipp_jstate_t)ippGetInteger(attr, 0);
@@ -817,19 +716,19 @@ monitor_printer(
         if ((attr = ippFindAttribute(response, "job-state-reasons", IPP_TAG_KEYWORD)) != NULL)
           ippAttributeString(attr, job_state_reasons, sizeof(job_state_reasons));
 
-        if (job_state != monitor->job_state || strcmp(job_state_reasons, monitor->job_state_reasons))
+        if (job_state != data->job_state || strcmp(job_state_reasons, data->job_state_reasons))
         {
-          printf("JOB %d: %s (%s)\n", monitor->job_id, ippEnumString("job-state", job_state), job_state_reasons);
+          printf("JOB %d: %s (%s)\n", data->job_id, ippEnumString("job-state", (int)job_state), job_state_reasons);
 
-          monitor->job_state = job_state;
-          strlcpy(monitor->job_state_reasons, job_state_reasons, sizeof(monitor->job_state_reasons));
+          data->job_state = job_state;
+          strlcpy(data->job_state_reasons, job_state_reasons, sizeof(data->job_state_reasons));
         }
 
         ippDelete(response);
       }
     }
 
-    if (monitor->job_state < IPP_JSTATE_CANCELED)
+    if (data->job_state < IPP_JSTATE_CANCELED)
     {
      /*
       * Sleep for 5 seconds...
@@ -845,6 +744,210 @@ monitor_printer(
 
   httpClose(http);
 
+  printf("FINISHED MONITORING JOB %d\n", data->job_id);
+
+  return (NULL);
+}
+
+
+/*
+ * 'run_client()' - Run a client thread.
+ */
+
+static void *                          /* O - Thread exit code */
+run_client(
+    _client_data_t *data)              /* I - Client data */
+{
+  _cups_thread_t monitor_id;           /* Monitoring thread ID */
+  const char   *name;                  /* Job name */
+  char         tempfile[1024] = "";    /* Temporary file (if any) */
+  _client_data_t ldata;                        /* Local client data */
+  http_t       *http;                  /* Connection to printer */
+  ipp_t                *request,               /* IPP request */
+               *response;              /* IPP response */
+  ipp_attribute_t *attr;               /* Attribute in response */
+  static const char * const pattrs[] =  /* Printer attributes we are interested in */
+  {
+    "job-template",
+    "printer-defaults",
+    "printer-description",
+    "media-col-database",
+    "media-col-ready"
+  };
+
+
+  ldata = *data;
+
+ /*
+  * Start monitoring the printer in the background...
+  */
+
+  monitor_id = _cupsThreadCreate((_cups_thread_func_t)monitor_printer, &ldata);
+
+ /*
+  * Open a connection to the printer...
+  */
+
+  http = httpConnect2(data->hostname, data->port, NULL, AF_UNSPEC, data->encryption, 1, 0, NULL);
+
+ /*
+  * Query printer status and capabilities...
+  */
+
+  request = ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES);
+  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, ldata.uri);
+  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, cupsUser());
+  ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, "requested-attributes", (int)(sizeof(pattrs) / sizeof(pattrs[0])), NULL, pattrs);
+
+  response = cupsDoRequest(http, request, ldata.resource);
+
+  if (verbosity)
+    show_capabilities(response);
+
+ /*
+  * Now figure out what we will be printing...
+  */
+
+  if (ldata.docfile)
+  {
+   /*
+    * User specified a print file, figure out the format...
+    */
+    const char *ext;                   /* Filename extension */
+
+    if ((ext = strrchr(ldata.docfile, '.')) != NULL)
+    {
+     /*
+      * Guess the format from the extension...
+      */
+
+      if (!strcmp(ext, ".jpg"))
+        ldata.docformat = "image/jpeg";
+      else if (!strcmp(ext, ".pdf"))
+        ldata.docformat = "application/pdf";
+      else if (!strcmp(ext, ".ps"))
+        ldata.docformat = "application/postscript";
+      else if (!strcmp(ext, ".pwg"))
+        ldata.docformat = "image/pwg-raster";
+      else if (!strcmp(ext, ".urf"))
+        ldata.docformat = "image/urf";
+      else
+        ldata.docformat = "application/octet-stream";
+    }
+    else
+    {
+     /*
+      * Tell the printer to auto-detect...
+      */
+
+      ldata.docformat = "application/octet-stream";
+    }
+  }
+  else
+  {
+   /*
+    * No file specified, make something to test with...
+    */
+
+    if ((ldata.docfile = make_raster_file(response, ldata.grayscale, tempfile, sizeof(tempfile), &ldata.docformat)) == NULL)
+      return ((void *)1);
+  }
+
+  ippDelete(response);
+
+ /*
+  * Create a job and wait for completion...
+  */
+
+  request = ippNewRequest(IPP_OP_CREATE_JOB);
+  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, ldata.uri);
+  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, cupsUser());
+
+  if ((name = strrchr(ldata.docfile, '/')) != NULL)
+    name ++;
+  else
+    name = ldata.docfile;
+
+  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "job-name", NULL, name);
+
+  if (verbosity)
+    show_attributes("Create-Job request", 1, request);
+
+  response = cupsDoRequest(http, request, ldata.resource);
+
+  if (verbosity)
+    show_attributes("Create-Job response", 0, response);
+
+  if (cupsLastError() >= IPP_STATUS_REDIRECTION_OTHER_SITE)
+  {
+    printf("Unable to create print job: %s\n", cupsLastErrorString());
+
+    ldata.job_state = IPP_JSTATE_ABORTED;
+
+    goto cleanup;
+  }
+
+  if ((attr = ippFindAttribute(response, "job-id", IPP_TAG_INTEGER)) == NULL)
+  {
+    puts("No job-id returned in Create-Job request.");
+
+    ldata.job_state = IPP_JSTATE_ABORTED;
+
+    goto cleanup;
+  }
+
+  ldata.job_id = ippGetInteger(attr, 0);
+
+  printf("CREATED JOB %d, sending %s of type %s\n", ldata.job_id, ldata.docfile, ldata.docformat);
+
+  ippDelete(response);
+
+  request = ippNewRequest(IPP_OP_SEND_DOCUMENT);
+  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, ldata.uri);
+  ippAddInteger(request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, "job-id", ldata.job_id);
+  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, cupsUser());
+  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, ldata.docformat);
+  ippAddBoolean(request, IPP_TAG_OPERATION, "last-document", 1);
+
+  if (verbosity)
+    show_attributes("Send-Document request", 1, request);
+
+  response = cupsDoFileRequest(http, request, ldata.resource, ldata.docfile);
+
+  if (verbosity)
+    show_attributes("Send-Document response", 0, response);
+
+  if (cupsLastError() >= IPP_STATUS_REDIRECTION_OTHER_SITE)
+  {
+    printf("Unable to print file: %s\n", cupsLastErrorString());
+
+    ldata.job_state = IPP_JSTATE_ABORTED;
+
+    goto cleanup;
+  }
+
+  puts("WAITING FOR JOB TO COMPLETE");
+
+  while (ldata.job_state < IPP_JSTATE_CANCELED)
+    sleep(1);
+
+ /*
+  * Cleanup after ourselves...
+  */
+
+  cleanup:
+
+  httpClose(http);
+
+  if (tempfile[0] && !ldata.keepfile)
+    unlink(tempfile);
+
+  _cupsThreadWait(monitor_id);
+
+  _cupsMutexLock(&client_mutex);
+  client_count --;
+  _cupsMutexUnlock(&client_mutex);
+
   return (NULL);
 }
 
@@ -946,6 +1049,7 @@ usage(void)
 {
   puts("Usage: ./testclient printer-uri [options]");
   puts("Options:");
+  puts("  -c num-clients      Simulate multiple clients");
   puts("  -d document-format  Generate the specified format");
   puts("  -f print-file       Print the named file");
   puts("  -g                  Force grayscale printing");