#
# gpcupsbackend.rb
# GoPrint backend for CUPS 1.2+ integration.
#
# Copyright (c) 2003-2014 GoPrint Systems, Inc.  All rights reserved.
#

require GS4Home + "lib/gpcupsconfig"
require GS4Home + "lib/gpcupslogger"
require 'socket'
require 'timeout'
require 'tempfile'

CUPS_BACKEND_HOLD = 3
CUPS_BACKEND_PRINT = 0
CUPS_BACKEND_CANCEL = 5

class GoPrintCUPSBackend
	include GoPrintCUPSLogger
	
	def initialize (config)
		@config = config
		super
	end

	# Determine what devices the given backend manages and report that GoPrint manages a similar device
	def manageAsGoprint(backend)
		# Create an array of managed devices for the given backend
		managedOutput = Array.new
		# Don't attempt to manage these backend names
		doNotManage = [ @config.commandName, "hal", "snmp" ]
		if !doNotManage.include?(backend)
			# Only attempt to manage executable, non-directories
			backendFileStat = File.stat(@config.runPath + backend)
			# if executable and not a directory then it's a backend
			if (backendFileStat.executable? && !backendFileStat.directory?)
				# Execute the backend to manage
				backendProcess = IO.popen(@config.runPath + backend, "r")
				# Parse each of the lines the backend generates
				while line = backendProcess.gets
					next if line.empty?
					# Lines starting with INFO: or ERROR: can be suppressed when enumerating backends.
					# They will show up when that backend is used individually so no need to include them
					# when saying what backends goprint finds.  If this ever changes then they should be
					# sent to stdout in the same sequence as normal output (e.g. managedOutput.push(line)).
					next if line =~ /^INFO: /
					next if line =~ /^ERROR: /
					# Parse the output from the backend.  The first two parameters have no quotes,
					# the remaining parameters (of which there may be two or three but typically just two)
					# are surrounded by quotes.  Examples of backend output follow:
					#  direct bluetooth "Unknown" "Bluetooth Printer (bluetooth)"
					#  network epsontcpip "Unknown" "EPSON TCPIP"
					#  network hpip "Unknown" "Network Printer (HP IP)"
					#  network hpip://10.0.2.100:HP%20LaserJet%204100%20Series%20/1:0.1.e6.1.f9.1d "HP LaserJet 4100 Series " "NPI01F91D"
					#  network smb "Unknown" "Windows Printer via SAMBA"
					#  network socket "Unknown" "AppSocket/HP JetDirect"
					# get the first two parameters, leaving the remaining in parsed[2]
					parsed = line.scan(%r{([^\s]+)\s+([^\s]+)\s+(.*)}).flatten
					# Make sure the output is of the expected form.  If it is, there should be
					# a third parameter segment with the quoted parameters in it.
					if (parsed[2])
						# Parse the third parameter segment as each element is surrounded by quotes
						endParse = parsed[2].split(%r{\"*\s*\"})
						# Clear out any empty strings
						endParse.collect! {|x| if !x.empty? then x end}
						# Clear out any nil strings
						endParse.compact!

						# form the goprint-managed device URI as goprint:backend
						managedBackendCommandName = @config.commandName + ":" + parsed[1]
						# The third parameter (endParse[0]) will be 'Unknown' or 'Nothing' if
						# it doesn't represent a real device, in which case we just return that.
						# If it is a real device, such as hpip backend may return, then we return
						# that text with ' via goprint' on the end of it.
						if endParse[0] !~ /Unknown/ && endParse[0] !~ /Nothing/
							endParse[0] = "#{endParse[0]} via #{@config.commandName}"
						end
            
						# Create the output to advertise that the device is being managed
						output = "#{parsed[0]} #{managedBackendCommandName} \"#{endParse[0]}\" \"#{@config.commandName} managed #{endParse[1]}\""

						if (endParse[2])
							# Add the third quoted parameter if present
							output = "#{output} \"#{endParse[2]}\""
						end

						# Add the output to the managedOutput array
						managedOutput.push(output)
					end
				end
			end
		end
		# Return the array of managed devices for the given backend
		return managedOutput
	end

	def runBackend(input, argv)
		if argv.size == 0
			STDERR.puts "DEBUG: goprint backend is discovering other backend devices..."
			# No argument given, return the managed devices
			managedOutput = Array.new
			# Find each of the other backends and present them as managed
			Dir.foreach(@config.runPath) {|backend| managedOutput.concat(manageAsGoprint(backend))}
			# Cull any nil entries
			managedOutput.compact!
			# Insert a Virtual Printer
			managedOutput.push("direct " + @config.commandName + "://hold-queue \"" + @config.commandName + "-hold-queue\" \"" +
				  @config.commandName + " managed Virtual Printer\"")
			# Output the managed devices
			puts managedOutput
		else
			jobNo = argv[0]
			# if there were 6 arguments then we were passed the filename
			# to print on the command-line, otherwise our stdin is the
			# print job data.  we don't need to do anything with that
			# knowledge at this time because we simply chain another
			# backend that should support both scenarios.

			# our return value to CUPS
			value = nil
			
			# Arguments given, a print job is incoming, hold all jobs unless
			# the Agent says they can be delivered.
			begin
				STDERR.puts "DEBUG: goprint backend processing for job #{jobNo} started"

				if ENV['AGENT_BYPASS'] =~ /true/i
					value = 0
				else
					agentSocket = nil
					timeout(@config.agentBackendTimeout) do
						# Open the connection to the Agent
						agentSocket = TCPSocket.open('localhost', @config.agentBackendMonitorPort)
						# Send the job number and the printer name
						agentSocket.puts "#{jobNo}#{@config.agentBackendInfoDelimiter}#{ENV['PRINTER']}"
						# Find the return value
						value = agentSocket.gets.to_i
						# Close the connection
						agentSocket.close
						# Nil out the agentSocket value
						agentSocket = nil
					end
					# Code returned from Agent is in: value
					if value == CUPS_BACKEND_HOLD
						# see http://localhost:631/help/api-filter.html
						STDERR.puts "INFO: goprint backend holding job #{jobNo}"
					elsif value == CUPS_BACKEND_CANCEL
						STDERR.puts "INFO: goprint backend canceling job #{jobNo}"
					end
				end

				if value == 0
					# The print job should be released, pass it off to the real backend
					# Determine the real backend
					deviceURI = ENV['DEVICE_URI'] || jobNo
					if (deviceURI[0, @config.commandName.length] == @config.commandName)
						realDeviceURI = deviceURI[@config.commandName.length+1..-1]
					end
					STDERR.puts "INFO: goprint backend releasing job #{jobNo} to #{realDeviceURI}"

					# Set the proper DEVICE_URI in the environment so the backend that
					# we chain will see it as being its own URI format.
					ENV['DEVICE_URI'] = realDeviceURI

					# Determine the real backend program to run
					if (realDeviceURI.index(":"))
						realBackend = realDeviceURI[0, realDeviceURI.index(":")]
					end

					realBackend = @config.runPath + realBackend
					backendFileStat = File.stat(realBackend)
					# if not executable then it's a backend script that really should
					# be marked executable.  warn about it.  we add 'sh -c' to the front
					# to work around it, but since switching to system() call below it
					# is not really necessary to do so because Ruby always adds that.
					if !backendFileStat.executable?
						STDERR.puts "INFO: Warning: backend script #{realBackend} is not marked executable, using 'sh -c' to run it"
						# prepend the current shell to the command so execution has a chance of working
						if ENV['SHELL']
							realBackend = "#{ENV['SHELL']} -c #{realBackend}"
						else
							realBackend = "/bin/sh -c #{realBackend}"
						end
					end

					# Append all given arguments to the real backend call
					realBackendCommand = realBackend + " \"" + argv.join("\" \"") + "\""

					STDERR.puts "DEBUG: goprint chaining to real backend: #{realBackend}"

					# Spawn the real backend process
					exitCode, outdata, errdata = runCommandAndCapture(realBackendCommand)

					if exitCode != 0
						STDERR.puts "DEBUG: Real backend #{realBackend} exit status was #{value}"
					end
					
					# Always return successful (!?!)
					value = CUPS_BACKEND_PRINT
				end
			rescue
				# Pause the job if there is a problem communicating with the Agent
				STDERR.puts "ERROR: goprint backend encountered an error: #{$!} at #{$@.to_s}"
				value = CUPS_BACKEND_HOLD
			rescue TimeoutError
				# Pause the job if we timed out trying to communicate with the Agent
				STDERR.puts "ERROR: goprint backend timed out communicating with local Agent service"
				value = CUPS_BACKEND_HOLD
			ensure
				# Ensure that the socket is closed
				agentSocket.close unless agentSocket.nil?
			end
			STDERR.puts "DEBUG: goprint backend processing for job #{jobNo} ended with status #{value}"
			exit value
		end
	end

	###
	# Runs a command and captures the stdout and stderr data and
	# the process exit code.
	#
	# This method is a deadlock-safe way in Ruby to execute a process
	# with control over stdout and stderr and the ability to capture 
	# the process exit code.  It uses Ruby's system() function and
	# temp files to capture stdout and stderr.  It does not require
	# any gems, otherwise the open4 gem would be best.
	#
	# There is no control over stdin at this time, but the current
	# Ruby process' stdin will be seen by the executed process as
	# long as STDIN.reopen() has not been used.
	#
	# Ruby is deficient in handling this basic task because:
	#   Kernel.exec() replaces the current process
	#   backticks return stdout but does not capture stderr
	#   IO.popen() provides good stdin and stdout stream control,
	#     and process exit code is easy enough, but no stderr control.
	#   Open3.popen3() provides good stdin, stdout, and stderr stream
	#     control, but no access to process exit code.
	#   Open4.popen4() does what we need, but it is not included
	#     in the Ruby standard library.
	#
	# Returns 3 elements: exit code, stdout data, and stderr data.
	#
	def runCommandAndCapture(command)
		# Tempfile automatically includes our process id and the current 
		# timestamp in the filename.
		outfile = Tempfile.new("goprint-backend-stdout-")
		errfile = Tempfile.new("goprint-backend-stderr-")
		# run the command with redirection of stdout and stderr
		fullCommand = "( #{command} ) > #{outfile.path} 2> #{errfile.path}"
		#puts "DEBUG: executing #{fullCommand}"
		# success will be true if exit code was zero, else it will be false
		success = system(fullCommand)
		# the real exit code is in exitCode
		exitCode = $?.exitstatus
		# read the contents of the temp files
		outdata = outfile.open().read()
		errdata = errfile.open().read()
		outfile.close()
		errfile.close()
		# remove the temp files
		outfile.delete()
		errfile.delete()
	
		return exitCode, outdata, errdata
	end
	
end
