////////////////////////////////////////////////////////////////////////////////
// File: LprJob.cs
// Purpose: Main LPR job control code.
// Notice: Copyright 2010 SunCat Services
// Author: Edward F Eaglehouse
//
// THIS WORK IS PROVIDED "AS IS" WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES
// OR CONDITIONS OR GUARANTEES.
//
// This product is licensed under the terms of The Code Project Open
// License (CPOL) 1.02. You are free to use this code in any application,
// private, public, or commercial, subject to this license agreement. A
// copy of this license may be found on the web at
// http://www.codeproject.com/info/cpol10.aspx.
////////////////////////////////////////////////////////////////////////////////
// Revision Contributor
// 12/08/2010 Edward F Eaglehouse
// Initial revision.
// I am very interested in tracking how meaningful a contribution this
// project has been to the programming community. If you use this code,
// please email me your name, company, and optionally a contact email
// address or phone number to mailto:MobileLPR@SunCatServices.com. Your
// email address and phone number will be kept private.
////////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Threading;
using System.Net.Sockets;
using System.Net;
namespace MobileLPR
{
#region Event Handler Delegates
public delegate void LprJobEventHandler(Object sender, LprJobProgressEventArgs args);
#endregion
/// <summary>
/// Class used to define and submit a print job to a print server.
/// It supports LPR, LPRng, and Direct protocols.
/// </summary>
public class LprJob
{
#region Constants
/// <summary>
/// Newline string used to terminate server commands.
/// </summary>
const String LPR_NEWLINE = "\n";
/// <summary>
/// Maximum job number count for assigning job numbers.
/// </summary>
const int MAX_JOB_COUNT = 1000;
/// <summary>
/// String of letters used to identify data file in job.
/// </summary>
const String LPR_INDEXNAMES = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/// <summary>
/// Size of read buffer used for data files.
/// </summary>
const int LPR_BUFSIZE = 2048;
/// <summary>
/// Receive timeout in milliseconds.
/// </summary>
const int LPR_RECV_TIMEOUT = 15000;
/// <summary>
/// Transmission timeout in milliseconds.
/// </summary>
const int LPR_SEND_TIMEOUT = 15000;
#endregion
#region Enumerations
/// <summary>
/// LPR/LPRng server command codes.
/// </summary>
public enum ServerCommands
{
None = 0,
PrintWaitingJobs = 1,
SubmitJob = 2,
ShortQueueStatus = 3,
LongQueueStatus = 4,
RemoveJobs = 5
};
/// <summary>
/// LPR/LPRng server Submit Job subcommand codes.
/// </summary>
public enum SubmitJobSubCommands
{
None = 0,
AbortJob = 1,
SubmitControlFile = 2,
SubmitDataFile = 3
};
#endregion
#region Events
/// <summary>
/// Event raised when print job is started.
/// </summary>
/// <remarks>
/// Argument values included: cumulative number of (local) bytes sent,
/// total number of (local) bytes to be sent.
/// </remarks>
public event LprJobEventHandler LprJobStarted;
/// <summary>
/// Event raised when print job is completed.
/// </summary>
/// <remarks>
/// Argument values included: cumulative number of (local) bytes sent,
/// total number of (local) bytes to be sent.
/// </remarks>
public event LprJobEventHandler LprJobCompleted;
/// <summary>
/// Event raised when a block of data has been sent.
/// </summary>
/// <remarks>
/// Argument values included: local control/data pathname,
/// cumulative number of (local) bytes sent, total number of (local)
/// bytes to be sent.
/// </remarks>
public event LprJobEventHandler LprJobProgressChanged;
#endregion
#region Static Members
/// <summary>
/// Next Job Number to be used (0 <= n < MAX_JOB_COUNT).
/// </summary>
private static int nextJobNumber_ = 0;
/// <summary>
/// Default Server Name (LPD host) to use when creating a job.
/// </summary>
private static String defaultServerName_ = String.Empty;
/// <summary>
/// Default Server Port (LPD port) to use when creating a job.
/// </summary>
private static Int32 defaultServerPort_ = 515;
/// <summary>
/// Default Printer Queue to use when creating a job.
/// </summary>
private static String defaultPrinterName_ = "printer";
/// <summary>
/// Default Protocol to use when transferring a job.
/// </summary>
private static LprProtocols defaultProtocol_ = LprProtocols.LPR;
#endregion
#region Private Members
#region Connection Parameters
/// <summary>
/// Protocol selected for transferring job to print server.
/// </summary>
private LprProtocols protocol_ = LprProtocols.LPR;
/// <summary>
/// Server Name (LPD host) assigned to the print job.
/// </summary>
private String serverName_ = String.Empty;
/// <summary>
/// Server Port (LPD port) assigned to the print job.
/// </summary>
private Int32 serverPort_ = 515;
#endregion
#region Job Parameters
/// <summary>
/// Printer Name (LPR queue) assigned to the print job.
/// </summary>
private String printerName_ = String.Empty;
/// <summary>
/// User Name of user creating the job.
/// </summary>
private String userName_ = String.Empty;
/// <summary>
/// Host Name of the computer sending the job.
/// </summary>
private String hostName_ = String.Empty;
/// <summary>
/// Job Name to print on Banner page or queue status.
/// </summary>
private String jobName_ = String.Empty;
/// <summary>
/// Number of spaces represented by tab characters in text files.
/// </summary>
private Int32 indentSize_ = 8;
/// <summary>
/// Maximum Width of text file to be printed.
/// </summary>
private Int32 pageWidth_ = 132;
/// <summary>
/// Flag indicating if a Banner Page should be printed.
/// </summary>
private Boolean bannerEnabled_ = false;
/// <summary>
/// Class Name to print on Banner page.
/// </summary>
private String bannerJobClass_ = String.Empty;
/// <summary>
/// Title to print on Banner page.
/// </summary>
private String pageTitle_ = String.Empty;
/// <summary>
/// Flag indicating to send mail when a job is completed.
/// </summary>
private Boolean mailEnabled_ = false;
/// <summary>
/// Mail recipient of job completion message.
/// </summary>
private String mailTo_ = String.Empty;
/// <summary>
/// Number of Copies to print of each data file.
/// </summary>
private Int32 copies_ = 1;
/// <summary>
/// Priority (0-25) for LPRng jobs.
/// </summary>
private Int32 priority_ = 0;
/// <summary>
/// Collection of data files included in job.
/// </summary>
private List<LprDataFile> dataFiles_ = new List<LprDataFile>();
#endregion
#region Housekeeping Values
/// <summary>
/// Job Number used to identify the print job.
/// </summary>
private int jobNumber_ = 0;
/// <summary>
/// Total count of local bytes to send for the job.
/// </summary>
private Int64 jobSizeTotal_ = 0;
/// <summary>
/// Current count of local bytes sent for the job.
/// </summary>
private Int64 jobSizeUsed_ = 0;
/// <summary>
/// Flag indicating to send control file before data files.
/// </summary>
private Boolean sendControlFileFirst_ = true;
/// <summary>
/// Pathname of local control file.
/// </summary>
private String controlFilename_ = String.Empty;
/// <summary>
/// Current index of data file being transferred.
/// </summary>
private Int32 dataFileIndex_ = 0;
/// <summary>
/// Current copy index of data file being transferred.
/// </summary>
private Int32 copyIndex_ = 0;
/// <summary>
/// Object used to translate between client and server encodings.
/// </summary>
private Encoding serverEncoding_ = null;
/// <summary>
/// Connection object used to exchange data with server.
/// </summary>
private TcpClient clientConnection_ = null;
/// <summary>
/// Flag indicating the control file was sent.
/// </summary>
private Boolean sentControlFile_ = false;
#endregion
#region Job Status and Results
/// <summary>
/// Synchronization object used to signal end-of-job.
/// </summary>
private ManualResetEvent endJob_ = new ManualResetEvent(true);
/// <summary>
/// Flag indicating that a connection to server is active.
/// </summary>
private Boolean isConnected_ = false;
/// <summary>
/// Last Exception encountered when an error occurs.
/// </summary>
private Exception lastException_ = null;
#endregion
#endregion
#region Properties
#region Default Properties
/// <summary>
/// Get or set default Server Name to use when creating a job.
/// </summary>
public static String DefaultServerName
{
get { return defaultServerName_; }
set { defaultServerName_ = value; }
}
/// <summary>
/// Get or set default Server Port to use when creating a job.
/// </summary>
public static Int32 DefaultServerPort
{
get { return defaultServerPort_; }
set { defaultServerPort_ = value; }
}
/// <summary>
/// Get or set default Printer Queue to use when creating a job.
/// </summary>
public static String DefaultPrinterName
{
get { return defaultPrinterName_; }
set { defaultPrinterName_ = value; }
}
/// <summary>
/// Get or set default Protocol to use when transferring a job.
/// </summary>
public static LprProtocols DefaultProtocol
{
get { return defaultProtocol_; }
set { defaultProtocol_ = value; }
}
#endregion
#region Connection Parameters
/// <summary>
/// Get or set Protocol to use when transferring job to print server.
/// </summary>
public LprProtocols Protocol
{
get { return protocol_; }
set { protocol_ = value; }
}
/// <summary>
/// Get or set Server Name (LPR host) assigned to the print job.
/// </summary>
public String ServerName
{
get { return serverName_; }
set { serverName_ = value; }
}
/// <summary>
/// Get or set Server Port (LPR port) assigned to the print job.
/// </summary>
public Int32 ServerPort
{
get { return serverPort_; }
set
{
serverPort_ = (value > 0 && value <= 65535) ?
value : defaultServerPort_;
}
}
#endregion
#region Job Parameters
/// <summary>
/// Get or set Printer Name (LPR queue) assigned to the print job.
/// </summary>
public String PrinterName
{
get { return printerName_; }
set { printerName_ = value; }
}
/// <summary>
/// Get or set User Name of user creating the job.
/// </summary>
public String UserName
{
get { return userName_; }
set { userName_ = value; }
}
/// <summary>
/// Get or set Host Name of the computer sending the job.
/// </summary>
public String HostName
{
get { return hostName_; }
set { hostName_ = value; }
}
/// <summary>
/// Get or set Job Name to display on Banner page or queue status.
/// </summary>
public String JobName
{
get { return jobName_; }
set { jobName_ = value; }
}
/// <summary>
/// Get or set Number of spaces represented by tab characters in text.
/// </summary>
public Int32 IndentSize
{
get { return indentSize_; }
set { indentSize_ = (value < 1) ? 8 : value; }
}
/// <summary>
/// Get or set Maximum Width of text file to be printed.
/// </summary>
public Int32 PageWidth
{
get { return pageWidth_; }
set { pageWidth_ = (value < 1) ? 1 : value; }
}
/// <summary>
/// Get or set flag indicating if a Banner Page should be printed.
/// </summary>
public Boolean BannerEnabled
{
get { return bannerEnabled_; }
set { bannerEnabled_ = value; }
}
/// <summary>
/// Class Name to print on Banner page.
/// </summary>
public String BannerJobClass
{
get { return bannerJobClass_; }
set { bannerJobClass_ = value; }
}
/// <summary>
/// Get or set Title to print on Banner page.
/// </summary>
public String PageTitle
{
get { return pageTitle_; }
set { pageTitle_ = value; }
}
/// <summary>
/// Get or set flag indicating to send mail when a job is completed.
/// </summary>
public Boolean MailEnabled
{
get { return mailEnabled_; }
set { mailEnabled_ = value; }
}
/// <summary>
/// Get or set Mail recipient of job completion message.
/// </summary>
public String MailTo
{
get { return mailTo_; }
set { mailTo_ = value; }
}
/// <summary>
/// Get or set Number of Copies to print of each data file.
/// </summary>
/// <remarks>
/// This option is unsupported by LPR protocol and is forced to 1 when
/// using it.
/// </remarks>
public Int32 Copies
{
get { return copies_; }
set { copies_ = (value > 0) ? value : 1; }
}
/// <summary>
/// Get or set Priority (0-25).
/// </summary>
/// <remarks>
/// This option supported by LPRng only. For LPR protocol, it is forced
/// to 0 (lowest priority). For Direct protocol, it is unused.
/// </remarks>
public Int32 Priority
{
get { return priority_; }
set { priority_ = value; }
}
/// <summary>
/// Get collection of data files included in job.
/// </summary>
public List<LprDataFile> DataFiles
{
get { return dataFiles_; }
}
#endregion
#region Housekeeping Values
/// <summary>
/// Get Job Number used to identify the print job.
/// </summary>
public Int32 JobNumber
{
get { return jobNumber_; }
}
/// <summary>
/// Get Job Size in bytes of local files to transfer.
/// </summary>
public Int64 JobSize
{
get { jobSizeTotal_ = GetJobSize(); return jobSizeTotal_; }
}
#endregion
#region Job Status and Results
/// <summary>
/// Get or set flag indicating that a connection to server is active.
/// </summary>
public Boolean IsConnected
{
get { return isConnected_; }
private set { isConnected_ = value; }
}
/// <summary>
/// Get flag indicating if job is Completed. True if job submission has
/// been completed; otherwise false.
/// </summary>
public Boolean IsCompleted
{
get { return endJob_.WaitOne(0, false); }
private set
{
if (value == true)
{
// Set the end-of-job signal.
endJob_.Set();
}
else
{
// Clear the end-of-job signal.
endJob_.Reset();
}
}
}
/// <summary>
/// Get or set Last Exception encountered when an error occurs.
/// </summary>
public Exception LastException
{
get { return lastException_; }
private set { lastException_ = value; }
}
#endregion
#endregion
#region Constructors
/// <summary>
/// Initialize a new instance of the LprJob class.
/// </summary>
public LprJob()
{
// Default print job.
jobNumber_ = GetNextJobNumber();
printerName_ = DefaultPrinterName;
serverName_ = DefaultServerName;
serverPort_ = DefaultServerPort;
protocol_ = DefaultProtocol;
serverEncoding_ = new ASCIIEncoding();
userName_ = "MobileLPR";
hostName_ = "WindowsCE";
}
/// <summary>
/// Initialize a new instance of the LprJob class.
/// </summary>
/// <param name="pathname">Pathname of local data file to print in raw format.</param>
public LprJob(String pathname)
: this()
{
// Specify file to print.
AddDataFile(pathname);
}
/// <summary>
/// Initialize a new instance of the LprJob class.
/// </summary>
/// <param name="pathname">Pathname of local data file to print in raw format.</param>
/// <param name="printerName">Name of printer queue to use.</param>
public LprJob(String pathname, String printerName)
: this(pathname)
{
// Specify file to print and name of printer queue.
printerName_ = printerName;
}
#endregion
#region Static Methods
/// <summary>
/// Get the next available Job Number.
/// </summary>
/// <returns>Job number to use for new job.</returns>
public static int GetNextJobNumber()
{
int jobNumber;
lock (typeof(LprJob))
{
jobNumber = nextJobNumber_++;
if (nextJobNumber_ >= MAX_JOB_COUNT)
{
nextJobNumber_ = 0;
}
}
return jobNumber;
} // GetNextJobNumber()
/// <summary>
/// Get size of a named file.
/// </summary>
/// <param name="pathname">Pathname of file.</param>
/// <returns>Size of file in bytes.</returns>
public static Int64 GetFileSize(String pathname)
{
Int64 size = 0;
if (File.Exists(pathname))
{
FileInfo f = new FileInfo(pathname);
size = f.Length;
}
return size;
}
#endregion
#region Public Methods
/// <summary>
/// Set printer parameters from a uniform resource indicator string.
/// </summary>
/// <param name="destination">URI string identifying destination printer.</param>
/// <remarks>
/// A URI string of the following form can be used to identify the print
/// server parameters:
/// protocol://[user@]hostname[:port][/printer]
/// </remarks>
/// <exception cref="UriException">when a URI is missing mandatory
/// components or is otherwise formatted incorrectly.</exception>
public void SetPrinterURI(String destination)
{
LprDestination dest = new LprDestination(destination);
Protocol = dest.Protocol;
if (!String.IsNullOrEmpty(dest.UserName))
{
UserName = dest.UserName;
}
ServerName = dest.ServerName;
ServerPort = dest.ServerPort;
if (!String.IsNullOrEmpty(dest.PrinterName))
{
PrinterName = dest.PrinterName;
}
} // SetPrinterURI(String destination)
/// <summary>
/// Add a data file to be printed in Literal (raw) format.
/// </summary>
/// <param name="pathname">Local pathname of file to print.</param>
/// <returns>True if data file was added; false if error.</returns>
public Boolean AddDataFile(String pathname)
{
return AddDataFile(pathname, LprDataFormats.Literal);
} // AddDataFile(String pathname)
/// <summary>
/// Add a data file to be printed in a specific format.
/// </summary>
/// <param name="pathname">Local pathname of file to print.</param>
/// <param name="format">Format of data file contents.</param>
/// <returns>True if data file was added; false if error.</returns>
/// <remarks>
/// The LPR and LPRng protocols limit the number of data files per job
/// to a maximum of 52, the number of index letters available. This
/// method will not add more than this many files. Though not specified,
/// some LPR servers can handle only one file per job.
/// </remarks>
public Boolean AddDataFile(String pathname, LprDataFormats format)
{
LprDataFile file = new LprDataFile(pathname, format);
if ((Protocol == LprProtocols.LPR ||
Protocol == LprProtocols.LPRng) &&
dataFiles_.Count >= LPR_INDEXNAMES.Length)
{
// Prevent attaching more files than the protocol allows.
return false;
}
dataFiles_.Add(file);
return true;
} // AddDataFile(String pathname, LprDataFormats format)
/// <summary>
/// Transfer the print job to the print server for processing.
/// </summary>
/// <remarks>
/// This is an asynchronous call. Either subscribe to the <see cref="LprJobCompleted"/>
/// event, call the <see cref="WaitForCompletion"/> method, or check the
/// <see cref="IsCompleted"/> property.
/// </remarks>
/// <exception cref="LprException">when </exception>
public void SubmitJob()
{
// Start the print job transfer.
StartJobTransfer();
} // SubmitJob()
/// <summary>
/// Wait for completion of a print job submission.
/// </summary>
public void WaitForCompletion()
{
// Wait for the End of Job signal.
endJob_.WaitOne();
} // WaitForCompletion()
/// <summary>
/// Start printing jobs waiting in queue if not already running.
/// </summary>
/// <returns>True if command was sent successfully; false if error.</returns>
public Boolean PrintWaitingJobs()
{
Boolean result = false;
try
{
// Clear the End-of-Job signal.
IsCompleted = false;
// Clear exception holder.
LastException = null;
// Establish connection to print server.
StartServerCommand();
result = SendPrintWaitingJobsCommand();
}
catch (Exception ex)
{
// Capture error.
LastException = ex;
}
finally
{
// Terminate the Receive Job command.
EndServerCommand();
// Set the End-of-Job signal.
IsCompleted = true;
}
return result;
} // PrintWaitingJobs()
/// <summary>
/// Get short or long status listing of queued jobs.
/// </summary>
/// <param name="longListing">True for long listing; false for short.</param>
/// <param name="joblist">List identifying specific users or jobs.</param>
/// <returns>String containing response from server.</returns>
/// <remarks>
/// Response lines from server are terminated by Unix newlines, for
/// standards-compliant LPR/LPRng servers.
/// </remarks>
public String GetQueueStatus(Boolean longListing, String[] joblist)
{
String response = String.Empty;
try
{
// Clear the End-of-Job signal.
IsCompleted = false;
// Clear exception holder.
LastException = null;
// Establish connection to print server.
if (!StartServerCommand())
{
throw new LprException("unable to establish connection");
}
if (SendQueueStatusCommand(longListing, joblist))
{
// Capture responses from print server.
response = GetResponseText();
}
}
catch (Exception ex)
{
// Capture error.
LastException = ex;
}
finally
{
// Terminate the Receive Job command.
EndServerCommand();
// Set the End-of-Job signal.
IsCompleted = true;
}
// Return response string retrieved from server.
return response;
} // GetQueueStatus(Boolean longListing, String[] joblist)
/// <summary>
/// Remove the currently running print job or list of jobs.
/// </summary>
/// <param name="joblist">List of users or jobs to remove; currently active job
/// if none listed.</param>
public Boolean RemoveJobs(String[] joblist)
{
Boolean result = false;
try
{
// Clear the End-of-Job signal.
IsCompleted = false;
// Clear exception holder.
LastException = null;
// Establish connection to print server.
StartServerCommand();
result = SendRemoveJobsCommand(joblist);
}
catch (Exception ex)
{
// Capture error.
LastException = ex;
}
finally
{
// Terminate the Receive Job command.
EndServerCommand();
// Set the End-of-Job signal.
IsCompleted = true;
}
return result;
} // RemoveJobs(String[] joblist)
#endregion
#region Private Methods
/// <summary>
/// Start transferring the job to the print server.
/// </summary>
private void StartJobTransfer()
{
// Clear the End-of-Job signal.
IsCompleted = false;
// Launch a background thread for transfer operations.
ThreadPool.QueueUserWorkItem(new WaitCallback(SendJobFiles));
} // StartJobTransfer()
/// <summary>
/// Perform cleanup tasks after the print job has been submitted.
/// </summary>
private void EndJobTransfer()
{
if (clientConnection_ != null)
{
// Close connection to the print server.
if (IsConnected)
{
// Socket must be closed independently.
clientConnection_.GetStream().Close();
IsConnected = false;
}
clientConnection_.Close();
clientConnection_ = null;
}
if (LprJobCompleted != null)
{
// Notify subscribers the submission has ended.
LprJobProgressEventArgs args = new LprJobProgressEventArgs();
args.Total = jobSizeTotal_;
args.Used = jobSizeUsed_;
args.Command = ServerCommands.SubmitJob;
LprJobCompleted(this, args);
}
// Set the End-of-Job signal.
IsCompleted = true;
} // EndJobTransfer()
/// <summary>
/// Send all job files to the server using the specified protocol.
/// </summary>
/// <remarks>
/// To submit a job to the LPR/LPRng server, the following steps are
/// implemented:
/// 1. Open a new connection to the server.
/// 2. Send control and data files.
/// 3. Close the server connection.
///
/// Steps to send a control file (either first or last file sent), use:
/// 1. Send a Receive Control File subcommand with length of file.
/// 2. Receive acknowledgement from server.
/// 3. Send contents of Control file.
/// 4. Send zero octet (NUL).
/// 5. Receive acknowledgement from server.
///
/// To send a data file, use:
/// 1. Send a Receive Data File subcommand with length of file.
/// 2. Receive acknowledgement from server.
/// 3. Send contents of Data file.
/// 4. Send zero octet (NUL).
/// 5. Receive acknowledgement from server.
///
/// For LPR protocol, copies are implemented by sending the print job
/// multiple times. For LPRng protocol, copies are implemented by
/// sending multiple copies of data files for a single print job.
/// For Direct protocol, the data files are sent multiple times.
/// </remarks>
private void SendJobFiles(Object state)
{
Boolean isDone = false;
Boolean needControlFile = false;
try
{
// Clear exception holder.
LastException = null;
// Determine if we need a control file.
controlFilename_ = String.Empty;
needControlFile = (Protocol == LprProtocols.LPR ||
Protocol == LprProtocols.LPRng);
if (needControlFile)
{
// Create the control file to send to the server.
CreateControlFile();
}
// Calculate job size, including the control file.
jobSizeTotal_ = GetJobSize();
jobSizeUsed_ = 0;
if (LprJobStarted != null)
{
// Notify subscribers the submission has begun.
LprJobProgressEventArgs args = new LprJobProgressEventArgs();
args.Total = jobSizeTotal_;
args.Used = jobSizeUsed_;
args.Command = ServerCommands.SubmitJob;
LprJobStarted(this, args);
}
// For LPR protocol, implement multiple copies by sending the
// entire print job multiple times.
for (int lprCopies = 0;
lprCopies < (Protocol == LprProtocols.LPR ? Copies : 1);
++lprCopies)
{
// Restart next print job.
sentControlFile_ = false;
// Establish connection to print server.
StartServerCommand();
SendReceiveJobCommand();
dataFileIndex_ = 0;
copyIndex_ = 0;
if (needControlFile &&
sendControlFileFirst_ &&
!sentControlFile_)
{
// Send control file before data files.
if (!SendControlFile())
{
isDone = true;
}
}
// Send multiple copies of data files to be printed.
for (dataFileIndex_ = 0;
dataFileIndex_ < DataFiles.Count && !isDone;
++dataFileIndex_)
{
// Send specified number of copies of each data file.
// For LPR protocol, send only a single copy.
for (copyIndex_ = 0;
!isDone &&
copyIndex_ < (Protocol == LprProtocols.LPR ? 1 : Copies);
++copyIndex_)
{
// Send next copy of data file.
if (!SendDataFile())
{
isDone = true;
}
} // for (copyIndex_ = 0;
} // for (dataFileIndex_ = 0;
if (needControlFile &&
!sentControlFile_ &&
!isDone)
{
// Send control file after data files.
if (!SendControlFile())
{
isDone = true;
}
}
// Terminate the Receive Job command.
EndServerCommand();
} // for (int extCopies = 0;
}
catch (Exception ex)
{
// Capture error.
LastException = ex;
}
finally
{
// Terminate the Receive Job command.
EndServerCommand();
// Delete the control file.
RemoveControlFile();
// Perform end-of-job cleanup processing.
EndJobTransfer();
}
} // SendJobFiles()
/// <summary>
/// Create the control file for the job.
/// </summary>
/// <returns>Pathname of control file created.</returns>
private String CreateControlFile()
{
controlFilename_ = Path.GetTempFileName();
StreamWriter file = new StreamWriter(controlFilename_, false);
file.Write(String.Format("H{0}" + LPR_NEWLINE, HostName));
file.Write(String.Format("P{0}" + LPR_NEWLINE, UserName));
if (BannerEnabled || MailEnabled)
{
if (BannerEnabled)
{
// Include banner data.
file.Write(String.Format("C{0}" + LPR_NEWLINE, BannerJobClass));
file.Write(String.Format("L{0}" + LPR_NEWLINE, UserName));
}
if (MailEnabled)
{
// Include email address.
file.Write(String.Format("M{0}" + LPR_NEWLINE, MailTo));
}
}
// Include Job Name for display as document name when listing
// print queue status.
file.Write(String.Format("J{0}" + LPR_NEWLINE, JobName));
file.Write(String.Format("T{0}" + LPR_NEWLINE, PageTitle));
file.Write(String.Format("I{0}" + LPR_NEWLINE, IndentSize));
file.Write(String.Format("W{0}" + LPR_NEWLINE, PageWidth));
// Include all data files that are part of this job.
for (dataFileIndex_ = 0; dataFileIndex_ < dataFiles_.Count; ++dataFileIndex_)
{
for (copyIndex_ = 0;
copyIndex_ < (Protocol == LprProtocols.LPR ? 1 : Copies);
++copyIndex_)
{
String name = MakeDataFilename(dataFileIndex_, copyIndex_);
file.Write(String.Format("{0}{1}" + LPR_NEWLINE,
DataFiles[dataFileIndex_].FormatCommand,
name));
file.Write(String.Format("N{0}" + LPR_NEWLINE,
DataFiles[dataFileIndex_].Pathname));
file.Write(String.Format("U{0}" + LPR_NEWLINE, name));
}
}
if (Protocol == LprProtocols.LPRng)
{
// LPRng-specific options.
// Add unique job identifier.
file.Write(String.Format("A{0}@{1}+{2}", UserName, HostName, JobNumber));
// Add name of original destination printer queue.
file.Write(String.Format("Q{0}" + LPR_NEWLINE, PrinterName));
}
file.Close();
return controlFilename_;
} // CreateControlFile()
/// <summary>
/// Delete the control file.
/// </summary>
private void RemoveControlFile()
{
if (!String.IsNullOrEmpty(controlFilename_) &&
File.Exists(controlFilename_))
{
File.Delete(controlFilename_);
}
} // RemoveControlFile()
/// <summary>
/// Generate the name of the control file on the server.
/// </summary>
/// <param name="priority">Priority of job on server.</param>
/// <returns>String containing the control filename.</returns>
/// <exception cref="ArgumentException">Thrown if priority is not between 0 and 25, inclusive.</exception>
/// <remarks>
/// For LPRng servers, the priority (0-25) identifies the job's
/// importance relative to other jobs in the queue. A higher priority
/// number suggests that the server print the job before others with a
/// lower priority. For LPR servers, priority is always 0 (lowest).
/// </remarks>
private String MakeControlFilename(int priority)
{
if (priority < 0 || priority >= 26)
{
throw new ArgumentException("priority must be between 0 and 25");
}
if (Protocol == LprProtocols.LPR)
{
priority = 0;
}
String name = String.Format("cf{0}{1:d3}{2}",
new object[] {
LPR_INDEXNAMES.Substring(priority, 1),
jobNumber_,
HostName
});
return name;
} // MakeControlFileName(int priority)
/// <summary>
/// Generate the name of the data file on the server.
/// </summary>
/// <param name="index">Index of data file to use for name.</param>
/// <param name="copy">Copy index of file to be printed.</param>
/// <returns>String containing the data filename.</returns>
/// <remarks>
/// If the second or later copy of a file is being sent, a suffix of
/// ".C" and the copy number is appended to the filename. This follows
/// the convention used by LPRng.
/// </remarks>
private String MakeDataFilename(int index, int copy)
{
String name = String.Format("df{0}{1:d3}{2}{3}",
new object[] {
LPR_INDEXNAMES.Substring(index, 1),
jobNumber_,
HostName,
(copy > 0) ? String.Format(".C{0}", copy + 1) : ""
});
return name;
} // MakeDataFileName(int index, int copy)
/// <summary>
/// Send Print Any Waiting Jobs command to the server.
/// </summary>
/// <returns>True if the command was sent successfully; false if error.</returns>
private Boolean SendPrintWaitingJobsCommand()
{
Boolean result = false;
if (Protocol == LprProtocols.Direct)
{
// Simply return when sending files directly.
return true;
}
try
{
// Format and send command to the print server.
String args = String.Format("{0}", PrinterName);
SendCommand((int)ServerCommands.PrintWaitingJobs, args);
result = true;
}
catch (Exception ex)
{
// Capture error.
LastException = ex;
}
return result;
} // SendPrintWaitingJobsCommand()
/// <summary>
/// Send initial Receive Job command to the server.
/// </summary>
/// <returns>True if the command was sent successfully; false if error.</returns>
private Boolean SendReceiveJobCommand()
{
Boolean result = false;
if (Protocol == LprProtocols.Direct)
{
// Simply return when sending files directly.
return true;
}
try
{
// Format and send command to the print server.
String args = String.Format("{0}", PrinterName);
SendCommand((int)ServerCommands.SubmitJob, args);
result = true;
}
catch (Exception ex)
{
// Capture error.
LastException = ex;
}
return result;
} // SendReceiveJobCommand()
/// <summary>
/// Send Short or Long Queue Status command to the server.
/// </summary>
/// <param name="joblist">List of users or job numbers to report status.</param>
/// <returns>True if the command was sent successfully; false if error.</returns>
private Boolean SendQueueStatusCommand(Boolean longListing, String[] joblist)
{
Boolean result = false;
if (Protocol == LprProtocols.Direct)
{
// Simply return when sending files directly.
return true;
}
try
{
// Format and send command to the print server.
StringBuilder args = new StringBuilder();
args.Append(String.Format("{0}", PrinterName));
if (joblist != null && joblist.Length > 0)
{
// Append list of users and jobs as operands.
foreach (String s in joblist)
{
if (!String.IsNullOrEmpty(s))
{
// Separate operands by whitespace.
args.Append(" ").Append(s);
}
}
}
SendCommand((int)(longListing ?
ServerCommands.LongQueueStatus :
ServerCommands.ShortQueueStatus),
args.ToString());
result = true;
}
catch (Exception ex)
{
// Capture error.
LastException = ex;
}
return result;
} // SendQueueStatusCommand(Boolean longListing, String[] joblist)
/// <summary>
/// Send Remove Jobs command to the server.
/// </summary>
/// <param name="joblist">list of users and job numbers to remove from queue.</param>
/// <returns>True if the command was sent successfully; false if error.</returns>
private Boolean SendRemoveJobsCommand(String[] joblist)
{
Boolean result = false;
if (Protocol == LprProtocols.Direct)
{
// Simply return when sending files directly.
return true;
}
try
{
// Format and send command to the print server.
StringBuilder args = new StringBuilder();
args.Append(String.Format("{0} {1}", PrinterName, UserName));
if (joblist != null && joblist.Length > 0)
{
// Append list of users and jobs as operands.
foreach (String s in joblist)
{
if (!String.IsNullOrEmpty(s))
{
// Separate operands by whitespace.
args.Append(" ").Append(s);
}
}
}
SendCommand((int)ServerCommands.RemoveJobs,
args.ToString());
result = true;
}
catch (Exception ex)
{
// Capture error.
LastException = ex;
}
return result;
} // SendRemoveJobsCommand(String[] joblist)
/// <summary>
/// Prepare to send a command to the server.
/// </summary>
/// <returns>True if the command session was opened successfully; false if error.</returns>
/// <exception cref="SocketException">Thrown if a connection error occurs.</exception>
private Boolean StartServerCommand()
{
Boolean result = false;
// Open a new connection to the server.
result = OpenServerConnection();
return result;
} // StartServerCommand()
/// <summary>
/// Perform post-command processing.
/// </summary>
private void EndServerCommand()
{
CloseServerConnection();
} // EndServerCommand()
/// <summary>
/// Open the network connection to the server.
/// </summary>
private Boolean OpenServerConnection()
{
const int WSAEINTR = 10004;
const int WSAENETDOWN = 10050;
const int WSAENETUNREACH = 10051;
const int WSAETIMEDOUT = 10060;
const int WSAECONNREFUSED = 10061;
const int WSAEHOSTDOWN = 10064;
const int WSAEHOSTUNREACH = 10065;
const int WSAHOST_NOT_FOUND = 11001;
Boolean isDone = false;
int startPort = 0;
int endPort = 0;
// Set local port range to try for connection.
switch (Protocol)
{
case LprProtocols.LPR:
// LPR requires port numbers 721-731.
startPort = 721;
endPort = 731;
break;
case LprProtocols.LPRng:
// LPRng uses any reserved port.
startPort = 512;
endPort = 1023;
break;
case LprProtocols.Direct:
default:
// Other protocols use any assigned port.
break;
} // switch (Protocol)
// Originate connection, using an available prescribed local port.
for (int localPort = startPort; localPort <= endPort; ++localPort)
{
try
{
// Create a new connection object.
IPEndPoint ipLocalEndPoint = new IPEndPoint(IPAddress.Any, localPort);
clientConnection_ = new TcpClient(ipLocalEndPoint);
// Set socket options.
clientConnection_.NoDelay = true;
clientConnection_.Client.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress,
1);
// Timeouts are unsupported in the .NET Compact Framework.
//clientConnection_.ReceiveTimeout = LPR_RECV_TIMEOUT;
//clientConnection_.SendTimeout = LPR_SEND_TIMEOUT;
// Open connection to print server.
clientConnection_.Connect(ServerName, ServerPort);
isConnected_ = isDone = true;
}
catch (SocketException ex)
{
// Throw fatal exceptions for caller to handle.
switch (ex.ErrorCode)
{
case WSAEINTR:
case WSAENETDOWN:
case WSAETIMEDOUT:
case WSAENETUNREACH:
case WSAECONNREFUSED:
case WSAEHOSTDOWN:
case WSAEHOSTUNREACH:
case WSAHOST_NOT_FOUND:
throw ex;
}
}
if (isDone)
{
break;
}
}
return isConnected_;
} // OpenServerConnection()
/// <summary>
/// Close the network connection to the server.
/// </summary>
private void CloseServerConnection()
{
if (clientConnection_ != null)
{
// Socket must be closed independently.
if (IsConnected)
{
clientConnection_.GetStream().Close();
IsConnected = false;
}
clientConnection_.Close();
clientConnection_ = null;
}
} // CloseServerConnection()
/// <summary>
/// Send a command to the print server.
/// </summary>
/// <param name="code">Command code to execute.</param>
/// <param name="args">String containing command arguments.</param>
/// <remarks>
/// This method automatically adds the required newline character.
/// </remarks>
private void SendCommand(int commandCode, String args)
{
Byte[] buffer = new byte[200];
int byteCount = 0;
buffer[0] = (byte)commandCode;
args += LPR_NEWLINE;
byteCount = serverEncoding_.GetBytes(args, 0, args.Length, buffer, 1);
clientConnection_.GetStream().Write(buffer, 0, byteCount + 1);
} // SendCommand(ServerCommands code, String command)
/// <summary>
/// Send the job control file to the server.
/// </summary>
private Boolean SendControlFile()
{
Boolean result = false;
if (Protocol == LprProtocols.Direct)
{
return true;
}
// Generate name of file and calculate size for transfer.
LprTransferState state = new LprTransferState(controlFilename_);
state.serverFilename = MakeControlFilename(Priority);
state.isBinary = false;
state.sizeTotal = CalculateServerFileSize(state);
if (SendControlFileSubCommand(state))
{
// Send file contents.
result = SendFileContents(state);
sentControlFile_ = result;
}
return result;
} // SendControlFile()
/// <summary>
/// Send Submit Job subcommand to transfer control file contents.
/// </summary>
/// <param name="state">State object describing control file.</param>
/// <returns>True if server accepted command; false if error.</returns>
private Boolean SendControlFileSubCommand(LprTransferState state)
{
Boolean result = false;
int acknowledgement = 0;
// Format and send Receive Control File subcommand.
String command = String.Format("{0} {1}",
state.sizeTotal, state.serverFilename);
SendCommand((int)SubmitJobSubCommands.SubmitControlFile, command);
// Get acknowledgement from server.
acknowledgement = GetResponseCode();
result = (acknowledgement == 0);
if (acknowledgement != 0)
{
// Save information about the error.
StringBuilder message = new StringBuilder(String.Format(
"lpr error {0} sending control file subcommand",
acknowledgement));
// Add rejection code interpretation per LPRng.
String rejection = GetLprngErrorMessage(acknowledgement);
if (!String.IsNullOrEmpty(rejection))
{
message.Append(" (").Append(rejection).Append(")");
}
LprException ex = new LprException(message.ToString());
ex.ErrorCode = acknowledgement;
LastException = ex;
} // if (acknowledgement != 0)
return result;
} // SendControlFileSubCommand(LprTransferState state)
/// <summary>
/// Get command response code from server.
/// </summary>
/// <returns>Integer value of response byte from server.</returns>
private int GetResponseCode()
{
return clientConnection_.GetStream().ReadByte();
} // GetResponseCode()
/// <summary>
/// Get textual response string from server.
/// </summary>
/// <returns>Raw text response from server.</returns>
private String GetResponseText()
{
StringBuilder text = new StringBuilder();
while (clientConnection_.GetStream().CanRead)
{
Byte[] bytes = new Byte[1024];
int bytesRead = clientConnection_.GetStream().Read(bytes, 0, bytes.Length);
if (bytesRead == 0)
{
break;
}
text.Append(serverEncoding_.GetChars(bytes, 0, bytesRead));
} // while (clientConnection_.GetStream().CanRead)
return text.ToString();
} // GetResponseText()
/// <summary>
/// Translate rejection code into an LPRng error message.
/// </summary>
/// <param name="error">Integer rejection code.</param>
/// <returns>String containing error message; empty if not matched.</returns>
private String GetLprngErrorMessage(int error)
{
switch (error)
{
case 1: return "queue not accepting jobs or invalid queue name";
case 2: return "queue temporarily full, retry later";
case 3: return "bad job format, do not retry";
}
return "";
}
/// <summary>
/// Send a data file to the server.
/// </summary>
private Boolean SendDataFile()
{
Boolean result = false;
LprTransferState state = new LprTransferState(DataFiles[dataFileIndex_].Pathname);
// Generate name of file and calculate size for transfer.
state.serverFilename = MakeDataFilename(dataFileIndex_, copyIndex_);
state.isBinary = DataFiles[dataFileIndex_].IsBinary;
state.sizeTotal = CalculateServerFileSize(state);
if (Protocol == LprProtocols.Direct ||
SendDataFileSubCommand(state))
{
// Send file contents.
result = SendFileContents(state);
}
return result;
} // SendDataFile()
/// <summary>
/// Send Submit Job subcommand to transfer data file contents.
/// </summary>
/// <param name="state">State object describing data file.</param>
/// <returns>True if server accepted command; false if error.</returns>
private Boolean SendDataFileSubCommand(LprTransferState state)
{
Boolean result = false;
int acknowledgement = 0;
// Format and send Receive Data File subcommand.
String command = String.Format("{0} {1}",
state.sizeTotal, state.serverFilename);
SendCommand((int)SubmitJobSubCommands.SubmitDataFile, command);
// Get acknowledgement from server.
acknowledgement = GetResponseCode();
result = (acknowledgement == 0);
if (acknowledgement != 0)
{
// Save information about the error.
StringBuilder message = new StringBuilder(String.Format(
"lpr error {0} sending data file subcommand",
acknowledgement));
// Add rejection code interpretation per LPRng.
String rejection = GetLprngErrorMessage(acknowledgement);
if (!String.IsNullOrEmpty(rejection))
{
message.Append(" (").Append(rejection).Append(")");
}
LprException ex = new LprException(message.ToString());
ex.ErrorCode = acknowledgement;
LastException = ex;
} // (acknowledgement != 0)
return result;
} // SendDataFileSubCommand(LprTransferState state)
/// <summary>
/// Calculate file size as sent to print server.
/// </summary>
/// <param name="state">Object containing information of file being sent.</param>
/// <returns>Size of transferred file in bytes.</returns>
private Int64 CalculateServerFileSize(LprTransferState state)
{
Int64 result = 0;
if (state.isBinary)
{
// If transferring binary, reading file is unnecessary.
return GetFileSize(state.pathname);
}
// Open file to be transferred.
state.stream = new FileStream(state.pathname,
FileMode.Open, FileAccess.Read,
FileShare.None, LPR_BUFSIZE, false);
state.buffer = new byte[LPR_BUFSIZE];
Encoding fileEncoding = null;
// Read file a block at a time.
int bytesRead = 0;
for ( ; ; )
{
bytesRead = state.stream.Read(state.buffer, 0, LPR_BUFSIZE);
if (bytesRead == 0)
{
// Reached end of current file.
break;
}
if (fileEncoding == null)
{
// Determine encoding for proper decoding of text.
if (bytesRead >= 2 &&
state.buffer[0] == 0xFE && state.buffer[1] == 0xFF)
{
// Read UTF-16 Big-endian file.
fileEncoding = new UnicodeEncoding(true, true);
}
else if (bytesRead >= 2 &&
state.buffer[0] == 0xFF && state.buffer[1] == 0xFE)
{
// Read UTF-16 Little-endian file.
fileEncoding = new UnicodeEncoding(false, true);
}
else
{
// Read UTF-8 file.
fileEncoding = new UTF8Encoding();
}
} // if (encoding == null)
// Translate text into ASCII.
char[] textin = fileEncoding.GetChars(state.buffer, 0, bytesRead);
result += serverEncoding_.GetByteCount(textin);
} // for ( ; ; )
state.stream.Close();
state = null;
return result;
} // CalculateServerFileSize(LprTransferState state)
/// <summary>
/// Send contents of a file to the server.
/// </summary>
/// <param name="state">Object containing information of file being sent.</param>
/// <returns>True if successful; false if error.</returns>
/// <remarks>
/// Read the file identified in the "state" parameter a block at a time
/// and send it to the print server over the current connection.
/// </remarks>
private Boolean SendFileContents(LprTransferState state)
{
Boolean result = false;
// Open file to be transferred.
state.stream = new FileStream(state.pathname,
FileMode.Open, FileAccess.Read,
FileShare.None, LPR_BUFSIZE, false);
state.buffer = new byte[LPR_BUFSIZE];
Encoding fileEncoding = null;
// Read and send file a block at a time.
int bytesRead = 0;
byte[] sendBuffer = null;
int sendCount = 0;
for ( ; ; )
{
bytesRead = state.stream.Read(state.buffer, 0, LPR_BUFSIZE);
if (bytesRead == 0)
{
// Reached end of current file.
break;
}
if (state.isBinary)
{
// Use raw data from file.
sendBuffer = state.buffer;
sendCount = bytesRead;
}
else
{
// Translate text from file encoding into ASCII.
if (fileEncoding == null)
{
// Determine encoding for proper decoding of text.
if (bytesRead >= 2 &&
state.buffer[0] == 0xFE && state.buffer[1] == 0xFF)
{
// Read UTF-16 Big-endian file.
fileEncoding = new UnicodeEncoding(true, true);
}
else if (bytesRead >= 2 &&
state.buffer[0] == 0xFF && state.buffer[1] == 0xFE)
{
// Read UTF-16 Little-endian file.
fileEncoding = new UnicodeEncoding(false, true);
}
else
{
// Read UTF-8 file.
fileEncoding = new UTF8Encoding();
}
} // if (encoding == null)
// Decode text and encode into ASCII.
char[] textin = fileEncoding.GetChars(state.buffer, 0, bytesRead);
sendBuffer = serverEncoding_.GetBytes(textin);
sendCount = sendBuffer.Length;
} // if (state.isBinary) ... else
// Transmit buffer to print server.
clientConnection_.GetStream().Write(sendBuffer, 0, sendCount);
// Update transfer counters.
state.sizeUsed += bytesRead;
jobSizeUsed_ += bytesRead;
if (LprJobProgressChanged != null)
{
// Notify subscribers of file transfer progress.
LprJobProgressEventArgs args = new LprJobProgressEventArgs();
args.Pathname = state.pathname;
args.ServerFilename = state.serverFilename;
args.Total = jobSizeTotal_;
args.Used = jobSizeUsed_;
args.Command = ServerCommands.SubmitJob;
LprJobProgressChanged(this, args);
}
} // for ( ; ; )
// Perform final progress notification.
if (LprJobProgressChanged != null)
{
// Notify subscribers of file transfer progress.
LprJobProgressEventArgs args = new LprJobProgressEventArgs();
args.Pathname = state.pathname;
args.ServerFilename = state.serverFilename;
args.Total = jobSizeTotal_;
args.Used = jobSizeUsed_;
args.Command = ServerCommands.SubmitJob;
LprJobProgressChanged(this, args);
}
if (Protocol == LprProtocols.LPR ||
Protocol == LprProtocols.LPRng)
{
// LPR and LPRng require terminating null and acknowledgement.
// Send terminating zero (null) octet.
state.buffer[0] = (byte)0;
sendBuffer = state.buffer;
sendCount = 1;
clientConnection_.GetStream().Write(sendBuffer, 0, sendCount);
// Get acknowledgement from server.
int acknowledgement = GetResponseCode();
result = (acknowledgement == 0);
if (acknowledgement != 0)
{
// Save information about the error.
LprException ex = new LprException(
String.Format("lpr error {0} sending file data",
acknowledgement));
ex.ErrorCode = acknowledgement;
ex.Pathname = state.pathname;
LastException = ex;
}
}
else
{
// Direct protocol requires no additional data.
result = true;
}
// Close file after completing transfer.
state.stream.Close();
state = null;
return result;
} // SendFileContents(LprTransferState state)
/// <summary>
/// Calculate size of all files to be transferred.
/// </summary>
/// <returns>Number of bytes in all files.</returns>
/// <remarks>
/// For LPR protocol, copies are implemented by sending the print job
/// multiple times, so the control file is also sent multiple times.
/// For LPRng protocol, copies are implemented by sending data files
/// multiple times within one print job, so control is sent only once.
/// For Direct protocol, data files are sent multiple times.
/// </remarks>
private Int64 GetJobSize()
{
Int64 size = 0;
// Sum sizes of all files to be sent.
if (!String.IsNullOrEmpty(controlFilename_))
{
size += GetFileSize(controlFilename_) *
(Protocol == LprProtocols.LPR ? Copies : 1);
}
for (int i = 0; i < DataFiles.Count; ++i)
{
size += GetFileSize(DataFiles[i].Pathname) * Copies;
}
return size;
} // GetJobSize()
#endregion
}
#region Exceptions
public class LprException : Exception
{
public int ErrorCode = 0;
public String Pathname = "";
public LprException() { }
public LprException(String message)
: base(message)
{
}
}
#endregion
}