Files
RedHotRoast-ios/HybridCLRData/StrippedAOTDllsTempProj/iOS/AppLovinQualityServiceSetup.rb
T

665 lines
4.7 MiB
Ruby
Raw Normal View History

2026-07-17 14:03:00 +08:00
#!/usr/bin/env ruby
# ----------------------------------------------------------------------------------------------------------------------------------------------------------
# AppLovin Quality Service Setup Script for iOS
# (c) 2023 AppLovin. All Rights Reserved
#
# Syntax:
# ruby AppLovinQualityServiceSetup-xyz.rb <command> <options>
#
# Commands:
# install - Installs AppLovinQualityService onto an iOS target
# uninstall - Uninstalls AppLovinQualityService from a target
# update - Updates the AppLovinQualityService pod to the latest version, or to specific version if version number follows.
# This command should only be used from within the context of an Xcode build phase
#
# Options:
# -targetid <target id> - installs on a specific target identified by its ID as appears in the xcodeproj file (applicable to "install" and "uninstall")
# -targetname <target name> - installs on a specific target identified by its name (applicable to "install" and "uninstall")
# -localframeworks <framework names comma-separated without spaces> - instructs AppLovinQualityService Plugin to analyze these frameworks looking for SDKs
# -version <version> - installs a specific version and blocks automatic future updates (applicable to "install")
#
# Examples:
# ruby AppLovinQualityServiceSetup-xyz.rb (when no parameters given, invokes the "install" command)
# ruby AppLovinQualityServiceSetup-xyz.rb install -targetname MyTarget (installs on target name "MyTarget")
# ruby AppLovinQualityServiceSetup-xyz.rb uninstall
# ruby AppLovinQualityServiceSetup-xyz.rb uninstall -targetname MyTarget
# ----------------------------------------------------------------------------------------------------------------------------------------------------------
require 'net/http'
require 'openssl'
require 'rexml/document'
require 'fileutils'
require 'date'
require 'digest'
require 'json'
require 'base64'
require 'tmpdir'
require 'securerandom'
# Developer-specific ID
application_data=<<APPLICATION_DATA
{
"api_key": "3FwgJWp0mc9soq0XbUerSfpAsO2UzxClyUTe9FtEngqvczhSNObAztMpDFMy7HXP7ynJubciGU6fpOCx5R96_t"
}
APPLICATION_DATA
SCRIPT_FETCH_URI = "https://api2.safedk.com/v1/build/ios_setup2"
MAVEN_REPO = "artifacts.applovin.com/ios"
MAVEN_USER = nil
MAVEN_PASSWORD = nil
@installer = "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
# =====================================================================================
@version = '7.12.6'
# Internal
class String
def escape
self.force_encoding("UTF-8").gsub(/[\\\"`$]/){|m| '\\' + m }
end
def description
self.sub(".pre.","-")
end
end
MAVEN_GROUP = "com/applovin/quality"
MAVEN_GROUP_ID = "com.applovin.quality"
MAVEN_ARTIFACT_IDS = ["AppLovinQualityServicePod2"]
MAVEN_ARTIFACT_TYPE = "zip"
SERVICE_NAME = "AppLovinQualityService"
HOME_PATH = File.expand_path('~')
REPO_DOMAIN = MAVEN_REPO.split('/').first
POD_REPO_PATHS = ["#{HOME_PATH}/.#{SERVICE_NAME}/#{REPO_DOMAIN}/iOSPodsRepo2"]
SCRIPT_PATH = File.expand_path(__FILE__)
SCRIPT_DIR = File.dirname(SCRIPT_PATH)
APPLOVIN_QUALITY_SERVICE_DIR = SERVICE_NAME
APPLOVIN_QUALITY_SERVICE_PATH = "#{SCRIPT_DIR}/#{APPLOVIN_QUALITY_SERVICE_DIR}"
SCRIPT_NAME = File.basename($0).escape
BUILD_ROOT = File.dirname(ENV["BUILD_ROOT"]) rescue nil
BUILD_ROOT_TMP = BUILD_ROOT.nil? ? nil : BUILD_ROOT + ".tmp"
LAST_UPDATE_PATH = "#{HOME_PATH}/.#{SERVICE_NAME}/repos_last_update.txt"
LAST_USED_FILE = "last_used.txt"
VERSION_FILE = "version.txt"
INSTALLER_APP = "#{SERVICE_NAME}Installer.app"
INSTALLER_EXECUTABLE = "Contents/MacOS/#{SERVICE_NAME}Installer"
PLUGIN_APP = "#{SERVICE_NAME}Plugin.app"
SETUP_SCRIPT = "#{SERVICE_NAME}Setup.rb"
CLIENT_FRAMEWORK = "#{SERVICE_NAME}.framework"
CLIENT_XCFRAMEWORK = "#{SERVICE_NAME}.xcframework"
CLIENT_FRAMEWORK_DSYM = "#{CLIENT_FRAMEWORK}.dSYM"
LICENSE_FILE = "APPLOVIN-LICENSE.txt"
LICENSES_DIR = "Third-Party-Licenses"
CHECKSUM_PREFIX = "\n#MD5="
CHECK_INTERVAL = 24*3600
CLEANUP_INTERVAL = 90*24*3600
LOWEST_VERSION = Gem::Version.new('0')
CUTOFF_VERSION = Gem::Version.new('5.0.0')
@specific_version = nil
@target_id = nil
@target_name = nil
@local_frameworks = nil
@nofetch = false
@noupdater = false
CMD_INSTALL = "install"
CMD_UNINSTALL = "uninstall"
CMD_UPDATE = "update"
CMD_NOFETCH = "nofetch"
SUPPORT_EMAIL = "devsupport@applovin.com"
def get_own_api_key
app_setup_content = File.read(__FILE__)
app_data_match = app_setup_content.scan(/(APPLICATION\_DATA.*APPLICATION\_DATA)/m)
return nil if app_data_match.count == 0
app_data = app_data_match.last.first
content_json = app_data.gsub(/APPLICATION\_DATA/,'')
content = JSON.parse(content_json)
api_key = content["api_key"]
api_key = nil if api_key == ''
api_key
rescue Exception => msg
raise "Failed to find api_key in #{__FILE__} file, reason: #{msg}"
end
def fetch_setup_script
read_last_update
return if ((Time.now - @last_update) < CHECK_INTERVAL) && @specific_version.nil?
return if !@specific_version.nil? && @specific_version == Gem::Version.new(@version)
api_key = get_own_api_key
uri = URI(SCRIPT_FETCH_URI)
puts @specific_version.nil? ? "Requesting latest setup script from #{uri.host}..." : "Requesting setup script version #{@specific_version.to_s.description} from #{uri.host}..."
postdata = {:api_key => api_key, :maven_repo => MAVEN_REPO}
postdata[:maven_user] = MAVEN_USER unless MAVEN_USER.nil?
postdata[:maven_password] = MAVEN_PASSWORD unless MAVEN_PASSWORD.nil?
postdata[:setup_script_version] = @version unless @version.nil?
postdata[:requested_script_version] = @specific_version.to_s.description unless @specific_version.nil?
res = Net::HTTP.post(uri, postdata.to_json, "Content-Type" => "application/json")
status_code = res.code.to_i
if status_code == 304
puts "You already have the latest setup script."
return
end
if status_code >= 300
msg = "Failed to fetch setup script, status code: #{status_code}"
if status_code >= 400 && status_code < 500 && !@specific_version.nil?
raise msg + ". Version #{@specific_version.to_s.description} may not exist"
else
raise msg
end
end
raise "Failed to fetch setup script" if res.body.nil?
md5_index = res.body.index(CHECKSUM_PREFIX)
raise "Missing file checksum" if md5_index.nil?
raise "Illegal setup file structure" if res.body.length < 7
file_checksum = res.body[(md5_index+CHECKSUM_PREFIX.length)..(res.body.length-1)]
original_setup_file = res.body[0..md5_index - 1]
content_md5 = Digest::MD5.hexdigest(original_setup_file)
raise "Setup file checksum failed" if content_md5 != file_checksum
Dir.mktmpdir do |tmpdir|
tmp_script_path = "#{tmpdir}/#{SCRIPT_NAME}"
File.open(tmp_script_path, 'w') { |file| file.write(res.body) }
FileUtils.mv(tmp_script_path, SCRIPT_PATH) if File.exist?(tmp_script_path)
puts "Setup script has been updated."
end
rescue Exception => e
raise e.message if e.message.include?("code: 4")
puts "Error downloading setup script: #{e.message}.\nSetup script will not be updated in this run."
end
def read_last_update
@last_update = Time.at(0)
content = File.read(LAST_UPDATE_PATH)
return unless content
@last_update = DateTime.iso8601(content.strip).to_time
rescue Exception
# ignore
end
def update_last_update
File.open(LAST_UPDATE_PATH, 'w') { |file| file.write(DateTime.now.to_s) } rescue nil
end
def read_pod_version
path = "#{APPLOVIN_QUALITY_SERVICE_PATH}/#{VERSION_FILE}"
content = File.read(path)
Gem::Version.new(content)
rescue Exception
LOWEST_VERSION
end
def clean_repo_periodically(repo_path)
return unless File.exist?(repo_path)
dirs = Dir.entries(repo_path).select{|f| f != '.' && f != '..' && File.directory?(File.join(repo_path, f)) }
dirs.each do |dir|
path = "#{repo_path}/#{dir}"
last_used_path = "#{path}/#{LAST_USED_FILE}"
content = File.read(last_used_path) rescue nil
next unless content
last_used = DateTime.iso8601(content.strip).to_time rescue Time.at(0)
FileUtils.rm_rf(path) if (Time.now - last_used) > CLEANUP_INTERVAL
end
end
def init_repo(repo_path)
repo = {path: repo_path}
FileUtils.mkdir_p repo_path
dirs = Dir.entries(repo_path).select{|f| f != '.' && f != '..' && File.directory?(File.join(repo_path, f)) }
repo[:local_versions] = dirs.map{|dir| Gem::Version.new(dir) rescue nil}.compact.sort
repo[:highest_local_version] = repo[:local_versions].max || LOWEST_VERSION
repo
end
def init_and_cleanup
@repos = POD_REPO_PATHS.map{|repo_path| init_repo(repo_path)}
# cleaning old versions from repos
POD_REPO_PATHS.map{|repo_path| clean_repo_periodically(repo_path)}
MAVEN_ARTIFACT_IDS.each_with_index {|artifact_id, i| @repos[i][:artifact_id] = artifact_id}
@repos[0][:minimal_version] = CUTOFF_VERSION
read_last_update
FileUtils.mkdir_p APPLOVIN_QUALITY_SERVICE_PATH
@previous_pod_version = read_pod_version
FileUtils.rm_rf BUILD_ROOT_TMP rescue nil
end
def confirm_xcode_projects_exist
project_paths = Dir.glob("#{SCRIPT_DIR}/*.xcodeproj")
raise "Could not find Xcode project file(s) under #{SCRIPT_DIR}\nPlease copy this script to your Xcode project directory and run it from there." unless project_paths.count > 0
project_paths
end
def update_last_used(pod_zip)
last_used_path = "#{File.dirname(pod_zip)}/#{LAST_USED_FILE}"
File.open(last_used_path, 'w') { |file| file.write(DateTime.now.to_s) } rescue nil
end
def fetch(uri_str, limit=10)
raise 'Too many HTTP redirects' if limit == 0
uri = URI(uri_str)
Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https', :verify_mode => OpenSSL::SSL::VERIFY_NONE, :open_timeout => 120) do |http|
request = Net::HTTP::Get.new uri.request_uri
request.basic_auth MAVEN_USER, MAVEN_PASSWORD unless MAVEN_USER.nil?
resp = http.request(request)
case resp
when Net::HTTPSuccess
resp.body
when Net::HTTPRedirection
location = resp['location']
fetch(location, limit - 1)
else
raise "Server response code: #{resp.code}"
end
end
rescue Exception => e
raise "AppLovin Maven repository returned the following error: #{e.message}"
end
def get_latest_remote_version(artifact_id)
begin
uri = URI("https://#{MAVEN_REPO}/#{MAVEN_GROUP}/#{artifact_id}/maven-metadata.xml")
maven_data = fetch(uri)
rescue Exception => e
raise "Failed to access AppLovin Maven repository: #{e.message}"
end
begin
doc = REXML::Document.new maven_data
group_id = doc.root.elements["groupId"].text
found_artifact_id = doc.root.elements["artifactId"].text
version = doc.root.elements["versioning/release"].text
rescue Exception => e
raise "Failed to parse maven metadata received from server: #{e.message}"
end
# Some basic validation
if group_id != MAVEN_GROUP_ID || found_artifact_id != artifact_id
raise "Failed to properly parse Maven metadata"
end
Gem::Version.new(version) rescue LOWEST_VERSION
rescue Exception => e
puts e.message
nil
end
def download_pod(repo, version)
repo_path = repo[:path]
artifact_id = repo[:artifact_id]
version_str = version.to_s.description
pod_zip = "#{repo_path}/#{version_str}/#{artifact_id}-#{version_str}.zip"
puts "Downloading #{SERVICE_NAME} Pod version #{version_str}..."
uri = URI("https://#{MAVEN_REPO}/#{MAVEN_GROUP}/#{artifact_id}/#{version_str}/#{artifact_id}-#{version_str}.#{MAVEN_ARTIFACT_TYPE}")
content = fetch(uri)
puts "Validating #{SERVICE_NAME} Pod checksum..."
uri = URI("https://#{MAVEN_REPO}/#{MAVEN_GROUP}/#{artifact_id}/#{version_str}/#{artifact_id}-#{version_str}.#{MAVEN_ARTIFACT_TYPE}.md5")
md5 = fetch(uri)
raise "Failed to validate Pod MD5 checksum" unless Digest::MD5.hexdigest(content) == md5
FileUtils.mkdir_p "#{repo_path}/#{version_str}"
open(pod_zip, "wb") do |file|
file.write(content)
end
pod_zip
end
def get_pod_zip
if @specific_version
@repos.each do |repo|
if repo[:local_versions].include?(@specific_version)
version_str = @specific_version.to_s.description
pod_zip = "#{repo[:path]}/#{version_str}/#{repo[:artifact_id]}-#{version_str}.zip"
if File.exist?(pod_zip)
puts "Requested version #{version_str} of #{SERVICE_NAME} Pod found locally"
return pod_zip
end
end
end
end
repos = @repos
update_required = false
version = @specific_version
if version.nil?
repos.each do |repo|
if repo[:highest_local_version] && @command != CMD_INSTALL
if (Time.now - @last_update) < CHECK_INTERVAL
version = [version || LOWEST_VERSION, repo[:highest_local_version]].max
else
update_required = true
end
end
end
end
puts "#{SERVICE_NAME} Pod was updated within the last #{CHECK_INTERVAL.to_i / 3600} hours, no update required" unless update_required || version.nil? || version != Gem::Version.new(@version)
if version.nil?
remote_version = repos.map{|repo| get_latest_remote_version(repo[:artifact_id])}.compact.max
highest_local_version = repos.map{|repo| repo[:highest_local_version]}.max
if remote_version
puts "#{SERVICE_NAME} Pod has the latest version, no update required" if highest_local_version == remote_version
update_last_update
version = remote_version
else
version = highest_local_version
end
end
raise "#{SERVICE_NAME} Pod version could not be evaluated" if version.nil? || version == LOWEST_VERSION
repo = @repos.select{|repo| version >= repo[:minimal_version]}.last
raise "Requested version too low: #{version}, aborting download" unless repo.size > 0
version_str = version.to_s.description
pod_zip = "#{repo[:path]}/#{version_str}/#{repo[:artifact_id]}-#{version_str}.zip"
pod_zip = download_pod(repo, version) unless File.exist?(pod_zip)
pod_zip
end
def dirs_identical?(*dirs)
signatures = dirs.map{|dir| Dir["#{dir}/**/*"].select{|p| File.file?(p)}.sort.map{|p| Digest::MD5.hexdigest(File.read(p)) rescue nil}.compact.join}
signatures.uniq.size == 1
end
def copy_dsym_to_products_dir
built_products_dir = ENV["BUILT_PRODUCTS_DIR"]
return if built_products_dir.nil?
src = "#{APPLOVIN_QUALITY_SERVICE_PATH}/#{CLIENT_FRAMEWORK_DSYM}"
return unless File.exist?(src)
dest = "#{built_products_dir}/#{CLIENT_FRAMEWORK_DSYM}"
return if File.exist?(dest) && dirs_identical?(src, dest)
FileUtils.rm_rf(dest)
FileUtils.cp_r(src, dest)
end
def refresh_framework_in_products_dir
built_products_dir = ENV["BUILT_PRODUCTS_DIR"]
return if built_products_dir.nil?
sdk_name = ENV["SDK_NAME"]
return if sdk_name.nil?
dest = "#{built_products_dir}/#{CLIENT_FRAMEWORK}"
return unless File.exist?(dest)
return unless File.exist?("#{APPLOVIN_QUALITY_SERVICE_PATH}/#{CLIENT_XCFRAMEWORK}")
framework_paths = Dir["#{APPLOVIN_QUALITY_SERVICE_PATH}/#{CLIENT_XCFRAMEWORK}/ios-*"]
simulator_framework_path = framework_paths.select{|p|p.include?("x86")}.first
device_framework_path = framework_paths.select{|p| !p.include?("x86")}.first
return if simulator_framework_path.nil? || device_framework_path.nil?
framework_path = sdk_name.start_with?("iphonesimulator") ? simulator_framework_path : device_framework_path
src = "#{framework_path}/#{CLIENT_FRAMEWORK}"
return if dirs_identical?(src, dest)
FileUtils.rm_rf(dest)
FileUtils.cp_r(src, built_products_dir)
end
def inflate_pod(pod_zip)
Dir.mktmpdir do |tmpdir|
puts "Extracting Pod #{File.basename(pod_zip)}..."
dest = File.join(tmpdir,SERVICE_NAME)
is_ok = system("unzip -qq -o \"#{pod_zip}\" -d \"#{dest}\"")
raise "Failed to unzip the #{SERVICE_NAME} Pod" unless is_ok
unless dirs_identical?(APPLOVIN_QUALITY_SERVICE_PATH, dest)
FileUtils.rm_rf(APPLOVIN_QUALITY_SERVICE_PATH)
FileUtils.mv(dest, APPLOVIN_QUALITY_SERVICE_PATH)
end
end
update_last_used(pod_zip)
copy_dsym_to_products_dir
refresh_framework_in_products_dir
end
def inflate_installer(tmpdir)
installer_path = "#{tmpdir}/AppLovinQualityServiceInstaller.app"
FileUtils.mkdir_p installer_path
installer_zip_path = "#{installer_path}.zip"
installer_zip_content = Base64.strict_decode64(@installer)
File.open(installer_zip_path, 'w') { |file| file.write(installer_zip_content) }
is_ok = system("unzip -qq -o \"#{installer_zip_path}\" -d \"#{installer_path}\"")
raise "Failed to inflate installer into #{tmpdir}" unless is_ok
installer_path
end
def exec_valid?(installer_exec)
return false unless File.exist?(installer_exec)
return false unless File.size(installer_exec) > 1000
true
end
def invoke_installer
Dir.mktmpdir do |tmpdir|
installer_path = inflate_installer(tmpdir)
installer_exec = "#{installer_path}/#{INSTALLER_EXECUTABLE}"
additional_params = ""
additional_params += " -targetid \"#{@target_id}\"" if @target_id
additional_params += " -targetname \"#{@target_name}\"" if @target_name
additional_params += " -localframeworks \"#{@local_frameworks}\"" if @local_frameworks
additional_params += " -noupdater" if @noupdater
invocation = "\"#{installer_exec}\" #{@command} -projectdir \"#{SCRIPT_DIR}\" -setupfile \"#{SCRIPT_NAME}\" -nosig #{additional_params}"
invocation += " -version \"#{@specific_version.to_s.description}\"" if @specific_version
is_ok = system(invocation)
unless is_ok
raise "Installer executable cannot be found. It may happen due to antivirus software blocking it" unless exec_valid?(installer_exec)
raise "Failed to apply the #{SERVICE_NAME} Pod"
end
end
end
def move_build_root
FileUtils.mv(BUILD_ROOT, BUILD_ROOT_TMP) rescue nil unless BUILD_ROOT.nil?
end
def update_setup
pod_version = read_pod_version
if @specific_version && (@specific_version < CUTOFF_VERSION)
pod_setup_path = "#{APPLOVIN_QUALITY_SERVICE_PATH}/#{SETUP_SCRIPT}"
pod_setup_content = File.read(pod_setup_path) rescue nil
return if pod_setup_content.nil?
current_setup_content = File.read(SCRIPT_PATH)
["MAVEN_REPO","MAVEN_USER","MAVEN_PASSWORD"].each do |symbol|
original_line = current_setup_content[/#{symbol}\s*=.*?$/]
pod_setup_content.gsub!(/#{symbol}\s*=.*?$/, original_line)
end
app_data_match = current_setup_content.scan(/(APPLICATION\_DATA.*APPLICATION\_DATA)/m)
return if app_data_match.count == 0
app_data = app_data_match.last.first
pod_setup_content.gsub!(/APPLICATION\_DATA.*APPLICATION\_DATA/m, app_data)
File.write(SCRIPT_PATH, pod_setup_content)
puts "Updated setup script to version #{pod_version}"
end
end
def read_arg(i)
if i+1 < ARGV.count
return ARGV[i+1], i+1
else
return nil, i
end
end
def read_specific_version(version_str)
begin
@specific_version = Gem::Version.new(version_str)
rescue
raise("Illegal version string: #{version_str}")
end
end
def read_command_line_options(i)
while i < ARGV.count
case ARGV[i]
when "-targetid"
@target_id, i = read_arg(i)
when "-targetname"
@target_name, i = read_arg(i)
when "-localframeworks"
@local_frameworks, i = read_arg(i)
when "-version"
specific_version, i = read_arg(i)
read_specific_version(specific_version)
when "-noupdater"
@noupdater = true
else
raise "Unrecognized command line option #{ARGV[i]}"
end
i += 1
end
end
def read_command_line_args
@nofetch = ARGV[0] == CMD_NOFETCH
i = @nofetch ? 1 : 0
@command = case ARGV[i]
when nil
CMD_INSTALL
when CMD_INSTALL
read_command_line_options(i+1)
CMD_INSTALL
when CMD_UNINSTALL
read_command_line_options(i+1)
CMD_UNINSTALL
when CMD_UPDATE
if ARGV[i+1]
if ARGV[i+1].start_with?('-')
read_command_line_options(i+1)
else
read_specific_version(ARGV[i+1])
end
end
CMD_UPDATE
else
raise "Unrecognized command: #{ARGV[i]}\n" +
"Usage: ruby #{File.basename($0)} <command> <options>\n" +
"Where:\n" +
" command - #{CMD_INSTALL} | #{CMD_UNINSTALL} | #{CMD_UPDATE}\n"
end
end
def print_end_message
if @command == CMD_INSTALL
puts "If you decide to uninstall the #{SERVICE_NAME} Pod at any stage, you may run:"
puts "ruby #{File.basename($0)} uninstall"
puts "In case you encounter any issues, please contact us at #{SUPPORT_EMAIL}"
elsif @command == CMD_UNINSTALL
puts "Please make sure run 'clean' before rebuilding your project."
puts "If you uninstalled #{SERVICE_NAME} from all of your targets and projects and no longer need it,"
puts "you may manually delete the #{APPLOVIN_QUALITY_SERVICE_DIR} directory under your project,"
puts "as well as delete this #{File.basename($0)} script."
puts "In case you encounter any issues, please contact us at #{SUPPORT_EMAIL}"
end
end
def print_framework_update_message(operation, framework_name)
puts "--------------------NOTE--------------------"
puts "Project file has been #{operation} to include #{framework_name}."
puts "This operation is done once and causes the build to stop."
puts "All should work fine once you clean your project and re-build it."
end
# === Start of script ====
begin
STDOUT.sync = true
read_command_line_args
if @nofetch
puts "Executing script version: #{@version}" if @version
init_and_cleanup
if @command == CMD_UPDATE
pod_zip = get_pod_zip
inflate_pod(pod_zip)
else
confirm_xcode_projects_exist
unless @command == CMD_UNINSTALL
pod_zip = get_pod_zip
inflate_pod(pod_zip)
end
puts
invoke_installer
print_end_message
end
update_setup
puts "..DONE"
else
puts "AppLovin Quality Service Xcode Setup Script"
puts "Copyright (c) 2026 AppLovin. All Rights Reserved."
puts "Script version: #{@version}" if @version
fetch_setup_script
args = ARGV.drop(1).map{|arg| '"' + arg + '"'}.join(' ')
exec("ruby \"#{SCRIPT_PATH}\" #{CMD_NOFETCH} #{@command} #{args}")
end
rescue Exception => e
abort("\n#{e.message}\n#{SERVICE_NAME} setup FAILED\n\n")
end
#MD5=dd470e6093b3c9035e92be102ff49c39