#!/usr/bin/env groovy
/*
* Licensed Materials - Property of IBM* and/or HCL**
* UrbanCode Deploy
* UrbanCode Build
* UrbanCode Release
* AnthillPro
* (c) Copyright IBM Corporation 2011, 2017. All Rights Reserved.
* (c) Copyright HCL Technologies Ltd. 2018. All Rights Reserved.
*
* U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
* GSA ADP Schedule Contract with IBM Corp.
*
* * Trademark of International Business Machines
* ** Trademark of HCL Technologies Limited
*/

import com.urbancode.air.AirPluginTool
import com.urbancode.commons.util.processes.Processes
import com.urbancode.shell.Shell

final String lineSep = System.getProperty('line.separator')
final String osName = System.getProperty('os.name').toLowerCase(Locale.US)
final String pathSep = System.getProperty('path.separator')
final boolean windows = (osName =~ /windows/)
final boolean vms = (osName =~ /vms/)
final boolean os9 = (osName =~ /mac/ && !osName.endsWith('x'))
final boolean unix = (pathSep == ':' && !vms && !os9)
final boolean zos = (osName =~ /z\/os/)
final boolean ibmi = (osName =~ /os\/400/)
final File PLUGIN_HOME = new File(System.getenv().get("PLUGIN_HOME"))
File AGENT_HOME;
File tempDir = null

if (System.getenv().get("AGENT_HOME")){
    AGENT_HOME = new File(System.getenv().get("AGENT_HOME"))
    tempDir = new File(AGENT_HOME, "var" + File.separator + "temp")
}
final Processes processes = new Processes()

def String getArch() {
    String result
    String arch = System.getProperty("os.arch").toLowerCase(Locale.US)

    if (arch.indexOf("amd64") > -1 || arch.indexOf("x64") > -1 || arch.indexOf("x86_64") > -1) {
        result = "x64"
    }
    else if (arch.indexOf("x86") > -1 || arch.indexOf("386") > -1 || arch.indexOf("486") > -1 ||
             arch.indexOf("586") > -1 || arch.indexOf("686") > -1 || arch.indexOf("pentium") > -1) {
        result = "x86"
    }
    else if (arch.indexOf("ia64") > -1 || arch.indexOf("itanium") > -1 || arch.indexOf("ia-64") > -1) {
        result = "ia64"
    }
    else if (arch.indexOf("ppc") > -1 || arch.indexOf("powerpc") > -1) {
        result = "ppc"
    }
    else if (arch.indexOf("sparc") > -1) {
        result = "sparc"
    }
    else if (arch.indexOf("parisc") > -1 || arch.indexOf("pa_risc") > -1 || arch.indexOf("pa-risc") > -1) {
        result = "parisc"
    }
    else if (arch.indexOf("alpha") > -1) {
        result = "alpha"
    }
    else if (arch.indexOf("mips") > -1) {
        result = "mips"
    }
    else if (arch.indexOf("arm") > -1) {
        result = "arm"
    }
    else {
        result = "unknown"
    }
    return result
}

if (windows) {
    def arch = getArch()
    def libraryPath = new File(PLUGIN_HOME, "lib/native/${arch}/WinAPI.dll")
    System.setProperty("com.urbancode.winapi.WinAPI.dllPath", libraryPath.absolutePath)
}

// TODO replace this with groovy 1.6 String.denormalize() method call
/**
 * Return a String with lines (separated by LF, CR/LF, or CR) terminated by the platform specific line separator
 * @param value the string to denormalize
 * @return the denormalized string
 */
def denormalizeLines = { String value ->
    return value.replaceAll(/\n|\r\n|\r/, java.util.regex.Matcher.quoteReplacement(lineSep))
}

def workDir = new File('.').canonicalFile
final AirPluginTool apt = new AirPluginTool(new File(args[0]), new File(args[1]));
final Properties props = apt.getStepProperties();

def defaultCharset = null
if (AGENT_HOME) {
    final def agentInstalledProps = new File(AGENT_HOME, "conf/agent/installed.properties")
    final def agentProps = new Properties();
    try {
        final def agentInputStream = new FileInputStream(agentInstalledProps);
        agentProps.load(agentInputStream);
    }
    catch (IOException e) {
        throw new RuntimeException(e);
    }
    defaultCharset = agentProps['system.default.encoding']
}

final def directoryOffset = props['directoryOffset']
final def runAsDaemon = Boolean.valueOf(props['runAsDaemon'])

def interpreter = props['shellInterpreter']
def scriptBody = props['scriptBody']
def outputFilePath = props['outputFile']

final def verbose = false

def exitCode = -1

//
// Validation
//

if (directoryOffset) {
    workDir = new File(workDir, directoryOffset).canonicalFile
}

if (workDir.isFile()) {
    throw new IllegalArgumentException("Working directory ${workDir} is a file!")
}

if (!scriptBody) {
    throw new IllegalArgumentException("Shell Script is required")
}

boolean daemon = vms ? false : runAsDaemon

//
// Determine OS specific interpreter and script extension if needed
//

def scriptExt = null
if (!interpreter) {

    if (windows) {
        scriptExt = '.bat'
    }
    else if (vms) {
        scriptExt = '.com'

        // Work dir needs to be a genuine VMS style path for inclusion in DCL
        def path = workDir.path.tokenize('/\\')

        def buf = path.first()+':'    // device
        def directories = path.tail() // everything after first element
        if (directories) {
            buf += '['+directories.join('.')+']'
        }

        scriptBody = "\$ SET DEFAULT ${buf}\n\$ ${scriptBody}"
    }
    else if (unix) {
        def isRexx = null
        if(zos){
            //A REXX program is recognized by the word REXX (not case-sensitive)
            //as the first word on the first line and within a REXX comment.
            if(scriptBody.startsWith('/*') && scriptBody.toLowerCase().contains("rexx")){
                //The last line needs to be a new line
                scriptBody = scriptBody + "\n"
                isRexx = true
            }
        }

        // unix doesn't care about extension, but does about #!
        if (!scriptBody.startsWith('#!') && !isRexx) {
            // defaultShell not on all agents right now
            //interpreter = defaultShell ?: '/bin/sh'
            interpreter = '/bin/sh'
        }

        // Use the PASE shell on IBMi
        if(ibmi) {
            interpreter = '/QOpenSys/usr/bin/sh'
        }
    }
    else {
        // uknown platform and unknown interpreter, use defaultShell as interpreter if available
        interpreter = defaultShell
    }
}


//
// Construct output file (if needed)
//

outputFilePath = outputFilePath?.trim()
File outputFile = null
if (outputFilePath) {
    outputFile = new File(outputFilePath)
    if (!outputFile.isAbsolute()) {
        outputFile = new File(workDir, outputFile.getPath()).absoluteFile
    }
}

//
// Create workDir and scriptFile
//

// ensure work-dir exists
workDir.mkdirs()

/*
ImpersonationToken impersonationToken = null
String user = user?.resolvedStr?.trim()
if (user != null) {
    String password = password?.resolvedStr?.trim();
    impersonationToken = new ImpersonationToken(user, password)
}
*/

// write script content (for groovy, filename must be legal java classname chars)
// If the AGENT_HOME was not set, possibly because the agent is very old, the tempDir will be null,
// resulting in createTempFile using the system default temp diretory
final def scriptFile = File.createTempFile("shell_command_", scriptExt?:'.tmp', tempDir)
try {
    if (defaultCharset) {
        scriptFile.setText(denormalizeLines(scriptBody), defaultCharset)
    }
    else {
        scriptFile.text = denormalizeLines(scriptBody) // write out with platform specific line endings
    }

    //
    // Build Command Line
    //
    def commandLine = []
    if (interpreter) {
        if (windows) {
            commandLine.add('cmd')
            commandLine.add('/C')
            commandLine.add(interpreter) // tokenize?
            commandLine.add(scriptFile.absolutePath)
        }
        else {
            commandLine.addAll(interpreter.tokenize())
            commandLine.add(scriptFile.absolutePath)
        }
    }
    else {
        if (unix) {
            // fix unix execute bit
            def chmod = ['chmod', '+x', scriptFile.absolutePath].execute()
            chmod.outputStream.close()
            chmod.consumeProcessOutput(System.out, System.err)
            chmod.waitFor() // TODO check exit code?
        }
        commandLine.add(scriptFile.absolutePath)
    }

    //
    // Launch Process
    //
    def shell = new Shell(commandLine as String[]);
    shell.workingDirectory = workDir
    shell.daemon = daemon
    shell.outputFile = outputFile
    //shell.impersonationToken = impersonationToken

    // print out command info
    println("")
    println("command line: ${commandLine.join(' ')}")
    println("script content: ")
    println('-------------------------------')
    println(scriptBody)
    println('-------------------------------')
    println("working directory: ${workDir.path}")

    if (daemon) {
        def message = "running as daemon: output "
        message += (outputFile ? "redirected to ${outputFile.path}" : 'discarded')
        println(message)

        //
        // WORKAROUND: Had a problem where running a daemon process in urban-deploy allowed
        // the plugin runtime process to terminate as expected, but the agent continued to
        // read from stdout/err. Closing the streams BEFORE running the daemon process
        // prevents that. The simplest explanation is that the handles from agent<->plugin_runtime
        // pipes are being inherited, but the cause for that is unknown. It may be that handle
        // inheritance flags get inherited with the handle.
        //
        // This caused issues with windows 7 where nothing would actually be redirected to the output file

        //System.out.close();
        //System.err.close();
        //System.in.close();
    }
    else {
        println('===============================')
        println("command output: ")
    }

    def proc = null
    if ( vms ) {
        proc = Runtime.runtime.exec(commandLine as String[])
    }
    else {
        shell.execute()
        proc = shell.process
    }

    if (!daemon) {
        def hook = {
            proc.destroy();
        }
        Runtime.getRuntime().addShutdownHook(hook as Thread);

        // Trigger early load of this class. AIX Java 5 has a bug the produces a
        // LinkageError if this class is loaded normally. Most likely, it is an
        // issue with concurrent loading of the class in different threads.
        Class.forName("com.urbancode.commons.util.IO");

        // handle process IO
        proc.outputStream.close()           // close process stdin
        def outFuture = processes.redirectOutput(proc, System.out);
        def errFuture = processes.redirectError(proc, System.err);
        outFuture.await()
        errFuture.await()
        proc.waitFor()

        Runtime.getRuntime().removeShutdownHook(hook as Thread);

        // print results
        println('===============================')
        println("command exit code: ${proc.exitValue()}")
        println("")

        exitCode = proc.exitValue()
    }
}
finally {
    if (!daemon) {
        scriptFile.delete()
    }
}

if (!daemon) {
    System.exit(exitCode)
}
