The "podcast" command-line tool is a ruby script, so, opening it up and reviewing it will expose you to other possible implementations & customization for Podcast Producer.
#!/usr/bin/env ruby
#
# Copyright (c) 2006-2007 Apple Inc. All Rights Reserved.
#
# IMPORTANT NOTE: This file is licensed only for use on Apple-labeled computers
# and is subject to the terms and conditions of the Apple Software License Agreement
# accompanying the package this file is a part of. You may not port this file to
# another platform without Apple's written consent.
#
require 'rexml/document'
require 'uri'
require '/usr/lib/podcastproducer/agentcommon'
require 'socket'
require 'ftools'
require 'fileutils'
require 'net/ftp'
require 'net/https'
ASLLogger.enable_console_logging(true)
ENV['PATH'] = "/usr/bin"
DEFAULT_TRANSFER_BUFFER_SIZE = 128 * 1024 # 128K
AGENT_UPLOADER_SLEEP_INTERVAL = 15 #seconds
class RemoteCommand
def execute_get(uri_path)
execute("GET", uri_path)
end
def execute_post(uri_path, parameters = nil)
execute("POST", uri_path, parameters)
end
def execute(request_type, uri_path, parameters = nil)
if (!$server_context)
raise PcastException.new, "Server context not initialized for #{uri_path}"
end
full_uri = URI.parse($server_context.server_url + uri_path)
begin
http = Net::HTTP.new(full_uri.host, full_uri.port)
http.use_ssl = (full_uri.scheme == "https")
if (http.use_ssl && $check_ssl_cert)
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
elsif (http.use_ssl)
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
http.open_timeout = $timeout
http.read_timeout = $timeout
if (request_type == "GET")
if (full_uri.query)
req = Net::HTTP::Get.new(full_uri.path + "?" + full_uri.query)
else
req = Net::HTTP::Get.new(full_uri.path)
end
elsif (request_type == "POST")
req = Net::HTTP::Post.new(full_uri.path)
else
raise PcastException.new, "Unexpected request_type: #{request_type}"
end
req.add_field 'User-Agent', "#{SCRIPT_NAME}/0.1"
req.add_field 'Accept', 'application/xml'
if (parameters && request_type == "POST")
parameters.apply_as_form_data(req)
end
response = http.start do |http|
http.request(req) # This should generate a 401
end
case response
when Net::HTTPSuccess then
results = response.body
when Net::HTTPUnauthorized then
auth_type = response['www-authenticate']
if (auth_type.startsWith("Basic"))
req.basic_auth $server_context.username, $server_context.password
else
digester = DigestAuthentication.new(auth_type)
auth_header_value = digester.generate_authorization_header_value(request_type, full_uri.path, "00000001", $server_context.username, $server_context.password)
req.add_field 'Authorization', auth_header_value
end
response = http.start do |http|
http.request(req) # This should generate the proper response
end
results = response.body
else
raise PcastException.new, "Unhandled HTTP response: #{response}"
end
rescue Timeout::Error => timedout
raise PcastCmdException.new(ERR_SERVER_TIMEOUT, timedout),
"Podcast Producer server '#{$server_context.server_url}' did not respond within #{$timeout} seconds"
rescue Errno::ETIMEDOUT => timedout
raise PcastCmdException.new(ERR_SERVER_TIMEOUT, timedout),
"Podcast Producer server '#{$server_context.server_url}' did not respond within #{$timeout} seconds"
rescue Errno::EHOSTDOWN => hostdown
raise PcastCmdException.new(ERR_SERVER_UNAVAILABLE, hostdown), "Podcast Producer server '#{$server_context.server_url}' not responding"
rescue Errno::ECONNREFUSED => refusal
raise PcastCmdException.new(ERR_SERVER_UNAVAILABLE, refusal), "Podcast Producer server '#{$server_context.server_url}' not responding"
rescue SocketError => error
if (error.to_s == "getaddrinfo: nodename nor servname provided, or not known")
raise PcastCmdException.new(ERR_SERVER_HOSTNAME, error), "Unknown host '#{$server_context.server_url}'"
else
ASLLogger.error error.class
ASLLogger.error error.backtrace
raise PcastException.new, "Unexpected SocketError: #{error}"
end
rescue OpenSSL::SSL::SSLError => sslerror
if (sslerror.to_s == "certificate verify failed")
raise PcastCmdException.new(ERR_SERVER_CERT_VALIDATION, sslerror), "Podcast Producer server '#{$server_context.server_url}' certificate did not pass validation"
elsif (sslerror.to_s == "unknown protocol")
raise PcastCmdException.new(ERR_SERVER_HTTPS_REQUIRED, sslerror),
"Podcast Producer server '#{$server_context.server_url}' is not HTTPS- enabled"
else
ASLLogger.error sslerror.class
ASLLogger.error sslerror.backtrace
raise PcastException.new, "Unexpected SSLError: #{sslerror}"
end
rescue Exception => boom
ASLLogger.error boom.class
ASLLogger.error boom.backtrace
raise PcastCmdException.new(ERR_SERVER_UNKNOWN_FAILURE, boom),
"Remote command '#{full_uri}' failed: #{boom}"
end
begin
if (!results)
raise PcastServerException.new(ERR_SERVER_BAD_RESPONSE), "No results from server"
end
# Check for apache responses within the body
if (results.index("401 Authorization Required"))
raise PcastServerException.new(ERR_SERVER_AUTH_FAILURE), "Not authorized"
elsif (results.index("503 Service Temporarily Unavailable"))
raise PcastServerException.new(ERR_SERVER_UNAVAILABLE), "Server unavailable"
end
# validate podcast producer xml
response_xml = REXML::Document.new(results)
if (!response_xml)
raise PcastServerException.new(ERR_SERVER_BAD_RESPONSE), "No XML response from server"
end
if (RemoteCommand.first_xml_value_for_key(response_xml, "podcast_producer_result"))
return response_xml
else
raise PcastServerException.new(ERR_SERVER_BAD_RESPONSE), "Invalid XML response from server (possibly not a Podcast Producer server)"
end
rescue REXML::ParseException => xml_exception
raise PcastServerException.new(ERR_SERVER_BAD_RESPONSE), "Invalid XML response from server (possibly not a Podcast Producer server)"
end
end
def RemoteCommand.first_xml_value_for_key(xml, key)
value = RemoteCommand.xml_value_for_key(xml, key)
if (value)
value[0]
else
return nil
end
end
def RemoteCommand.xml_value_for_key(xml, key)
if (xml)
query = xml.elements["//#{key}"]
if (query)
return query
else
ASLLogger.error("Key '#{key}' not found in XML response")
end
end
return nil
end
end
class PListOutput
def PListOutput.output_plist(plist)
puts %q{<?xml version="1.0" encoding="UTF-8"?>}
puts %q{<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">}
puts plist.to_s
end
end
class Query < RemoteCommand
def Query.list_cameras
Query.new.execute_query("/cameras")
end
def Query.list_workflows
# Request the user's primary language for names and descriptions of workflows
user_defaults = OSX::NSUserDefaults.standardUserDefaults
languages = user_defaults.objectForKey("AppleLanguages")
default_language = languages[0]
Query.new.execute_query("/workflows?language=#{default_language.to_s}")
end
def execute_query(uri_path)
response_xml = execute_get(uri_path)
if (response_xml)
status = RemoteCommand.first_xml_value_for_key(response_xml, "status")
if (status != "success")
reason = RemoteCommand.first_xml_value_for_key(response_xml, "reason")
raise PcastServerException.new(ERR_SERVER_AUTH_FAILURE), "#{reason}"
else
plist = RemoteCommand.xml_value_for_key(response_xml, "plist")
if (plist)
PListOutput.output_plist(plist)
end
end
end
end
end
class FileReader
def initialize(file_path, upload)
@file_path = file_path
@upload = upload
end
def length
File.size(@file_path)
end
def open
@file_io = File.open(@file_path, "r")
end
def read(length)
# We ignore the dinky 1024 read length sent to us and substitute our value @upload.upload_buffer_size
buffer = @file_io.read(@upload.upload_buffer_size)
if (buffer)
@upload.update_progress(buffer.length)
end
return buffer
end
def close
@file_io.close
end
end
class Upload < RemoteCommand
def Upload.set_progress(control_plist, percentage, estimated_time_left, status, save_immediate=true)
if (status == "pending")
control_plist.set("created_at", Time.now, save_immediate)
end
# We're going from pending to uploading
if (control_plist.get("status") == "pending" && status == "uploading")
control_plist.set("started_at", Time.now, save_immediate)
end
# We're going from uploading to completed or error
if (control_plist.get("status") == "uploading" && (status == "completed" || status == "error"))
control_plist.set("finished_at", Time.now, save_immediate)
end
control_plist.set("status", status, save_immediate)
control_plist.set("percent_done", percentage.to_s, save_immediate)
control_plist.set("estimated_time_left", estimated_time_left, save_immediate)
end
def update_progress(bytes_written)
@uploaded_size += bytes_written
percent_done = @uploaded_size * 100 / @total_upload_size
now = Time.now
elapsed = now - @started_at
time_each_percent = elapsed / percent_done
remaining_time = time_each_percent * (100 - percent_done)
ASLLogger.notice "Uploaded: #{@uploaded_size}/#{@total_upload_size} bytes (#{percent_done} %)"
ASLLogger.notice "Time elapsed: #{elapsed} and remaining: #{remaining_time} (seconds)"
Upload.set_progress(@control_plist, percent_done, remaining_time, "uploading")
end
def initialize(control_plist_path)
@control_plist_path = control_plist_path
@started_at = nil
@total_upload_size = 0
@uploaded_size = 0
end
def upload_type
@control_plist.get("upload_type")
end
def upload_uuid
@control_plist.get("upload_uuid")
end
def delete_after_upload
@control_plist.get("delete_after_upload") == "true"
end
def submission_uuid
@control_plist.get("submission_uuid")
end
def recording_uuid
@control_plist.get("recording_uuid")
end
def file_copy_path
@control_plist.get("file_copy_path")
end
def https_upload_url
@control_plist.get("https_upload_url")
end
def ftp_upload_url
@control_plist.get("ftp_upload_url")
end
def content_paths
@control_plist.get_array("content_paths")
end
def metadata_file
@control_plist.get("cached_metadata_file")
end
def original_metadata_file
@control_plist.get("original_metadata_file")
end
def upload_buffer_size
@control_plist.get("upload_buffer_size").to_i
end
def paths_to_submit
paths_to_submit = Array.new
if (metadata_file && !metadata_file.empty?)
paths_to_submit << metadata_file
end
paths_to_submit = paths_to_submit | content_paths
end
def paths_to_cleanup
paths_to_cleanup = Array.new
if (original_metadata_file && !original_metadata_file.empty?)
paths_to_cleanup << original_metadata_file
end
paths_to_cleanup = paths_to_cleanup | content_paths
end
def primary_content_path
if (content_paths)
content_paths[0]
else
nil
end
end
def run
begin
ASLLogger.notice "Processing: #{@control_plist_path}"
begin
@control_plist = PList.new(@control_plist_path)
rescue Exception => boom
# Likely, the file is not complete yet, skip it and try again later
ASLLogger.error "Invalid upload configuration file: #{@control_plist_path}."
ASLLogger.error boom
return
end
if (@control_plist.get("status") == "completed")
ASLLogger.notice "Skipping completed upload."
return
end
# This is the last thing written to the plist, if it's not there, it's not ready
submission_uuid = @control_plist.get("submission_uuid")
if (!submission_uuid || submission_uuid.empty?)
if (@control_plist.get("upload_type") == "recording")
ASLLogger.notice "Missing submission_uuid. Initiating submission."
Submission.initiate_recording_submission(@control_plist)
else
ASLLogger.notice "Missing submission_uuid for #{@control_plist.get("upload_type")} upload. Skipping."
return
end
end
if (!upload_and_complete_submission)
ASLLogger.error "Failed to upload and complete #{upload_type} submission: #{upload_uuid}"
return
end
# Mark the upload as completed
Upload.set_progress(@control_plist, 100, "0", "completed")
# If the original upload requested deletion of content files and original metadata file,
# remove them after the upload completes. The cached metadata file still exists
if (delete_after_upload)
cleanup_paths(paths_to_cleanup)
end
# Move the completed control file into the completed area
completed_path = File.join(Uploader.completed_control_files_area, File.basename(@control_plist_path))
FileUtils.move(@control_plist_path, completed_path)
ASLLogger.notice "Successfully completed #{upload_type} upload: #{upload_uuid}"
rescue Exception => boom
# Once we've gotten an error, we need to mark the control plist as error and not pick it up anymore
Upload.set_progress(@control_plist, 0, "0", "error", true)
raise boom # Re-raise
end
end
def upload_cmd(cmd, cmd_args, file_path)
cmd_result, results = system_cmd(cmd, cmd_args)
if (cmd_result != 0)
raise PcastServerException.new(ERR_SERVER_FILE_UPLOAD_FAILED), "Failed to upload: #{file_path} error_code: #{$?}"
end
results
end
def cleanup_paths(paths_array)
paths_array.each do |path|
if (File.exist?(path))
if (FileUtils.rm_r(path, :force => true, :secure => true))
ASLLogger.notice("Cleaned up #{path}")
end
end
end
end
def copy_upload(file_copy_path, file_path, dest_file_name, recording_uuid)
begin
if (!file_copy_path || file_copy_path.to_s.empty?)
raise "No file copy path"
end
copy_dir = File.join(file_copy_path.to_s, recording_uuid)
# We never create directories, the server does. If it doesn't exist, we can't copy
if (!File.exist?(copy_dir))
raise "Destination directory: '#{copy_dir}' is not mounted on client machine. Skipping."
end
# Where are we going?
destpath = File.join(copy_dir, dest_file_name)
ASLLogger.notice("Trying direct copy to '#{copy_dir}'...")
buffer_length = self.upload_buffer_size
begin
file_size = File.size(file_path)
File.open(file_path, "r") do |read_file|
File.open(destpath, "w+") do |write_file|
# Preallocate here (assume it's an Xsan)
PcastPreallocate.preallocate(write_file.fileno, file_size)
while (true)
buffer = read_file.read(buffer_length)
if (!buffer)
break
end
bytes_written = write_file.write(buffer)
update_progress(bytes_written)
end
end
end
rescue Exception => boom
#puts boom.backtrace
raise "#{copy_dir} is not writable. Skipping."
end
ASLLogger.notice "Local copy succeeded for '#{file_path}'"
true
rescue Exception => details
ASLLogger.notice(details)
false
end
end
def https_upload(https_upload_url, file_path, dest_file_name, recording_uuid)
begin
if (!https_upload_url || https_upload_url.to_s.empty?)
raise "No HTTPS upload URL"
end
https_uri = URI.parse(https_upload_url.to_s)
ASLLogger.notice "Trying HTTPS POST of '#{File.basename(file_path)}' to '#{https_uri.to_s}'"
http = Net::HTTP.new(https_uri.host, https_uri.port)
http.use_ssl = (https_uri.scheme == "https")
if (http.use_ssl && $check_ssl_cert)
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
elsif (http.use_ssl)
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
results = ""
fr = FileReader.new(file_path, self)
begin
fr.open
http.start do |http|
request = Net::HTTP::Post.new(https_uri.path)
request.add_field 'File-Name', dest_file_name
request.add_field 'Recording-UUID', recording_uuid
request.add_field 'Content-Length', fr.length
request.add_field 'Content-Type', 'application/x-www-form-urlencoded'
request.body_stream = fr # Stream the files
response = http.request(request)
response.value # Check for 2xx response code
results = response.body # Read the response body
end
ensure
fr.close
end
if (results.match(/<status>success<\/status>/))
ASLLogger.notice "HTTPS POST upload succeeded for '#{file_path}'"
true
else
raise PcastServerException.new(ERR_SERVER_FILE_UPLOAD_FAILED), "Failed to upload: #{file_path}"
end
rescue Exception => details
ASLLogger.notice(details)
false
end
end
def ftp_upload(ftp_upload_url, file_path, dest_file_name, recording_uuid)
begin
if (!ftp_upload_url || ftp_upload_url.to_s.empty?)
raise "No FTP upload URL"
end
ftp_uri = URI.parse(ftp_upload_url.to_s)
if (ftp_uri.user == "username" && ftp_uri.password == "password")
raise "FTP upload not configured"
end
# Within the FTP server, what's the destination path?
path = File.join(recording_uuid, dest_file_name)
ASLLogger.notice "Trying FTP upload of '#{File.basename(file_path)}' as '#{ftp_uri.user}@#{ftp_uri.host}' to: '#{path}'"
Net::FTP.open(ftp_uri.host) do |ftp|
ftp.login(ftp_uri.user, ftp_uri.password)
ftp.chdir(recording_uuid) # Below <PodcastProducerSharedFS>/Submissions
ftp.putbinaryfile(file_path, dest_file_name, self.upload_buffer_size) do |data|
update_progress(data.length)
end
end
ASLLogger.notice "FTP upload succeeded for '#{file_path}'"
true
rescue Exception => details
ASLLogger.notice(details)
false
end
end
def upload_and_complete_submission
ASLLogger.notice "Starting #{upload_type} upload for #{upload_uuid}"
# Set the start time
@started_at = Time.now
# Evaluate each path and if it's a directory, tar it up
archive_paths = Array.new
paths_to_upload = Array.new
paths_to_submit.each do |path|
if (File.directory?(path))
basename = File.basename(path)
archive_path = File.join(Uploader.archives_area, "FOLDER_#{basename}.cpgz")
cmd = "/usr/bin/ditto"
cmd_args = Array.new
cmd_args << "-c"
cmd_args << "-z"
cmd_args << "--keepParent"
cmd_args << path
cmd_args << archive_path
# Run the ditto operation
upload_cmd(cmd, cmd_args, path)
archive_paths << archive_path
paths_to_upload << archive_path
else
# Just a file
paths_to_upload << path
end
end
# Calculate the total size to upload
paths_to_upload.each do |path|
@total_upload_size += File.size(path)
end
ASLLogger.notice "Total Upload Size: #{@total_upload_size}"
# We attempt to upload each of the files in the submission
expected_files = Array.new
paths_to_upload.each do |path|
# Keep track of the original basename of the file to maintain it
extname = File.extname(path)
basename = File.basename(path)
# Keep track of the file size
file_size = File.size(path)
# Currently, the metadata is submitted in two different formats, plist and xml
if (upload_type == "file")
if (path == metadata_file)
dest_file_name = "user_metadata.plist"
else
dest_file_name = basename
end
else
# This is a recording
if (extname == ".xml")
dest_file_name = "recording_metadata.xml"
else
dest_file_name = basename
end
end
# Keep track of expected files
expected_files << "#{dest_file_name}:#{file_size.to_s}"
# We favor File copy over HTTPS over FTP
upload_succeeded = copy_upload(file_copy_path, path, dest_file_name, recording_uuid.to_s)
if (!upload_succeeded)
$check_ssl_cert = @control_plist.get("check_ssl_cert") == "true"
upload_succeeded = https_upload(https_upload_url, path, dest_file_name, recording_uuid.to_s)
end
if (!upload_succeeded)
upload_succeeded = ftp_upload(ftp_upload_url, path, dest_file_name, recording_uuid.to_s)
end
if (!upload_succeeded)
raise PcastServerException.new(ERR_SERVER_FILE_UPLOAD_FAILED), "Unable to upload file: #{path}"
end
end
# Before completing the submission, remove all the archives
cleanup_paths(archive_paths)
# Contact the server and complete the submission
params = CmdParameters.new
params.add("recording_uuid", recording_uuid)
params.add("submission_uuid", submission_uuid)
params.add("submitted_files", expected_files.join(";"))
params.add("primary_content_file", File.basename(primary_content_path))
# We need to return to the original server to do the completion, it does not require a username/password
$server_url = @control_plist.get("server_url")
$check_ssl_cert = @control_plist.get("check_ssl_cert") == "true"
$server_context = ServerContext.get(false)
complete_xml = execute_post("/submissions/complete", params)
# Evaluate the response
status = RemoteCommand.first_xml_value_for_key(complete_xml, "status")
if (status.to_s == "success")
pcast_job_id = RemoteCommand.first_xml_value_for_key(complete_xml, "pcast_job_id")
ASLLogger.notice "Podcast Producer Job ID: #{pcast_job_id}"
true
else
false
end
end
end
class Uploader
def terminate
ASLLogger.notice "pcastuploader terminated."
@running = false
end
def Uploader.start(control_plist_path = nil)
ASLLogger.enable_timestamps(true)
if (!control_plist_path)
ASLLogger.notice("Starting pcastuploader as pid: #{Process.pid}...")
end
uploader = Uploader.new
trap("TERM") do
ASLLogger.notice("pcastuploader got TERM signal...")
uploader.terminate
end
uploader.run(control_plist_path)
ASLLogger.notice("pcastuploader quit")
end
def run(control_plist_path = nil)
if (control_plist_path)
begin
# For each control plist we find, do an upload
upload = Upload.new(control_plist_path)
upload.run
rescue Exception => e
ASLLogger.error e
end
else
ASLLogger.notice("Using uploads area: #{Uploader.control_files_area}")
@running = true
while (@running)
# For every file we find in the directory, do an upload run
# Always sort this by earliest first
control_files = Dir["#{Uploader.control_files_area}/*"].sort_by {|f| test(?M, f)}
control_files.each do |control_plist_path|
begin
# For each control plist we find, do an upload
upload = Upload.new(control_plist_path)
upload.run
rescue Exception => e
ASLLogger.error e
end
end
# Sleep at the bottom of the loop for 15 seconds
sleep AGENT_UPLOADER_SLEEP_INTERVAL
end
end
end
def Uploader.area(area_name)
if (Process.uid == 0)
# This is below /var/spool/pcastuploader/<area_name>
area = File.join(AGENT_UPLOAD_AREA, area_name)
else
# This is below ~/Library/Application Support/pcastuploader/<area_name>
area = File.join(File.expand_path(ADHOC_UPLOAD_AREA), area_name)
end
FileUtils.mkdir_p area
File::chmod(0750, area)
area
end
def Uploader.control_files_area
Uploader.area("control")
end
def Uploader.metadata_area
Uploader.area("metadata")
end
def Uploader.archives_area
Uploader.area("archives")
end
def Uploader.completed_control_files_area
Uploader.area("completed")
end
def Uploader.list_uploads
all_uploads = OSX::NSMutableDictionary.dictionary
# This lists all pending, errored, and in-progress control files in the library area
control_files = Dir["#{Uploader.control_files_area}/*"]
control_files.each do |control_plist_path|
upload_uuid = File.basename(control_plist_path, ".plist")
upload = PList.new(control_plist_path)
if (upload && upload.dict)
all_uploads.setObject_forKey(upload.dict, upload_uuid)
end
end
# This lists all completed control files in the library area
control_files = Dir["#{Uploader.completed_control_files_area}/*"]
control_files.each do |control_plist_path|
upload_uuid = File.basename(control_plist_path, ".plist")
upload = PList.new(control_plist_path)
if (upload && upload.dict)
all_uploads.setObject_forKey(upload.dict, upload_uuid)
end
end
data = OSX::NSPropertyListSerialization.dataFromPropertyList_format_errorDescription(all_uploads, OSX::NSPropertyListXMLFormat_v1_0, nil)
puts OSX::NSString.alloc.initWithData_encoding(data, OSX::NSUTF8StringEncoding)
#puts all_uploads.description.to_s
end
def Uploader.clear_completed_uploads
# We iterate through all the control plists and remove the completed ones
control_files = Dir["#{Uploader.completed_control_files_area}/*"]
control_files.each do |control_plist_path|
upload_uuid = File.basename(control_plist_path, ".plist")
upload = PList.new(control_plist_path)
if (upload.get("status") == "completed")
File.delete(control_plist_path)
# We also delete the metadata file associated with this uploader if it exists
metadata_path = upload.get("cached_metadata_file")
if (metadata_path && File.exist?(metadata_path))
File.delete(metadata_path)
end
end
end
end
def Uploader.clear_errored_uploads
# We iterate through all the control plists and remove the errored ones
control_files = Dir["#{Uploader.control_files_area}/*"]
control_files.each do |control_plist_path|
upload_uuid = File.basename(control_plist_path, ".plist")
upload = PList.new(control_plist_path)
if (upload.get("status") == "error")
File.delete(control_plist_path)
# We also delete the metadata file associated with this uploader if it exists
metadata_path = upload.get("cached_metadata_file")
if (metadata_path && File.exist?(metadata_path))
File.delete(metadata_path)
end
end
end
end
end
class Submission < RemoteCommand
def Submission.read_create_response(create_xml, control_plist)
if (!create_xml)
raise PcastServerException.new(ERR_SERVER_SUBMISSION_CREATE_FAILED), "Submission create failed"
end
# Extract the information from the response and store it in the control_plist
status = RemoteCommand.first_xml_value_for_key(create_xml, "status")
if (status != "success")
raise PcastServerException.new(ERR_SERVER_SUBMISSION_CREATE_FAILED), "Submission create failed"
end
# Extract our important keys for this submission
submission_uuid = RemoteCommand.first_xml_value_for_key(create_xml, "uuid")
recording_uuid = RemoteCommand.first_xml_value_for_key(create_xml, "recording_uuid")
file_copy_path = RemoteCommand.first_xml_value_for_key(create_xml, "file_copy_path")
https_upload_url = RemoteCommand.first_xml_value_for_key(create_xml, "https_upload_url")
ftp_upload_url = RemoteCommand.first_xml_value_for_key(create_xml, "ftp_upload_url")
control_plist.set("recording_uuid", recording_uuid, false)
control_plist.set("file_copy_path", file_copy_path, false)
control_plist.set("https_upload_url", https_upload_url, false)
control_plist.set("ftp_upload_url", ftp_upload_url, false) # TODO: make sure the user:pass is secured
control_plist.set("submission_uuid", submission_uuid, false)
end
def Submission.create_upload_for_recording(recording_uuid, delete_after_upload)
# Create a new upload_uuid
upload_uuid = UUID.generate
# Make sure the control files area exists
Uploader.control_files_area
# Make sure logs area exists
FileUtils.mkdir_p AGENT_UPLOAD_LOGS_AREA
# If the launchd plist does not exist, create and load it
if (!File.exist?(AGENT_LAUNCHD_PLIST))
FileUtils.cp(AGENT_LAUNCHD_TEMPLATE, AGENT_LAUNCHD_PLIST)
if (File.exist?(AGENT_LAUNCHD_PLIST))
system("/bin/launchctl load #{AGENT_LAUNCHD_PLIST}")
if ($? != 0)
ASLLogger.error "Failed to load #{AGENT_LAUNCHD_PLIST}"
end
end
end
# Where will the upload configuration go?
control_plist_path = File.join(Uploader.control_files_area, "#{upload_uuid}.plist")
# Within the recording repository, find the completed recording...
recording_dir = AgentPreferences.get("RecordingRepositoryPath")
content_file = File.join(recording_dir, recording_uuid + ".mov")
recording_metadata_file = File.join(recording_dir, recording_uuid + ".xml")
# Copy the metadata file into the metadata files area
cached_metadata_path = File.join(Uploader.metadata_area, "#{upload_uuid}.xml")
FileUtils.cp(recording_metadata_file, cached_metadata_path)
content_paths = Array.new
content_paths << content_file
# Create properties that this upload will need when it is run
control_plist = PList.new(control_plist_path)
control_plist.set("upload_type", "recording", false)
control_plist.set("upload_uuid", upload_uuid, false)
control_plist.set("recording_uuid", recording_uuid, false)
control_plist.set("delete_after_upload", delete_after_upload ? "true" : "false", false)
control_plist.set("original_metadata_file", recording_metadata_file, false)
control_plist.set_array("content_paths", content_paths, false)
control_plist.set("cached_metadata_file", cached_metadata_path, false)
control_plist.set("upload_buffer_size", $upload_buffer_size, false)
control_plist.set("camera_uuid", AgentPreferences.get("CameraUUID"), false)
# Lookup our agent shared secret for the server and encrypt the recording_uuid with it
server_uuid = AgentPreferences.get("ServerUUID")
secret = Base64.decode64(PcastSecret.agent_shared_secret_for_server(server_uuid))
enc_recording_uuid = Crypto.encrypt(secret, recording_uuid)
control_plist.set("enc_recording_uuid", enc_recording_uuid, false)
# Make sure we know what server we'll be using
control_plist.set("server_url", $server_url, false)
control_plist.set("check_ssl_cert", $check_ssl_cert, false)
# Initialize the progress in the control file
Upload.set_progress(control_plist, 0, "N/A", "pending", false)
# We save out our upload config file and the uploader will be started by launchd
if (!control_plist.save)
raise PcastException.new(ERR_PLIST_FAILURE), "Unable to save #{control_plist_path}"
end
# Lock it down to root
File::chmod(0640, control_plist_path)
ASLLogger.notice("Upload #{upload_uuid} for recording: #{$recording_uuid} initiated successfully.")
end
def Submission.initiate_recording_submission(control_plist)
# Contact the server and create a new submission for this recording
params = CmdParameters.new
params.add("submission_type", "recording")
params.add("camera_uuid", control_plist.get("camera_uuid"))
params.add("enc_recording_uuid", control_plist.get("enc_recording_uuid"))
# We contact the server to do start the submission, it does not require a username/password
$server_url = control_plist.get("server_url")
$check_ssl_cert = control_plist.get("check_ssl_cert") == "true"
$server_context = ServerContext.get(false)
# Here, we contact the server to create a submission container for this recording
sub = Submission.new
create_xml = sub.execute_post("/submissions/create_for_recording", params)
Submission.read_create_response(create_xml, control_plist)
end
def Submission.create_upload_for_files(paths_to_submit, metadata_file, workflow_name, delete_after_upload)
# Create a new upload_uuid
upload_uuid = UUID.generate
# Make sure the control files area exists
Uploader.control_files_area
# Make sure logs area exists
FileUtils.mkdir_p File.expand_path(ADHOC_UPLOAD_LOGS_AREA)
# Where will the upload configuration go?
control_plist_path = File.join(Uploader.control_files_area, "#{upload_uuid}.plist")
# If passed, validate that the identified metadata file is a valid dictionary plist
if ($metadata_file_path)
begin
md = OSX::NSDictionary.dictionaryWithContentsOfFile($metadata_file_path)
rescue
raise PcastException.new(ERR_CMDLINE_PARAMETERS), "#{$metadata_file_path} is not a valid dictionary plist"
end
# Copy the metadata file into the metadata files area
metadata_plist_path = File.join(Uploader.metadata_area, "#{upload_uuid}.plist")
FileUtils.cp($metadata_file_path, metadata_plist_path)
end
# Check that all the files exist and are readable
paths_to_submit.each do |path|
if (!File.exist?(path))
raise PcastException.new(ERR_CMDLINE_PARAMETERS), "#{path} does not exist"
end
if (!File.readable?(path))
raise PcastException.new(ERR_CMDLINE_PARAMETERS), "#{path} is not readable"
end
end
# Create properties that this upload will need when it is run
control_plist = PList.new(control_plist_path)
control_plist.set("upload_type", "file", false)
control_plist.set("upload_uuid", upload_uuid, false)
control_plist.set("user", $username, false)
if ($metadata_file_path)
control_plist.set("original_metadata_file", $metadata_file_path, false)
end
control_plist.set_array("content_paths", paths_to_submit, false)
control_plist.set("delete_after_upload", delete_after_upload ? "true" : "false", false)
if (metadata_plist_path)
control_plist.set("cached_metadata_file", metadata_plist_path, false)
end
control_plist.set("upload_buffer_size", $upload_buffer_size, false)
control_plist.set("workflow_name", workflow_name, false)
# Initialize the progress in the control file
Upload.set_progress(control_plist, 0, "N/A", "pending")
# Initiate a file submission on the server
Submission.initiate_file_submission(control_plist)
# We save out our upload config file and the uploader will be started by launchd
if (!control_plist.save)
raise PcastException.new(ERR_PLIST_FAILURE), "Unable to save #{control_plist_path}"
end
# Lock it down to this user
File::chmod(0640, control_plist_path)
ASLLogger.enable_timestamps(true)
ASLLogger.notice("Upload UUID: #{upload_uuid}")
# For adhoc file submissions, we run the upload synchronously
Uploader.start control_plist_path
end
def Submission.initiate_file_submission(control_plist)
# Contact the server and create a new submission
params = CmdParameters.new
params.add("submission_type", "file")
params.add("workflow_name", control_plist.get("workflow_name"))
sub = Submission.new
create_xml = sub.execute_post("/submissions/create_for_file", params)
Submission.read_create_response(create_xml, control_plist)
control_plist.set("server_url", $server_url, false)
control_plist.set("check_ssl_cert", $check_ssl_cert, false)
end
end
class Registration < RemoteCommand
def Registration.is_bound
if (File.exist?(AGENT_PREFS_FILE))
camera_name = AgentPreferences.get("CameraName")
return camera_name && !camera_name.empty?
else
return false
end
end
def Registration.bind
Registration.new.bind
end
def Registration.unbind
Registration.new.unbind
end
def bind
if (Registration.is_bound)
ASLLogger.notice "WARNING: Agent was previously bound as: #{AgentPreferences.get("CameraName")}"
end
ASLLogger.notice "Registering camera: #{$camera_agent_name} on #{$server_context.server_url}"
# First, we need a new shared secret
shared_secret = GoodRandom.bytes64(16)
params = CmdParameters.new
params.add("camera_name", $camera_agent_name)
params.add("shared_secret", shared_secret)
bind_xml = execute_post("/cameras/bind", params)
status = RemoteCommand.first_xml_value_for_key(bind_xml, "status")
if (status == "success")
camera_uuid = RemoteCommand.first_xml_value_for_key(bind_xml, "camera_uuid")
server_uuid = RemoteCommand.first_xml_value_for_key(bind_xml, "server_uuid")
tunnel_host = RemoteCommand.first_xml_value_for_key(bind_xml, "tunnel_host")
tunnel_port = RemoteCommand.first_xml_value_for_key(bind_xml, "tunnel_port")
ASLLogger.notice "Binding local camera: #{$camera_agent_name}"
# Now we need to save it all out to prefs
if (PcastSecret.set_agent_shared_secret_for_server(shared_secret, server_uuid) &&
AgentConfig.bind($camera_agent_name, camera_uuid, server_uuid,
$server_context.server_url, tunnel_host, tunnel_port))
ASLLogger.notice "Successfully bound #{camera_uuid} to server #{server_uuid}"
else
AgentConfig.unbind
raise PcastException.new(ERR_PLIST_FAILURE), "Failed to bind camera: #{$camera_agent_name}"
end
else
reason = RemoteCommand.first_xml_value_for_key(bind_xml, "reason")
AgentConfig.unbind
raise PcastServerException.new(ERR_SERVER_BIND_FAILED), "#{reason}"
end
end
def unbind
ASLLogger.notice "Unregistering camera: #{$camera_agent_name} from #{$server_context.server_url}"
params = CmdParameters.new
params.add("camera_name", $camera_agent_name)
unbind_xml = execute_post("/cameras/unbind", params)
status = RemoteCommand.first_xml_value_for_key(unbind_xml, "status")
if (status != "success")
reason = RemoteCommand.first_xml_value_for_key(unbind_xml, "reason")
ASLLogger.error "Failure reason: #{reason}"
# We continue on here to unbind locally, even if the server operation failed
end
# Remove all traces from this agent
local_camera_name = AgentPreferences.get("CameraName")
if (Registration.is_bound)
if ($camera_agent_name == local_camera_name)
ASLLogger.notice "Unbinding local camera: #{local_camera_name}"
camera_uuid = AgentPreferences.get("CameraUUID")
server_uuid = AgentPreferences.get("ServerUUID")
# remove the shared secret for this server
PcastSecret.remove_agent_shared_secret_for_server(server_uuid)
if (AgentConfig.unbind)
ASLLogger.notice "Successfully unbound #{camera_uuid} from server #{server_uuid}"
else
raise PcastException.new(ERR_PLIST_FAILURE), "Failed to unbind camera: #{local_camera_name}"
end
else
ASLLogger.notice "WARNING: Local camera is not #{$camera_agent_name}, skipping local unbind."
end
else
ASLLogger.notice "WARNING: Local camera is not bound"
end
end
end
class CameraCmd < RemoteCommand
def CameraCmd.start(audio_only)
ASLLogger.notice "Starting recording on '#{$camera_agent_name}' on #{$server_context.server_url}"
params = nil
if (audio_only)
params = CmdParameters.new
params.add("audio_only", "true")
end
CameraCmd.new.send_cmd("start", params)
ASLLogger.notice("'#{$camera_agent_name}' recording started")
end
def CameraCmd.stop(workflow_name, metadata_file_path)
ASLLogger.notice "Stopping recording on '#{$camera_agent_name}' on #{$server_context.server_url}"
params = CmdParameters.new
params.add("workflow_name", workflow_name)
# Read md_file_path as plist, then add key/value pairs as parameters
metadata_plist = PList.new(metadata_file_path)
# Add all the keys to the params
keys = metadata_plist.keys
if (keys)
keys.each do |key|
if (key.startsWith("UserMetadata_"))
params.add("#{key}", metadata_plist.get(key))
else
params.add("UserMetadata_#{key}", metadata_plist.get(key))
end
end
else
raise PcastServerException.new(ERR_CMDLINE_PARAMETERS), "Invalid property list file: #{metadata_file_path}"
end
CameraCmd.new.send_cmd("stop", params)
ASLLogger.notice("'#{$camera_agent_name}' recording stopped")
end
def CameraCmd.pause
ASLLogger.notice "Pausing recording on '#{$camera_agent_name}' on #{$server_context.server_url}"
CameraCmd.new.send_cmd("pause")
ASLLogger.notice("'#{$camera_agent_name}' recording paused")
end
def CameraCmd.resume
ASLLogger.notice "Resuming recording on '#{$camera_agent_name}' on #{$server_context.server_url}"
CameraCmd.new.send_cmd("resume")
ASLLogger.notice("'#{$camera_agent_name}' recording resumed")
end
def CameraCmd.cancel
ASLLogger.notice "Canceling recording on '#{$camera_agent_name}' on #{$server_context.server_url}"
CameraCmd.new.send_cmd("cancel")
ASLLogger.notice("'#{$camera_agent_name}' recording canceled")
end
def CameraCmd.status(update_preview)
params = CmdParameters.new
if (update_preview)
params.add("update_preview", "true")
end
response_xml = CameraCmd.new.send_cmd("status", params)
plist = RemoteCommand.xml_value_for_key(response_xml, "plist")
if (plist)
PListOutput.output_plist(plist)
end
end
def send_cmd(operation, params=nil)
params = CmdParameters.new unless params
params.add("camera_name", $camera_agent_name)
response_xml = execute_post("/cameras/#{operation}", params)
status = RemoteCommand.first_xml_value_for_key(response_xml, "status")
if (status == "success")
results = RemoteCommand.first_xml_value_for_key(response_xml, "results")
if (results == "OK")
return response_xml
else
raise PcastServerException.new(ERR_SERVER_CAMERA_CMD_FAILED), "Unexpected camera agent results: #{results}"
end
else
reason = RemoteCommand.first_xml_value_for_key(response_xml, "reason")
raise PcastServerException.new(ERR_SERVER_CAMERA_CMD_FAILED), "Failed: '#{reason}'"
end
end
end
class ServerContext
def ServerContext.get(requires_user_pass=true)
# Choose an intelligent default if no -s is passed
if (!$server_url)
$server_url = "https://#{Socket.gethostname}:#{SERVER_DEFAULT_PORT}/podcastproducer"
end
# Provide compatibility with -s <hostname> (not a URL)
if (!$server_url.startsWith("http"))
$server_url = "https://#{$server_url}:#{SERVER_DEFAULT_PORT}/podcastproducer"
end
begin
server_uri = URI.parse($server_url)
rescue URI::InvalidURIError => boom
raise PcastException.new(ERR_CMDLINE_PARAMETERS), boom.to_s
end
if (server_uri.scheme != "https")
raise PcastException.new(ERR_CMDLINE_PARAMETERS), "Invalid --server URL: Must be https://"
end
# Use the environment if no -u is passed
if (requires_user_pass && !$username)
$username = ENV['USER']
end
# If the -p was not passed on the command line, prompt for it
if (requires_user_pass && !$password)
gp = PcastPassword.new
begin
$password = gp.get_password("Enter password for #{$username}: ")
rescue Interrupt => boom
raise PcastException.new(ERR_CMD_CANCELED), "Command canceled"
end
end
if (requires_user_pass)
require_argument($username, "user name")
require_argument($password, "password")
end
ServerContext.new($server_url, $username, $password, $check_ssl_cert)
end
def initialize(server_url, user, pass, check_ssl_cert)
@server_url = server_url
@username = user
@password = pass
@check_ssl_cert = check_ssl_cert
end
def username
@username
end
def password
@password
end
def server_url
@server_url
end
def check_ssl_cert
@check_ssl_cert
end
end
class AgentConfig
def AgentConfig.create_if_missing
if (Process.uid != 0)
adhoc_path = File.expand_path(ADHOC_PREFS_FILE)
if (!File.exist?(adhoc_path))
# Make sure the agent settings file exists
File.copy("#{ETC_BASEDIR}/pcastagentd.adhoc.plist-default", adhoc_path)
File::chmod(0640, adhoc_path)
AgentPreferences.reload
end
else
if (!File.exist?(AGENT_PREFS_FILE))
# Make sure the agent settings file exists
File.copy("#{ETC_BASEDIR}/pcastagentd.agent.plist-default", AGENT_PREFS_FILE)
File::chmod(0644, AGENT_PREFS_FILE)
AgentPreferences.reload
end
end
end
def AgentConfig.bind(camera_name, camera_uuid, server_uuid, server_url, tunnel_host, tunnel_port)
AgentConfig.create_if_missing
AgentPreferences.set("CameraName", camera_name) &&
AgentPreferences.set("CameraUUID", camera_uuid) &&
AgentPreferences.set("ServerUUID", server_uuid) &&
AgentPreferences.set("PodcastProducerURL", server_url) &&
AgentPreferences.set("TunnelHost", tunnel_host) &&
AgentPreferences.set("TunnelPort", tunnel_port)
end
def AgentConfig.unbind
AgentConfig.create_if_missing
AgentPreferences.remove("CameraName")
AgentPreferences.remove("CameraUUID")
AgentPreferences.remove("ServerUUID")
AgentPreferences.remove("PodcastProducerURL")
AgentPreferences.remove("TunnelHost")
AgentPreferences.remove("TunnelPort")
true
end
def AgentConfig.set_config(settings_list)
AgentConfig.create_if_missing
# $config_settings_list looks like 'a=b;c=d;e=f'
settings = settings_list.split(";")
settings.each do |setting|
parts = setting.split("=")
if (parts.length == 2)
key = parts[0]
value = parts[1]
case key
when "Capture"
# Capture = <CaptureType>:<CaptureQuality>
# CaptureType = "Audio|Video|Screen"
# CaptureQuality = "Good|Better|Best"
capture_parts = value.split(":")
if (capture_parts.length != 2)
raise PcastException.new(ERR_PLIST_FAILURE), "Invalid capture setting #{value}"
end
type = capture_parts[0]
quality = capture_parts[1]
# Get the presets dictionary
presets = AgentConfig.presets
# Find the preset type first
preset_type = presets.objectForKey(type)
if (!preset_type)
raise PcastException.new(ERR_PLIST_FAILURE), "Invalid capture type #{type}"
end
# Find the quality next
preset_dict = preset_type.objectForKey(quality)
if (!preset_dict)
raise PcastException.new(ERR_PLIST_FAILURE), "Invalid capture quality #{quality}"
end
# Slam all the keys in the preset_dict into the agent plist
preset_dict.keys.each do |preset_key|
preset_value = preset_dict.objectForKey(preset_key)
if (!AgentPreferences.set(preset_key.to_s, preset_value.to_s))
raise PcastException.new(ERR_PLIST_FAILURE), "Could not set #{preset_key} to #{preset_value}"
end
end
ASLLogger.notice "Set Capture to #{value}"
when "AudioDevice"
# AudioDevice = Automatic|<Name>
if (AgentPreferences.set("RecordingAudioInput", value))
ASLLogger.notice "Set Audio Device to #{value}"
else
raise PcastException.new(ERR_PLIST_FAILURE), "Could not set Audio Device to #{value}"
end
when "VideoDevice"
# VideoDevice = Automatic|<Name>
if (AgentPreferences.set("RecordingVideoInput", value))
ASLLogger.notice "Set Video Device to #{value}"
else
raise PcastException.new(ERR_PLIST_FAILURE), "Could not set Video Device to #{value}"
end
else
raise PcastException.new(ERR_PLIST_FAILURE), "Unknown configuration key #{key}"
end
else
raise PcastException.new(ERR_CMDLINE_PARAMETERS), "Invalid key=value pair: #{setting}"
end
end
end
def AgentConfig.presets
# Get the presets dictionary
capture_presets = "/etc/podcastproducer/pcastagentd.capture-presets.plist"
presets = OSX::NSDictionary.dictionaryWithContentsOfFile(capture_presets)
if (!presets)
raise PcastException.new(ERR_PLIST_FAILURE), "Unable to load presets: #{capture_presets}"
end
presets
end
def AgentConfig.list_presets
presets = AgentConfig.presets
ASLLogger.notice("Capture preset choices: ")
presets.keys.each do |type|
# Find the dict for the type
preset_type = presets.objectForKey(type)
if (!preset_type)
raise PcastException.new(ERR_PLIST_FAILURE), "Invalid capture type #{type}"
end
preset_type.keys.each do |quality|
dict = preset_type.objectForKey(quality)
description = dict.objectForKey("RecordingCaptureDescription")
ASLLogger.notice("\t'Capture=#{type}:#{quality}' => #{description}")
end
end
end
def AgentConfig.list_devices
system("/usr/libexec/podcastproducer/pcastagentd -i")
end
def AgentConfig.get_config
AgentConfig.create_if_missing
ASLLogger.notice("Capture=#{AgentPreferences.get('RecordingCapturePreset')}")
ASLLogger.notice("AudioDevice=#{AgentPreferences.get('RecordingAudioInput')}")
ASLLogger.notice("VideoDevice=#{AgentPreferences.get('RecordingVideoInput')}")
end
end
# Debug
#ASLLogger.notice "ARGS: " + ARGV.join(" ")
opts = GetoptLong.new(
# Server Connection
["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
["--user", "-u", GetoptLong::REQUIRED_ARGUMENT],
["--pass", "-p", GetoptLong::REQUIRED_ARGUMENT],
["--checksslcert", GetoptLong::NO_ARGUMENT],
# Query Operations
["--listcameras", GetoptLong::NO_ARGUMENT],
["--listworkflows", GetoptLong::NO_ARGUMENT],
# Submission Operations
["--submit", GetoptLong::NO_ARGUMENT],
["--recording", "-r", GetoptLong::REQUIRED_ARGUMENT],
["--file", "-f", GetoptLong::REQUIRED_ARGUMENT],
["--workflow", "-w", GetoptLong::REQUIRED_ARGUMENT],
["--metadata", "-m", GetoptLong::REQUIRED_ARGUMENT],
["--delete", GetoptLong::NO_ARGUMENT],
# Uploader
["--run_uploader", GetoptLong::NO_ARGUMENT],
["--list_uploads", GetoptLong::NO_ARGUMENT],
["--clear_completed_uploads", GetoptLong::NO_ARGUMENT],
# Registration Operations
["--isbound", GetoptLong::NO_ARGUMENT],
["--bind", GetoptLong::REQUIRED_ARGUMENT],
["--unbind", GetoptLong::REQUIRED_ARGUMENT],
# AgentPreferences Operations
["--setconfig", GetoptLong::REQUIRED_ARGUMENT],
["--getconfig", GetoptLong::NO_ARGUMENT],
["--presets", GetoptLong::NO_ARGUMENT],
["--devices", GetoptLong::NO_ARGUMENT],
# Camera Command Operations
["--status", GetoptLong::REQUIRED_ARGUMENT],
["--start", GetoptLong::REQUIRED_ARGUMENT],
["--stop", GetoptLong::REQUIRED_ARGUMENT],
["--pause", GetoptLong::REQUIRED_ARGUMENT],
["--resume", GetoptLong::REQUIRED_ARGUMENT],
["--cancel", GetoptLong::REQUIRED_ARGUMENT],
["--update_preview", GetoptLong::NO_ARGUMENT],
["--audio_only", GetoptLong::NO_ARGUMENT],
# Networking
["--timeout", "-t", GetoptLong::REQUIRED_ARGUMENT],
["--upload_buffer_size", GetoptLong::REQUIRED_ARGUMENT],
# Help
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
[ '--debug', '-d', GetoptLong::NO_ARGUMENT ]
)
$server_url = nil
$username = nil
$password = nil
$cmd = nil
$camera_agent_name = nil
$recording_uuid = nil
$paths_to_submit = Array.new
$workflow_name = nil
$metadata_file_path = nil
$camera_agent_name = nil
$config_settings_list = nil
$check_ssl_cert = false
$update_preview = false
$audio_only = false
$timeout = DEFAULT_CMD_TIMEOUT
$delete_after_upload = false
$upload_buffer_size = DEFAULT_TRANSFER_BUFFER_SIZE
# Extract options and arguments
begin
opts.each do |opt, arg|
case opt
when '--help', '-h'
system("/usr/bin/man #{SCRIPT_NAME}")
exit 0
when '--debug', '-d'
ASLLogger.enable_debug_logging(true)
when '--server', '-s'
$server_url = arg
when '--user', '-u'
$username = arg
when '--pass', '-p'
$password = arg
when '--checksslcert'
$check_ssl_cert = true
when '--listcameras'
$cmd = "listcameras"
when '--listworkflows'
$cmd = "listworkflows"
when '--status'
$cmd = "status"
$camera_agent_name = arg
when '--submit'
$cmd = "submit"
when '--run_uploader'
$cmd = "run_uploader"
when '--list_uploads'
$cmd = "list_uploads"
when '--clear_completed_uploads'
$cmd = "clear_completed_uploads"
when '--recording', '-r'
$recording_uuid = arg
when '--file', '-f'
$paths_to_submit << File.expand_path(arg)
when '--workflow', '-w'
$workflow_name = arg
when '--metadata', '-m'
if ($metadata_file_path)
# We already have an identified metadata file
raise GetoptLong::InvalidOption.new, "Only a single --metadata file may be used"
end
$metadata_file_path = File.expand_path(arg)
when '--delete'
$delete_after_upload = true
when '--isbound'
$cmd = "isbound"
when '--bind'
$cmd = "bind"
$camera_agent_name = arg
when '--unbind'
$cmd = "unbind"
$camera_agent_name = arg
when '--setconfig'
$cmd = "setconfig"
$config_settings_list = arg
when '--getconfig'
$cmd = "getconfig"
when '--presets'
$cmd = "presets"
when '--devices'
$cmd = "devices"
when '--start'
$cmd = "start"
$camera_agent_name = arg
when '--stop'
$cmd = "stop"
$camera_agent_name = arg
when '--pause'
$cmd = "pause"
$camera_agent_name = arg
when '--resume'
$cmd = "resume"
$camera_agent_name = arg
when '--cancel'
$cmd = "cancel"
$camera_agent_name = arg
when '--update_preview'
$update_preview = true
when '--audio_only'
$audio_only = true
when '--timeout', '-t'
$timeout = arg.to_i
when '--upload_buffer_size'
$upload_buffer_size = arg
else
print "Illegal argument: #{opt}"
end
end
rescue GetoptLong::InvalidOption => boom
ASLLogger.crit_and_exit(boom, ERR_CMDLINE_PARAMETERS)
rescue GetoptLong::MissingArgument => boom
ASLLogger.crit_and_exit(boom, ERR_CMDLINE_PARAMETERS)
end
def require_argument(argument, display_name)
if (!argument || argument.empty?)
raise PcastException.new(ERR_CMDLINE_PARAMETERS), "--#{$cmd} requires a #{display_name}"
end
end
class PcastServerException < PcastException
def initialize(return_code)
super(return_code)
end
end
# Check for required arguments and context and dispatch commmands
begin
case $cmd
when nil
raise PcastException.new(ERR_CMDLINE_PARAMETERS), "No command specified. Use #{SCRIPT_NAME} --help for assistance."
when "listcameras"
$server_context = ServerContext.get
Query.list_cameras
when "listworkflows"
$server_context = ServerContext.get
Query.list_workflows
when "submit"
if ($recording_uuid)
require_argument($server_url, "server url")
require_argument($recording_uuid, "recording uuid")
check_for_root("--#{$cmd} with --recording")
if (!Registration.is_bound)
raise PcastException.new(ERR_INVALID_AGENT_STATE), "Agent is not bound"
end
$server_context = ServerContext.get(false)
Submission.create_upload_for_recording($recording_uuid, $delete_after_upload)
elsif ($paths_to_submit.length > 0)
require_argument($workflow_name, "workflow name")
$server_context = ServerContext.get
Submission.create_upload_for_files($paths_to_submit, $metadata_file_path, $workflow_name, $delete_after_upload)
else
raise PcastException.new(ERR_CMDLINE_PARAMETERS), "--#{$cmd} requires either --recording or --file"
end
when "run_uploader"
Uploader.start
when "list_uploads"
Uploader.list_uploads
when "clear_completed_uploads"
Uploader.clear_completed_uploads
Uploader.clear_errored_uploads
when "isbound"
if (Registration.is_bound)
puts "BOUND"
exit 1
else
puts "UNBOUND"
exit 0
end
when "bind"
require_argument($camera_agent_name, "camera agent name")
check_for_root($cmd)
$server_context = ServerContext.get
Registration.bind
when "unbind"
require_argument($camera_agent_name, "camera agent name")
check_for_root($cmd)
$server_context = ServerContext.get
Registration.unbind
when "setconfig"
require_argument($config_settings_list, "settings list")
AgentConfig.set_config($config_settings_list)
when "getconfig"
AgentConfig.get_config
when "presets"
AgentConfig.list_presets
when "devices"
AgentConfig.list_devices
when "start"
require_argument($camera_agent_name, "camera agent name")
$server_context = ServerContext.get
CameraCmd.start($audio_only)
when "stop"
require_argument($camera_agent_name, "camera agent name")
require_argument($workflow_name, "workflow name")
require_argument($metadata_file_path, "metadata file path")
$server_context = ServerContext.get
CameraCmd.stop($workflow_name, $metadata_file_path)
when "pause"
require_argument($camera_agent_name, "camera agent name")
$server_context = ServerContext.get
CameraCmd.pause
when "resume"
require_argument($camera_agent_name, "camera agent name")
$server_context = ServerContext.get
CameraCmd.resume
when "cancel"
require_argument($camera_agent_name, "camera agent name")
$server_context = ServerContext.get
CameraCmd.cancel
when "status"
require_argument($camera_agent_name, "camera agent name")
$server_context = ServerContext.get
CameraCmd.status($update_preview)
else
raise PcastException.new(ERR_CMDLINE_PARAMETERS), "Unknown command: #{$cmd}"
end
rescue PcastException => detail
ASLLogger.crit_and_exit(detail, detail.return_code)
rescue SystemExit => detail
raise detail
rescue Exception => detail
ASLLogger.crit_and_exit("Unexpected exception: #{detail} #{detail.backtrace}")
else
exit 0
end