Commit: 156640260ed9029d9e185f3f2f6a2d511158a41c Author: matt <matt@mattfickenlaptop.local> Mon, 5 Mar 2012 16:22:32 -0800 Parents: 53f7b2745bbd67b3c41b495fb9f2caf4559c28a9 Branches: master
Link: http://git.php.net/?p=pftt2.git;a=commitdiff;h=156640260ed9029d9e185f3f2f6a2d511158a41c Log: updating with recent changes Former-commit-id: 096834badedd11b759feba673808ff2080328c5a Changed paths: A PFTT/lib/AbstractHostList.groovy A PFTT/lib/Build.groovy A PFTT/lib/Command.groovy A PFTT/lib/Host.groovy A PFTT/lib/HostList.groovy A PFTT/lib/HostsManager.groovy A PFTT/lib/LocalHost.groovy A PFTT/lib/Middleware.groovy A PFTT/lib/RemoteHost.groovy A PFTT/lib/SSHHost.groovy A PFTT/lib/Scenario.groovy A PFTT/lib/base.groovy A PFTT/lib/case_runner.groovy
diff --git a/PFTT/lib/AbstractHostList.groovy b/PFTT/lib/AbstractHostList.groovy new file mode 100644 index 0000000..b89b54b --- /dev/null +++ b/PFTT/lib/AbstractHostList.groovy @@ -0,0 +1,557 @@ +package com.mostc.pftt + +import groovy.lang.Closure; + +import java.util.ArrayList; + + +abstract class AbstractHostList extends ArrayList { + +// def size(value=null) { +// //length() returns number of hosts +// // //length(value) returns number of hosts that returned given value +// case value.null? +// when true +// super +// when false +// count_values(value) +// end +// } + +// def count_values(value) { +// // count number of hosts that returned given value +// c = 0 +// each { k, v -> +// if eq_value(v, value) +// c += 1 +// end +// end +// c +// } + +// def keys(value=null) { +// // //keys() returns an array of hosts +// // //keys(value) returns a Host::Array of hosts that returned given value +// case value.null? +// when true +// super +// when false +// slice_value(value) +// end +// } + +// def slice_value(value) { +// // returns a Host::Array of hosts that returned given value +// ret = Host::Array.new +// each do |k, v| +// if eq_value(v, value) +// ret.push(k) +// end +// end +// ret +// } + + def eq_value(a, b) { + a == b + } + + def which(cmd, ctx=null) { + each_ret(cmd) { host, sub_cmd -> + host.which(sub_cmd, ctx) + } + } + + def hasCmd(cmd, ctx=null) { + each_ret(cmd) { host, sub_cmd -> + host.hasCmd(sub_cmd, ctx) + } + } + + def exec_pw(posix_cmd, windows_cmd, ctx, opts={}) { + each_ret { host -> + if (host.isPosix(ctx)) + host.exec(sub_cmd(host, posix_cmd), ctx, opts) + else + host.exec(sub_cmd(host, windows_cmd), ctx, opts) + } + } + + def exec_ok(cmd, ctx, opts={}) { + each_ret(cmd) { host, sub_cmd -> + host.exec_ok(sub_cmd, ctx, opts) + } + } + + def exec_pw_ok(posix_cmd, windows_cmd, ctx, opts={}) { + each_ret { host -> + if (host.isPosix(ctx)) + host.exec_ok(sub_cmd(host, posix_cmd), ctx, opts) + else + host.exec_ok(sub_cmd(host, windows_cmd), ctx, opts) + } + } + + def cmd_pw(posix_cmd, windows_cmd, ctx, opts={}) { + each_thread { host -> + if (host.isPosix(ctx)) + host.cmd(sub_cmd(host, posix_cmd), ctx, opts) + else + host.cmd(sub_cmd(host, windows_cmd), ctx, opts) + } + } + + def exec(command, ctx, opts={}) { + each_ret(command) { host, sub_cmd -> + host.exec(sub_cmd, ctx, opts) + } + } + + def cmd(cmdline, ctx) { + each_ret(cmdline) { host, sub_cmdline -> + host.cmd(sub_cmdline, ctx) + } + } + + def line(cmdline, ctx) { + each_ret(cmdline) { host, sub_cmdline -> + host.line(sub_cmdline, ctx) + } + } + + def isPosix(ctx=null) { + each_ret { host -> + host.isPosix(ctx) + } + } + + def isWindows(ctx=null) { + each_ret { host -> + host.isWindows(ctx) + } + } + + def isLonghorn(ctx=null) { + each_ret { host -> + host.isLonghorn(ctx) + } + } + + def isBSD(ctx=null) { + each_ret { host -> + host.isBSD(ctx) + } + } + + def isFreeBSD(ctx=null) { + each_ret { host -> + host.isFreeBSD(ctx) + } + } + + def isLinux(ctx=null) { + each_ret { host -> + host.isLinux(ctx) + } + } + + def isRedhat(ctx=null) { + each_ret { host -> + host.isRedhat(ctx) + } + } + + def isFedora(ctx=null) { + each_ret { host -> + host.isFedora(ctx) + } + } + + def isDebian(ctx=null) { + each_ret { host -> + host.isDebian(ctx) + } + } + + def isUbuntu(ctx=null) { + each_ret { host -> + host.isUbuntu(ctx) + } + } + + def isGentoo(ctx=null) { + each_ret { host -> + host.isGentoo(ctx) + } + } + + def isSUSE(ctx=null) { + each_ret { host -> + host.isSUSE(ctx) + } + } + + def isSolaris(ctx=null) { + each_ret { host -> + host.isSolaris(ctx) + } + } + + // def utype? ctx=null + // each_ret do |host| + // host.utype?(ctx) + // end + // end + // + // def windows_and_utype ctx=null + // // TODO + // end + + def unquote_line(cmdline, ctx) { + each_ret(cmdline) { host, sub_cmdline -> + host.unquote_line(sub_cmdline, ctx) + } + } + + def line_prefix(prefix, cmd, ctx) { + each_ret(cmd) { host, sub_cmd -> + host.line_prefix(prefix, sub_cmd, ctx) + } + } + + def reboot(ctx=null) { + each_ret { host -> + host.reboot(ctx) + } + } + + def reboot_wait(seconds, ctx=null) { + each_ret { host -> + host.reboot_wait(seconds, ctx) + } + } + + def isRebooting(ctx=null) { + each_ret { host -> + host.rebooting(ctx) + } + } + + def env_values(ctx=null) { + each_ret { host -> + host.env_values(ctx) + } + } + + def env_value(name, ctx=null) { + each_ret { host -> + host.env_value(name, ctx) + } + } + + def name(ctx=null) { + each_ret { host -> + host.name(ctx) + } + } + + def mkdir(path, ctx) { + each_ret(path) { host, sub_path -> + host.mkdir(sub_path, ctx) + } + } + + def mktmpdir(ctx, path=null, suffix='') { + each_ret(path) { host, sub_path -> + host.mktmpdir(ctx, sub_path, suffix) + } + } + + def mktmpfile(suffix, ctx, content=null) { + each_ret { host -> + host.mktmpfile(suffix, ctx, content) + } + } + + // alias :mktempfile :mktmpfile + // alias :mktempdir :mktmpdir + + def delete_if(path, ctx) { + each_ret(path) { host, sub_path -> + host.delete_if(sub_path, ctx) + } + } + + def upload(from, to, ctx, opts={}) { + each_ret(to) { host, sub_to -> + host.upload(from, sub_to, ctx, opts) + } + } + + def download(from, to, ctx) { + each_ret(to) { host, sub_to -> + host.download(from, sub_to, ctx) + } + } + + def upload_force(local, remote, ctx, opts={}) { + each_ret(remote) { host, sub_remote -> + host.upload_force(local, sub_remote, ctx, opts) + } + } + + def upload_if_not(local, remote, ctx) { + each_ret(remote) { host, sub_remote -> + host.upload_if_not(local, sub_remote, ctx) + } + } + + def glob(path, spec, ctx) { + each_ret(path) { host, sub_path -> + host.glob(sub_path, spec, ctx) + } + } + + def list(path, ctx) { + each_ret(path) { host, sub_path -> + host.list(sub_path, ctx) + } + } + + def mtime(file, ctx=null) { + each_ret(file) { host, sub_file -> + host.mtime(sub_file, ctx) + } + } + + def write(string, path, ctx) { + // TODO + each_ret(path) { host, sub_path -> + host.write(string, sub_path, ctx) + } + } + + def read(path, ctx) { + each_ret(path) { host, sub_path -> + host.read(sub_path, ctx) + } + } + + def cwd(ctx=null) { + each_ret { host -> + host.cwd(ctx) + } + } + + def cd(path, hsh, ctx=null) { + // TODO + each_ret(path) { host, sub_path -> + host.cd(sub_path, hsh, ctx) + } + } + + def directory(path, ctx=null) { + each_ret(path) { host, sub_path -> + host.directory(sub_path, ctx) + } + } + + def open_file(path, flags='r', ctx=null, Closure block=null) { + each_ret(path) { host, sub_path -> + host.open_file(sub_path, flags, ctx, block) + } + } + + def isRemote() { + each_ret { host -> + host.isRemote() + } + } + + def delete(path, ctx=null) { + // TODO + each_ret(path) { host, sub_path -> + host.delete(sub_path, ctx) + } + } + + def exist(path, ctx=null) { + each_ret(path) { host, sub_path -> + host.exist(sub_path, ctx) + } + } + + // alias :exist :exist? + // alias :exists :exist? + // alias :exists? :exist? + + def copy(from, to, ctx, opts={}) { + each_ret(to) { host, sub_to -> + host.copy(from, sub_to, ctx, opts) + } + } + + def move(from, to, ctx) { + each_ret(to) { host, sub_to -> + host.move(from, sub_to, ctx) + } + } + + def time(ctx=null) { + each_ret { host -> + host.time(ctx) + } + } + + def setTime(time, ctx=null) { +// // TODO +// ret = VSA.new +// each_thread(ret) do |host| +// ret[host] = host.time = time +// end +// ret + } + + def shell(ctx=null) { + each_ret { host -> + host.shell(ctx) + } + } + + def systeminfo(ctx=null) { + each_ret { host -> + host.systeminfo(ctx) + } + } + + def processor(ctx=null) { + each_ret { host -> + host.processor(ctx) + } + } + + def isX86(ctx=null) { + each_ret { host -> + host.isX86(ctx) + } + } + + def isX64(ctx=null) { + each_ret { host -> + host.isX64(ctx) + } + } + + def isARM(ctx=null) { + each_ret { host -> + host.isARM(ctx) + } + } + + def number_of_processors(ctx=null) { + each_ret { host -> + host.number_of_processors(ctx) + } + } + + def systemroot(ctx=null) { + each_ret { host -> + host.systemroot(ctx) + } + } + + def systemdrive(ctx=null) { + each_ret { host -> + host.systemdrive(ctx) + } + } + + def desktop(ctx=null) { + each_ret { host -> + host.desktop(ctx) + } + } + + def userprofile(ctx=null) { + each_ret { host -> + host.userprofile(ctx) + } + } + + def appdata(ctx=null) { + each_ret { host -> + host.appdata(ctx) + } + } + + def appdata_local(ctx=null) { + each_ret { host -> + host.appdata_local(ctx) + } + } + + def tempdir(ctx) { + each_ret { host -> + host.tempdir(ctx) + } + } + + // alias :tmpdir :tempdir + + def osname(ctx=null) { + each_ret { host -> + host.osname(ctx) + } + } + + // alias :os_name osname + + def hasDebugger(ctx) { + each_ret { host -> + host.hasDebugger(ctx) + } + } + + def on_error(host, ex) { + } + + def sub_cmd(host, cmd) { + // important that 'host' variable is named 'host' + eval("$cmd") + } + + def each_ret(path=null) {// TODO , &block) { +// ret = VSA.new +// each_thread do |host| +// ret[host] = block.call(host, path.null? ? null : sub_cmd(host, path)) +// end +// ret + } + + protected def each_thread(ret=null) {// TODO , &block) { +// tq = @thread_manager.new_queue +// +// this.each do |host| +// // TODO coordinate with the threads in exec! system +// // +// // TODO @thread_queue +// tq.add_task do +// begin +// block.call(host) +// rescue +// if !ret.null? and !ret.has_key?(host) +// // caller is supposed to return true|false for host +// // to indicate success|failure +// // +// // make sure this gets set to false (since this failed) +// ret[host] = false +// end +// on_error(host, $!) +// end // begin +// end // add_task +// +// end // each +// +// tq.execute + } // def each_thread + +} // class AbstractHostList diff --git a/PFTT/lib/Build.groovy b/PFTT/lib/Build.groovy new file mode 100644 index 0000000..34a6c08 --- /dev/null +++ b/PFTT/lib/Build.groovy @@ -0,0 +1,5 @@ +package com.mostc.pftt + +abstract class Build { + +} diff --git a/PFTT/lib/Command.groovy b/PFTT/lib/Command.groovy new file mode 100644 index 0000000..7ea4d42 --- /dev/null +++ b/PFTT/lib/Command.groovy @@ -0,0 +1,105 @@ +package com.mostc.pftt + +import com.mostc.pftt.util.StringUtil + +/** Handles generation and manipulation of command line commands without ugly String parsing. + * + * Also allows custom evaluation of the result (success|failure, etc....) of command execution. + * + * + */ + +class Command { + public static final int SUCCESS = 0 + String program, args=[], arg_sep=[], arg2_sep=[], args_args2_sep=[], args2=[] + /** the output expected in the STDOUT and STDERR streams from the program, for + * the run to be considered successful. + * + * @see #isSuccess + * + * if an empty string then STDOUT|STDERR is expected to be empty + * if null, then run is successful regardless of STDOUT|STDERR being empty or not empty + */ + String expect_stdout, expect_stderr + int expect_exit_code = SUCCESS + def exe_opts + + def ExpectedCommand(String program=null) { + this.program = program + } + + /** decides if Actual run of the command was successful. + * + * by default, this is based on the Actual command matching the expected STDOUT, STDERR and exit code. + * + * this may be overridden to evaluate success differently. + * + * @param actual + * @return + */ + def isSuccess(actual) { + actual.cmd_str == toString() && (expect_stdout==null||expect_stdout == actual.stdout) && (expect_stderr==null||expect_stderr == actual.stderr) && expect_exit_code == actual.exit_code + } + + /** creates an Actual instance to store information about an instance of a run of this Command. + * + * @param cmd_line + * @param stdout + * @param stderr + * @param exit_code + * @return + */ + def createActual(String cmd_line, String stdout, String stderr, int exit_code) { + new Actual(cmd_line, stdout, stderr, exit_code) + } + + public class Actual { + String cmd_line, stdout, stderr + int exit_code + + def Actual(String cmd_line, String stdout, String stderr, int exit_code) { + this.cmd_line = cmd_line + this.stdout = stdout + this.stderr = stderr + this.exit_code = exit_code + } + + def isCmdNotFound() { + return ( stderr.contains('bash:') && exit_code == 127 ) + } + + def isSuccess() { + Command.this.isSuccess(Actual.this) + } + + @Override + String toString() { + cmd_line + } + } + + @Override + String toString() { + def str = program + if (!args.isEmpty()) { + str += ' ' + str += args_str + } + if (!args2.isEmpty()) { + str += arg2_sep + str += args2_str + } + return str + } + + def args_str() { + StringUtil.flatten(args) + StringUtil.join(arg_sep, args) + } + + def args2_str() { + StringUtil.flatten(args2) + StringUtil.join(arg2_sep, args2) + } + +} // end class Command diff --git a/PFTT/lib/Host.groovy b/PFTT/lib/Host.groovy new file mode 100644 index 0000000..be33246 --- /dev/null +++ b/PFTT/lib/Host.groovy @@ -0,0 +1,1695 @@ +package com.mostc.pftt + +/** Host handles file system, system info and program execution for local and remote hosts abstracting + * and automatically handling differences between operating systems. + +1. use #systemdrive in place of C: + + Windows 2003r2 and above will use a different drive letter (not C) + if its not installed on the first *FAT or NTFS Partition (first partition with type set to + FAT or NTFS during installation process). + + host.exec_pw!("/usr/bin/unzip", "$host.systemdrive/php-sdk/bin/unzip") + host.exec_pw!("/usr/bin/p7zip", "$host.programfiles/7-Zip/7zFM") + host.exec_pw!("/usr/bin/p7zip", "$host.programfiles86/7-Zip/7zFM") + +2. Multiple hosts variable substitution + + when using host.systemdrive, host.programfiles, host.tempdir, etc... with Hosts::Array, pass in using ' ' not " " + and name the variable 'host'. then it will be automatically substituted for each host. + + hosts.exec_pw!("/usr/bin/unzip", '$host.systemdrive/php-sdk/bin/unzip') + hosts.exec_pw!("/usr/bin/p7zip", '$host.programfiles/7-Zip/7zFM') + hosts.exec_pw!("/usr/bin/p7zip", '$host.programfiles86/7-Zip/7zFM') + +3. Important Methods + +Host#systemdir +Host#programfiles +Host#tempdir +Host#homedir +Host#documents +Host#downloads +Host#desktop +Host#exec_pw +Host#exec +Host#cmd_pw +Host#exist +Host#directory +Host#mktempfile +Host#mktempdir +Host#copy +Host#move +Host#upload +Host#download +Host#list +Host#glob + + +*/ + +abstract class Host { + + static def sub(String base, String path) { + if ( path.startsWith(base) ) { + return path.substring(base.length()+1); + } + return path; + } + + static def join(String... array) { + StringBuilder sb = new StringBuilder() + for (def e : array ) { + if (sb.length() > 0) + sb.append('/') + sb.append(e.toString()) + } + sb.toString() + } +// def this.join *path_array +// path_array.join('/') +// end + +// static def administrator_user (platform) { +// if platform == :windows { +// return 'administrator' +// } else { +// return 'root' +// } +// } + + static def no_trailing_slash(path) { + if (path.endsWith('/') || path.endsWith('\\')) { + path = path[0..path.length-1] + } + return path + } + + static def to_windows_path(path) { + // remove \\ from path too. they may cause problems on some Windows SKUs + path = path.replaceAll('/', '\\').replaceAll('\\\\', '\\').replaceAll('\\\\', '\\') + if (path[0] == "\\" || path[0] == '/') { + // TODO + path = path[1..path.length] + } + path + } + + static def to_posix_path(path) { + path.replaceAll('\\\\', '/') + path.replaceAll('//', '/') + path.replaceAll('//', '/') + } + +// static def fs_op_to_cmd(fs_op, src, dst) { +// // TODO +// return case fs_op +// when :move +// when :copy +// when :delete +// when :mkdir +// when :list +// when :glob +// when :cwd +// when :cd +// when :exist +// when :is_dir +// when :open +// when :upload +// null +// when :download +// null +// else +// null +// end +// } + + def registry_query(key, value, type, ctx) { + for ( def line : host.lines('REG QUERY "'+key+'" /v '+value, ctx) ) { + if ( line.trim().startsWith("$value ")) { + def parts = line.split("$type ") + if ( parts.length > 1 ) { + return parts[1].trim() + } + } + } + null + } + + def registry_update(key, value, data, type, ctx) { + registry_add(key, value, data, type, ctx) + } + + def registry_add(key, value, data, type, ctx) { + host.exec('REG ADD "'+key+'" /v '+value+' /d '+data+' /t '+type+' /f', ctx) + } + + def registry_delete(key, value, ctx) { + host.exec('REG DELETE "'+key+'" /v '+value+' /f', ctx) + } + + def read_config(path, ctx=null) { + // /etc/[program]/ + // %ProgramFiles%/[program]/ + // TODO + } + + class Config { + def write() { + } + } + + def write_config(path, config, ctx=null) { + // TODO + } + + def registry_import(file, ctx=null) { + // TODO + } + + def registry(key, ctx=null) { + // TODO + } + + class RegistryKey { + def hive() { + } + def export(file) { + } + def add() { + } + def query() { + } + def delete() { + } + } + + def isWDWOS() { + false // LATER + } + + def getWDWOS() { + // OS::WDW::MS::Win::Win7::x64::SP1 + // OS::WDW::MS::Win::Win7::x86::SP0 + null // LATER + } + + def isVMGuest() { + false // LATER + } + + def getVMHost() { + null // LATER + } + + def getVMHostManager() { + if (isVMGuest()) { + // LATER how to share vm_host() instances amongst guest host instances? + def h = getVMHost() + if (h) { + return h.vm_host_mgr() + } + } + // Host::VMManager.new (save and share w/ //clone too!) + null // LATER + } + + // LATER int findProcess(String, svc_host=false) + // LATER kill(String) and kill(int) + + def isRunning(exe, ctx=null, svc_host=false) { + // checks if the named process is running (if 1+ processes are running that match the name) + // + // windows note: some processes(ex: Internet Information Services) are run within a 'service host' + // in which case the process will show up as 'svchost.exe'. these processes are 'windows services'. + // if the process you're checking for is a 'windows service', set svc_host=true. + // note: service name is case sensitive! + // note: most/all services don't include .exe in the name (checking will fail if this doesn't match up) + // note: svc_host is ignored on posix. you can set it to true in case of windows without causing a problem on posix. + // + // windows note: if svc_host=false, and if you ommit the .exe from the process name, this will + // check for both processes named with the given name and the given name + '.exe' (returns + // true if either are running). + // + if (isWindows(ctx)) { + if (svc_host) { + return exec("tasklist /FI \"SERVICES eq $exe\"").output.contains(exe) + } + def r = exec("tasklist /FI \"IMAGENAME eq $exe\"").output.contains(exe) + if (!r && !exe.endsWith('.exe')) { + return isRunning("$exe.exe", ctx) + } + return r + } else { + return exec("pgrep $exe", ctx).output.length() > 1 + } + } + + enum EProcessor { + x64, x86, arm, mips, alpha, ppc, sparc, unknown + } + + def processor(ctx=null) { + // TODO cache result + def a = isPosix(ctx) ? cmd('uname -a', ctx).output : env_value('PROCESSOR_ARCHITECTURE', ctx) + if (a == null||a.length()==0) { + return EProcessor.unknown + } + a = a.toLowerCase() + if (a.contains('x86_64') || a.contains('i86pc')) + EProcessor.x64 + else if (a.contains('x86')) + EProcessor.x86 + else if (a.contains('arm')) + EProcessor.arm + else if (a.contains('mips')) + EProcessor.mips + else if (a.contains('alpha')) + EProcessor.alpha + else if (a.contains('ppc')) + EProcessor.ppc + else if (a.contains('sparc')) + EProcessor.sparc + else + EProcessor.unknown + } + + def isX86(ctx=null) { + // x64 also supports x86 + isX86Only(ctx) || isX64(ctx) + } + + def isX86Only(ctx=null) { + processor(ctx) == EProcessor.x86 + } + + def isX64(ctx=null) { + processor(ctx) == EProcessor.x64 + } + + def isARM(ctx=null) { + processor(ctx) == EProcessor.arm + } + + def number_of_processors(ctx=null) { + // TODO cache result + // counts number of CPUs in host + def p = 0 + if (isWindows(ctx)) { + p = env_value('NUMBER_OF_PROCESSORS', ctx) + if (p) { + // TODO get env_value to parse integer, float, bool + p = Integer.parseInt(p) + } + } else { + def cpuinfo = read_lines('/proc/cpuinfo', ctx) + + p = 0 + // each processor will have a line like 'processor : //', followed by lines of info + // about that processor + // + // count number of those lines == number of processors + for (def line : cpuinfo) { + if (line.startsWith('processor')) { + p += 1 + } + } + } + + return p > 0 ? p : 1 // ensure > 0 returned + } // end def number_of_processors + + def username(ctx=null) { + if (isPosix(ctx)) + env_value('USER', ctx) + else + env_value('USERNAME', ctx) + } + + abstract def upload(local_file, remote_path, ctx, opts=[]) + abstract def cwd(ctx=null) + abstract def cd(path, hsh, ctx=null) + abstract def read_lines(path, ctx=null, max_lines=16384) + abstract def read(path) + abstract def directory(path, ctx=null) + abstract def list(path, ctx) + abstract def isRemote() + abstract def mtime(file, ctx=null) + abstract def write(string, path, ctx) + abstract def isAlive() + abstract def env_values(ctx=null) + abstract def env_value(name, ctx=null) + abstract def close() + + def write_lines(lines, path, ctx) { + write(lines.join("\n"), path, ctx) + } + + def isRebooting() { + false + } + + def reboot(ctx) { + // reboots host and waits 120 seconds for it to become available again + reboot_wait(120, ctx) + } + + def reboot_wait(seconds, ctx) { + // + if (isWindows(ctx)) { + exec("shutdown /r /t 0", ctx) + } else { + exec("shutdown -r -t 0", ctx) + } + } + + def nt_version(ctx) { + if (isWindows(ctx)) { + return null + } + + def nt_version = systeminfo_line('OS Version', ctx) + + return nt_version ? nt_version.to_f : 5 // 5 (aka Windows 2000) is earliest supported NT Version + } + + def eol(ctx) { + (isWindows(ctx)) ? "\r\n" : "\n" + } + + def eol_escaped(ctx) { + (isWindows(ctx)) ? "\\r\\n" : "\\n" + } + + def upload_force(local, remote, ctx, mkdir=true) { + delete_if(remote, ctx) + + upload(local, remote, ctx, mkdir) + } + + def copy(from, to, ctx, opts=[]) { + if (ctx) { + ctx.fs_op2(this, EFSOp.copy, from, to) |new_from; new_to| { + return copy(new_from, new_to, ctx, opts) + } + } + + if (opts.hasProperty('mkdir')&&opts.mkdir!=false) { + mkdir(dirname(to), ctx) + } + + copy_cmd(from, to, ctx) + } + + def trash(path, ctx) { + move(path, join(trashdir(ctx), basename(path)), ctx) + } + + def basename(path) { + new File(path).getName() + } + + def dirname(path) { + new File(path).getParent() + } + + def move(from, to, ctx) { + if (ctx) { + ctx.fs_op2(this, EFSOp.move, from, to) |new_from; new_to| { + return move(new_from, new_to, ctx) + } + } + + if (!directory(from)) { + move_file(from, to, ctx) + return + } + + move_cmd(from, to, ctx) + } + + def setTime(time, ctx=null) { + // sets the host's time/date + // TODO posix support + if (isPosix(ctx)) { + exec('date --set="'+time+'"', ctx) + } else { + exec("date $time.month-$time.day-$time.year && time $time.hour:$time.minute-$time.second", ctx) + } + } + + abstract def getTime(ctx=null) + + def systemroot(ctx=null) { + // get's the file system path pointing to where the host's operating system is stored + if (isPosix(ctx)) { + return '/' + } else if (_systemroot) { + return _systemroot + } else { + _systemroot = env_value('SYSTEMROOT', ctx) + return _systemroot + } + } + + def systemdrive_or_homedir(ctx=null) { + // returns systemdrive on windows and the user's home directory on posix + isWindows(ctx) ? systemdrive(ctx) : homedir(ctx) + } + + def systemdrive(ctx=null) { + // gets the file system path to the drive where OS and other software are stored + if (isPosix(ctx)) { + return '/' + } else if (_systemdrive) { + return _systemdrive + } else { + _systemdrive = env_value('SYSTEMDRIVE', ctx) + return _systemdrive + } + } + + def desktop(ctx=null) { + return join(userprofile(ctx), 'Desktop') + } + + def userprofile(ctx=null) { + if (isPosix(ctx)) { + return homedir(ctx) + } + + if (_userprofile) { + return _userprofile + } + + def p = env_value('USERPROFILE', ctx) + + if (exists(p, ctx)) { + return _userprofile = p + } else if (!_homedir) { + return _userprofile = _homedir + } else { + return _userprofile = systemdrive(ctx) + } + } + + def programfiles(ctx=null) { + if (isPosix(ctx)) { + return '/usr/local/bin' + } else if (!_programfiles) { + _program_files = env_value('PROGRAMFILES', ctx) + if (_program_files) { + _program_files = systemdrive(ctx)+'/Program Files/' + } + } + return _programfiles + } + + def programfilesx86(ctx=null) { + if (isPosix()) { + return '/usr/local/bin' + } else if (!_program_files_x86) { + _program_files_x86 = env_value('PROGRAMFILES(x86)', ctx) + if (!exists(_program_files_x86, ctx)) { + _program_files_x86 = env_value('PROGRAMFILES', ctx) + } + if (_program_files_x86) { + _program_files = systemdrive(ctx)+'/Program Files/' + } + } + return _programfiles + } + + def homedir(ctx=null) { + if (!_homedir) { + if (isPosix(ctx)) { + // on linux/unix, its simple, just get $HOME + _homedir = env_value('HOME', ctx) + } else { + // on WIndows, home directory is %HOMEDRIVE%%HOMEPATH% + // (though those variables may be undefined for a Windows user) + _homedir = env_value('HOMEDRIVE', ctx) + if (_homedir) { + // fallback to user profile (or null if not set) + _homedir = _userprofile + } else { + def a = env_value('HOMEPATH', ctx) + if (a) { + if (!_userprofile) { + // fallback to %USERPROFILE% + _homedir = _userprofile + // else: fallback to %HOMEDRIVE% + } + } else { + // add %HOMEPATH% to %HOMEDRIVE% + _homedir += a + } + } + } + } + _homedir + } // end def homedir + + def documents(ctx=null) { + return userprofile(ctx) + '/Documents' + } + + def downloads(ctx=null) { + return userprofile(ctx) + '/Downloads' + } + + def trashdir(ctx=null, drive=null) { + if (isPosix(ctx)) { + return desktop(ctx) + '/Trash' + } + + if (drive) { + drive = systemdrive(ctx) + } + + return drive + '\\$Recycle.Bin' + } + + def appdata(ctx=null) { + if (_appdata) { + return _appdata + } + if (isPosix(ctx)) { + def p = env_value('HOME', ctx) + if (p && exists(p, ctx)) { + return _appdata = p + } + } else { + def p = env_value('USERPROFILE', ctx) + if (p) { + def q = p + '\\AppData\\' + if (exists(q, ctx)) { + return _appdata = q + } else if (exists(p, ctx)) { + mkdir(q, ctx) + return _appdata = q + } + } + } + _appdata = systemdrive(ctx) + } // end def appdata + + def appdata_local(ctx=null) { + if (_appdata_local) { + return _appdata_local + } + if (isPosix()) { + def p = env_value('HOME', ctx) + if (p) { + def q = p + '/PFTT' + if (exists(q, ctx)) { + return _appdata_local = q + } else if (exists(p, ctx)) { + mkdir(q, ctx) + return _appdata_local = q + } + } + } else { + def p = env_value('USERPROFILE', ctx) + if (p) { + def q = p + '\\AppData\\Local' + if (exists(q, ctx)) { + return _appdata_local = q + } else if (exists(p, ctx)) { + mkdir(q, ctx) + return _appdata_local = q + } + } + } + _appdata_local = systemdrive(ctx) + } // end def appdata_local + + def tempdir(ctx) { + if (_tempdir) { + return _tempdir + } + if (isPosix()) { + def p = '/usr/local/tmp' + def q = p + '/PFTT' + if (exists(q, ctx)) { + return _tempdir = q + } else if (exists(p, ctx)) { + mkdir(q, ctx) + return _tempdir = q + } + p = '/tmp' + q = p + '/PFTT' + if (exists(q, ctx)) { + return _tempdir = q + } else if (exists(p, ctx)) { + mkdir(q, ctx) + return _tempdir = q + } + } else { + // try %TEMP%\\PFTT + def p = env_value('TEMP', ctx) + if (p) { + def q = p + '\\PFTT' + if (exists(q, ctx)) { + return _tempdir = q + } else if (exists(p, ctx)) { + mkdir(q, ctx) + return _tempdir = q + } + } + + // try %TMP%\\PFTT + p = env_value('TMP', ctx) + if (p) { + def q = p + '\\PFTT' + if (exists(q, ctx)) { + return _tempdir = q + } else if (exists(p, ctx)) { + mkdir(q, ctx) + return _tempdir = q + } + } + + // try %USERPROFILE%\\AppData\\Local\\Temp\\PFTT + p = env_value('USERPROFILE', ctx) + if (p) { + p = '\\AppData\\Local\\Temp\\' + p + def q = p + '\\PFTT' + if (exists(q, ctx)) { + return _tempdir = q + } else if (exists(p, ctx)) { + mkdir(q, ctx) + return _tempdir = q + } + } + + // try %SYSTEMDRIVE%\\temp\\PFTT + p = systemdrive(ctx)+'\\temp' + q = p + '\\PFTT' + if (exists(q, ctx)) { + return _tempdir = q + } else if (exists(p, ctx)) { + mkdir(q, ctx) + return _tempdir = q + } + + } + + _tempdir = systemdrive(ctx) + } // end def tempdir + + + def systeminfo(ctx=null) { + // gets information about the host (as a string) including CPUs, memory, operating system (OS dependent format) + if (!_systeminfo) { + if (isPosix(ctx)) + _systeminfo = exec('uname -a', ctx).output + "\n" + exec('cat /proc/meminfo', ctx).output +"\n" + exec('cat /proc/cpuinfo', ctx).output // LATER?? glibc version + else + _systeminfo = exec('systeminfo', ctx).output + } + return _systeminfo + } + + static def os_short_name(os) { + os = os.replaceAll('Windowsr', 'Win') + os = os.replaceAll('Microsoft', '') + os = os.replaceAll('Server', '') + os = os.replaceAll('Developer Preview', 'Win 8') + os = os.replaceAll('Win 8 Win 8', 'Win 8') + os = os.replaceAll('Full', '') + os = os.replaceAll('Installation', '') + os = os.replaceAll("\\(", '') + os = os.replaceAll("\\)", '') + os = os.replaceAll('tm', '') + os = os.replaceAll('VistaT', 'Vista') + + os = os.replaceAll('Windows', 'Win') + os = os.replaceAll('/', '') + + // remove common words + os = os.replaceAll('Professional', '') + os = os.replaceAll('Standard', '') + os = os.replaceAll('Enterprise', '') + os = os.replaceAll('Basic', '') + os = os.replaceAll('Premium', '') + os = os.replaceAll('Ultimate', '') + os = os.replaceAll('GNU', '') + if (!os.contains('XP')) { + // XP Home != XP Pro + os = os.replaceAll('Home', '') + } + os = os.replaceAll('Win Win', 'Win') + os = os.replaceAll("\\(R\\)", '') + os = os.replaceAll(',', '') + os = os.replaceAll('Edition', '') + os = os.replaceAll('2008 R2', '2008r2') + os = os.replaceAll('2003 R2', '2003r2') + os = os.replaceAll('RTM', '') + os = os.replaceAll('Service Pack 1', '') + os = os.replaceAll('Service Pack 2', '') + os = os.replaceAll('Service Pack 3', '') + os = os.replaceAll('Service Pack 4', '') + os = os.replaceAll('Service Pack 5', '') + os = os.replaceAll('Service Pack 6', '') + os = os.replaceAll('Microsoft', '') + os = os.replaceAll('N/A', '') + os = os.replaceAll('PC', '') + os = os.replaceAll('Server', '') + os = os.replaceAll('-based', '') + os = os.replaceAll('Build', '') + // + os = os.replaceAll('6.1.7600', '') + os = os.replaceAll('6.1.7601', '') + os = os.replaceAll('6.0.6000', '') + os = os.replaceAll('6.0.6001', '') + os = os.replaceAll('6.0.6002', '') + os = os.replaceAll('5.1.3786', '') + os = os.replaceAll('5.1.3787', '') + os = os.replaceAll('5.1.3788', '') + os = os.replaceAll('5.1.3789', '') + os = os.replaceAll('5.1.3790', '') + os = os.replaceAll('5.0.2600', '') + os = os.replaceAll('5.0.2599', '') + os = os.replaceAll('5.2.SP2', '') + os = os.replaceAll('5.2', '') + os = os.replaceAll('7600', 'SP0') // win7/win2k8r2 sp0 + os = os.replaceAll('7601', 'SP1') // win7/win2k8r2 sp1 + os = os.replaceAll('6000', 'SP0') // winvista/win2k8 sp0 + os = os.replaceAll('6001', 'SP1') // winvista/win2k8 sp1 + os = os.replaceAll('6002', 'SP2') // winvista/win2k8 sp2 + os = os.replaceAll('3786', 'SP0') // win2k3 sp0? + os = os.replaceAll('3787', 'SP1') // win2k3 sp1? + os = os.replaceAll('3788', 'SP0') // win2k3r2 sp0? + os = os.replaceAll('3789', 'SP1') // win2k3r2 sp1? + os = os.replaceAll('3790', 'SP2') // win2k3r2 sp2 + os = os.replaceAll('2600', 'SP3') // windows xp sp3 + os = os.replaceAll('2599', 'SP2') // windows xp sp2? + // + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + os = os.replaceAll(' ', ' ') + + return os.trim() + } // end static def os_short_name + + def os_name_short(ctx=null) { + osname_short(ctx) + } + + def osname_short(ctx=null) { + return Host.os_short_name(osname(ctx)) + } + + def os_name(ctx=null) { + osname(ctx) + } + + def osname(ctx=null) { + // returns the name, version and hardware architecture of the Host's operating system + // ex: Windows 2008r2 SP1 x64 + if (!this._osname) { + if (isPosix(ctx)) { + this._osname = line('uname -om', ctx) + } else { + osname = systeminfo_line('OS Name', ctx) + osname += ' ' + // get service pack too + osname += systeminfo_line('OS Version', ctx) + osname += ' ' + // and cpu arch + osname += systeminfo_line('System Type', ctx) // x86 or x64 + this._osname = osname + + } + } + this._osname + } + + def line_prefix(prefix, cmd, ctx) { + // executes the given cmd, splits the STDOUT output by lines and then returns + // only the (last) line that starts with the given prefix + // + line = line(cmd, ctx) + if (line.startsWith(prefix)) { + line = line.substring(prefix.length()) + } + return line.strip() + } + + def silence_stderr(str) { + isPosix() ? "$str 2> /dev/null" : "$str 2> NUL" + } + + def silence_stdout(str) { + isPosix() ? "$str > /dev/null" : "$str 1> NUL" + } + + def silence(str) { + isPosix() ? "$str 1> /dev/null 2>&1" : "$str 1> NUL 2>&1" + } + + def devnull() { + isPosix() ? '/dev/null' : 'NUL' + } + + def hasDebugger(ctx) { + return debugger(ctx) != null + } + + def debug_wrap(cmd_line, ctx) { + def dbg = debugger(ctx) + if (dbg) { + if (isPosix()) { + return dbg+" --args "+cmd_line + } else if (isWindows()) { + return dbg+" "+cmd_line + } + } + return cmd_line + } + + def debugger(ctx) { + if (isPosix(ctx)) { + if (exists('/usr/bin/gdb', ctx) || exists('/usr/local/gdb', ctx)) { + return 'gdb' + } + } else if (isWindows(ctx)) { + // windbg must be the x86 edition (not x64) because php is only compiled for x86 + // TODO use //programfiles ! + // LATER allow specifying which debugger to use (!php or x64 php) + if (exists("%ProgramFiles(x86)%\\Debugging Tools For Windows (x86)\\windbg.exe", ctx)) + return '%ProgramFiles(x86)%\\Debugging Tools For Windows (x86)\\windbg.exe' + else if (exists('%ProgramFiles(x86)%\\Debugging Tools For Windows\\windbg.exe', ctx)) + return '%ProgramFiles(x86)%\\Debugging Tools For Windows\\windbg.exe' + else if (exists('%ProgramFiles%\\Debugging Tools For Windows (x86)\\windbg.exe', ctx)) + return '%ProgramFiles%\\Debugging Tools For Windows (x86)\\windbg.exe' + else if (exists('%ProgramFiles%\\Debugging Tools For Windows (x86)\\windbg.exe', ctx)) + return '%ProgramFiles%\\Debugging Tools For Windows (x86)\\windbg.exe' + } + if (ctx) { + // prompt context to get debugger for this host (or null) + return ctx.find_debugger(this) + } else { + return null // signal that host has no debugger + } + } + +//attr_accessor :manager // Host::Manager + +def Host(opts={}) { + // TODO +// +////set the opts as properties in the Test::Factor sense +//// opts.each_pair do |key,value| +//// property key => value +//// // LATER merge Test::Factor and host info (name, os_version) +//// // so both Test::Factor and host info can access each other +//// end +// +//// allow override what //name() returns +//// so at least that can be used even +//// when a host is not accessible +//if (opts.has_key(hostname)) { +//_name = opts[hostname] +//} +//// +// + dir_stack = [] + cwd = null +} // def initialize + +def describe() { +//description ||= this.properties.values.join('-').downcase +} + + def shell(ctx=null) { + // returns the name of the host's shell + // ex: /bin/bash /bin/sh /bin/csh /bin/tcsh cmd.exe command.com + if (isPosix(ctx)) { + return env_value('SHELL', ctx) + } else { + return basename(env_value('ComSpec', ctx)) + } + } + + def which(cmd, ctx=null) { + cmd_pw("which $cmd", "where $cmd", ctx).output.trim() + } + + def hasCmd(cmd, ctx=null) { + def w = which(cmd, ctx) + return !w && w.length > 0 + } + + def exec_pw(posix_cmd, windows_cmd, ctx, opts={}) { + if (isPosix(ctx)) { + exec(posix_cmd, ctx, opts) + } else { + exec(windows_cmd, ctx, opts) + } + } + + def exec_ok(command, ctx, opts={}) { + exec(command, ctx, opts).exit_code + } + + def exec_pw_ok(posix_cmd, windows_cmd, ctx, opts={}) { + if (isPosix()) { + exec_ok(posix_cmd, ctx, opts) + } else { + exec_ok(windows_cmd, ctx, opts) + } + } + + class ExecHandle { + def write_stdin(stdin_data) { + } + def read_stderr() { + '' + } + def read_stdout() { + '' + } + def has_stderr() { + read_stdout.length > 0 + } + def has_stdout() { + read_stderr.length > 0 + } + } + + // command: + // command line to run. program name and arguments to program as one string + // this may be a string or Tracing::Command::Expected + // + // options: a hash of + // :env + // keys and values of ENV must be strings (not ints, or anything else!) + // :binmode true|false + // will set the binmode of STDOUT and STDERR. binmode=false on Windows may affect STDOUT or STDERR output + // :by_line true|false + // if true, will read input line by line, otherwise, reads input by blocks + // :chdir + // the current directory of the command to run + // :debug true|false + // runs the command with the host's debugger. if host has no debugger installed, command will be run normally + // :stdin_data '' + // feeds given string to the commands Standard Input + // :null_output true|false + // if true, returns '' for both STDOUT and STDERR. (if host is remote, STDOUT and STDERR are not sent over + // the network) + // :max_len 0+ bytes default=128 kilobytes (128*1024) + // maximum length of STDOUT or STDERR streams(limit for either, STDERR.length+STDOUT.length <= :max_len*2). + // 0 = unlimited + // :timeout 0+ seconds default=0 seconds + // maximum run time of process (in seconds) + // process will be sent SIGKILL if it is still running after that amount of time + // :success_exit_code int, or [int] array default=0 + // what exit code(s) defines success + // note: this is ignored if Command::Expected is used (which evaluates success internally) + // :ignore_failure true|false + // ignores exit code and always assumes command was successful unless + // there was an internal PFTT exception (ex: connection to host failed) + // :exec_handle true|false, default=false + // if true, returns a handle to the process which allows you to close(sigkill|sigterm|sigint) the process later. + // if true, all options (including :timeout) may be used except :success_exit_code and :ignore_failure, as interpretting exit status code + // will then be up to your own code, since you're controlling the process. + // LATER :elevate and :sudo support for windows and posix + // other options are silently ignored + // + // + // returns once command has finished executing + // + // if command is a string returns array of 3 elements. 0=> STDOUT output as string 1=>STDERR 2=>command's exit code (0==success) + // if command is a Command::Expected, returns an Command::Actual + def exec(command, ctx, opts=[], Closure block=null) { + _exec(false, command, opts, ctx, block) + } + + // same as exec! except it returns immediately + def exec_async(command, ctx, opts=[], Closure block=null) { + _exec(true, command, opts, ctx, block) + } + + def cmd_pw(posix_cmd, windows_cmd, ctx, opts={}) { + if (isPosix(ctx)) + cmd(posix_cmd, ctx, opts) + else + cmd(windows_cmd, ctx, opts) + } + + // executes command or program on the host + // + // can be a DOS command, Shell command or a program to run with options to pass to it + // + // some DOS commands (for Windows OSes) are not actual programs, but rather just commands + // to the command processor(cmd.exe or command.com). those commands can't be run through + // exec!( since exec! is only for actual programs). + // + def cmd(cmdline, ctx, opts={}) { + if (isWindows(ctx)) { + cmdline = "CMD /C $cmdline" + } + return exec(cmdline, ctx, opts) + } + + // executes command using cmd! returning the output (STDOUT) from the command, + // with the new line character(s) chomped off + def line(cmdline, ctx=null) { + lines(cmdline, ctx).split("\n")[0] + } + + def lines(cmdline, ctx) { + cmd(cmdline, ctx).output + } + + def unquote_line(cmdline, ctx) { + line(cmdline, ctx).replaceAll(/\"/, '') + } + + def isLonghorn(ctx=null) { + // checks if its a longhorn(Windows Vista/2008) or newer version of Windows + // (longhorn added new stuff not available on win2003 and winxp) + if (is_longhorn) { + return is_longhorn + } + + is_longhorn = nt_version(ctx) >= 6 + + if (ctx) { + is_longhorn = ctx.check_os_generation_detect(longhorn, is_longhorn) + } + + return is_longhorn + } + + def isWindows(ctx=null) { + // returns true if this host is Windows OS + if (is_windows) { + return is_windows + } + if (posix) { + return is_windows = false + } + + // Windows will always have a C:\ even if the C:\ drive is not the systemdrive + // posix doesn't have C: D: etc... drives + is_windows = _exist("C:\\", ctx) + + if (ctx) { + // cool stuff: allow user to override OS detection + is_windows = ctx.check_os_type_detect(windows, is_windows) + } + + return is_windows + } + + def isBSD(ctx=null) { + osname(ctx).contains('BSD') + } + + def isFreeBSD(ctx=null) { + osname(ctx).contains('FreeBSD') + } + + def isLinux(ctx=null) { + osname(ctx).contains('Linux') + } + + def isRedhat(ctx=null) { + isLinux(ctx) and exist('/etc/redhat-release', ctx) + } + + def isFedora(ctx=null) { + isLinux(ctx) and exist('/etc/fedora-release', ctx) + } + + def isDebian(ctx=null) { + isLinux(ctx) and exist('/etc/debian_release', ctx) + } + + def isUbuntu(ctx=null) { + isLinux(ctx) and exist('/etc/lsb-release', ctx) + } + + def isGentoo(ctx=null) { + isLinux(ctx) and exist('/etc/gentoo-release', ctx) + } + + def isSUSE(ctx=null) { + isLinux(ctx) and exist('/etc/SUSE-release', ctx) + } + + def isSolaris(ctx=null) { + osname(ctx).contains('Solaris') + } + +//def utype? ctx=null +//if linux?(ctx) +//:linux +//elsif solaris?(ctx) +//:solaris +//elsif freebsd?(ctx) +//:freebsd +//else +//null +//end +//end + + def isPosix(ctx=null) { + if (posix) { + return posix + } + if (is_windows) { + return posix = false + } + + posix = _exist('/usr', ctx) + + if (ctx) { + // TODO do this for utype too + posix = ctx.check_os_type_detect(posix, posix) + } + + return posix + } + + def make_absolute(path) { + if (isWindows()) { + // support for Windows drive letters + if (path.contains(':')) + return path + } else if (isPosix()) { + if (path.startsWith("/")) + return path + } + _make_absolute(path) + } + + def exist(path, ctx=null) { + path = format_path(path, ctx) + path = make_absolute(path) + + if (ctx) { + ctx.fs_op1(this, EFsOp.exist, path) { new_path -> + return exist(new_path, ctx) + } + } + + _exist(path, ctx) + } + + def exists(path, ctx=null) { + exist(path, ctx) + } + + def format_path(path, ctx=null) { + if (isWindows()) + to_windows_path(path) + else + to_posix_path(path) + } + +// def format_path! path, ctx=null +// case +// when windows?(ctx) then to_windows_path!(path) +// else to_posix_path!(path) +// end +// end + + def pushd(path, ctx=null) { + cd(path, ctx, no_clear=true) + dir_stack.push(path) + } + + def popd(ctx=null) { + def popped = dir_stack.pop + if (popped) + cd(popped, ctx, no_clear=true) + } + + def peekd() { + dir_stack.last + } + + def separator(ctx=null) { + isWindows(ctx) ? '\\' : '/' + } + + def upload_if_not(local, remote, ctx) { + if (exist(remote, ctx)) { + return false + } else { + upload(local, remote, ctx) + return true + } + } + + def delete_if(path, ctx) { + if (exist(path, ctx)) { + delete(path, ctx) + true + } else { + false + } + } + + def delete(glob_or_path, ctx) { + if (ctx) { + ctx.fs_op1(this, EFsOp.delete, glob_or_path) { new_glob_or_path -> + return delete(new_glob_or_path, ctx) + } + } + + glob_or_path = make_absolute(glob_or_path) + + if (!isSafe(glob_or_path)) + throw new IllegalArgumentException() + + if (directory(glob_or_path, ctx)) { + if (isPosix(ctx)) + exec("rm -rf \"$glob_or_path\"", ctx) + else + exec("cmd /C rmdir /S /Q \"$glob_or_path\"", ctx) + } else { + _delete(glob_or_path, ctx) + } + } + + def escape(str, quote=true) { + if (isWindows()) { + s = str.dup +// if (quote) { +// s.replace %Q{"//{s}"} unless s.replaceAll(/(["])/,'\\\\\1').null? +// } +// s.replaceAll(/[\^&|><]/,'^\\1') + s + } else { + s + } + } + + def mkdir(path, ctx) { + path = make_absolute(path) + def parent = dirname(path) + if (!directory(parent)) { + mkdir(parent, ctx) + } + if (!directory(path)) { + _mkdir(path, ctx) + } + } + + def mktmpdir(ctx, path=null, suffix='') { + if (!path) { + path = tempdir(ctx) + } + + path = make_absolute(path) + tries = 10 + try { + dir = File.join( path, String.random(6)+suffix ) +// raise 'exists' if directory? dir + mkdir(dir, ctx) + } catch ( Exception ex ) { +// retry if (tries -= 1) > 0 + throw ex + } + dir + } + + def mktmpfile(suffix, ctx, content=null) { + tries = 10 + try { + path = File.join( tmpdir(ctx), String.random(6) + suffix ) + +// raise 'exists' if exists?(path, ctx) + + if (content) { + write(content, path, ctx) + } + + return path + } catch (Exception ex) { +// retry if (tries -= 1) > 0 + throw ex + } + } + + def isSafe(path) { + true + //make_absolute! path + //insane = case + //when posix? + ///\A\/(bin|var|etc|dev|usr)\Z/ + //else + ///\A[A-Z]:(\/(Windows)?)?\Z/ + //end =~ path + //!insane + } + + def administrator_user(ctx=null) { + if (isWindows(ctx)) { + // LATER? should actually look this up??(b/c you can change it) + return 'administrator' + } else { + return 'root' + } + } + + def name(ctx=null) { + if (!_name) { + // find a name that other hosts on the network will use to reference localhost + if (isWindows(ctx)) + _name = env_value('COMPUTERNAME', ctx) + else + _name = env_value('HOSTNAME', ctx) + } + _name + } + + protected def move_cmd(from, to, ctx) { + from = no_trailing_slash(from) + to = no_trailing_slash(to) + if (isPosix(ctx)) { + cmd("mv \"$from\" \"$to\"", ctx) + } else { + from = to_windows_path(from) + to = to_windows_path(to) + + cmd("move \"$from\" \"$to\"", ctx) + } + } + + protected def copy_cmd(from, to, ctx) { + if (isPosix(ctx)) { + cmd("cp -R \"$from\" \"$to\"", ctx) + } else { + from = to_windows_path(from) + to = to_windows_path(to) + cmd("xcopy /Y /s /i /q \"$from\" \"$to", ctx) + } + } + + protected def _exec(in_thread, command, opts, ctx, block) { + opts = [env:[]] // TODO + cwd = null // clear cwd cache + + if (opts.hasProperty('exec_handle') && !in_thread) + opts.exec_handle = null + + def orig_cmd = command +// TODO if (command instanceof Command) +// // get CmdString for this host +// command = command.to_cmd_string(this) + + // for CmdString + command = command.toString() + // TODO if command.is_a?(Tracing::Command::Expected) + // command = command.cmd_line + // end + + if (ctx) { + ctx.cmd_exe_start(this, command, opts) { new_command -> + return _exec(in_thread, new_command, opts, ctx, block) + } + } + + // begin preprocessing command and options + + // if the program being run in this command (the part of the command before " ") + // has / in the path, convert to \\ for Windows (or Windows might not be able to find the program otherwise) + if (isWindows(ctx)) { + def i + if (command.startsWith('"')) + i = command.index('"', 1) + else + i = command.index(' ', 1) + if (i>0) + command = to_windows_path(command(0, i)) + command.substring(i+1) + } + // + + // + if (opts.hasProperty('null_output') && opts.null_output) + command = silence(command) + // + + if (!opts.hasProperty('env')) + opts.env = false //[] + + if (opts.hasProperty('chdir') && opts.chdir) { + // NOTE: chdir seems to be ignored|invalid on Windows(even Win7) if / is NOT converted to \ !! + // convert the \ / to the correct for this host + opts.chdir = format_path(opts.chdir, ctx) + } + + if (!opts.hasProperty('max_len') || opts.max_len < 0) + opts.max_len = 128*1024 + + // run command in platform debugger + if (opts.hasProperty('debug') && opts.debug) + command = debug_wrap(command) + + // end preprocessing command and options + + def ret + if (in_thread && !(opts.hasProperty('exec_handle') && opts.exec_handle)) { + new Thread() { + void run() { + ret = _exec_thread(command, opts, ctx, block) + } + }.start() + } else { + ret = _exec_thread(command, opts, ctx, block) + } + + if (opts.exec_handle) + // ret is a ExecHandle (like LocalExecHandle or SshExecHandle) + return ret + else if (orig_cmd instanceof Command) + // ret is a Command.Actual + return ret + else + return ret//[ret[0], ret[1], ret[2]] + } // def _exec + + def _exec_thread(command, opts, ctx, block) { + def stdout, stderr, exit_code, ret=null + try { + ret = _exec_impl(command, opts, ctx, block) + + if (opts.hasProperty('exec_handle') && opts.exec_handle) + return ret + + stdout = ret.output + stderr = ret.error + exit_code = ret.exit_code + + // + // don't let output get too large + if (opts.hasProperty('max_len') && opts.max_len > 0) { + if (stdout.length() > opts.max_len) + stdout = stdout.substring(0, opts.max_len) + if (stderr.length() > opts.max_len) + stderr = stderr.substring(0, opts.max_len) + } + // + + // execution done... evaluate and report success/failure + if (ctx) { + // + // decide if command was successful + // + def success = opts.hasProperty('ignore_failure') && opts.ignore_failure ? true : false + if (!success) { + // default evaluation + success = 0 == exit_code + if (command instanceof Command) { + // custom evaluation + // + ret = command.createActual(command.cmd_line, stdout, stderr, exit_code) + // exit_code => share with _exec + success = ret.isSuccess() + + } else if (opts.hasProperty('success_exit_code')) { + // + if (opts.success_exit_code instanceof List) { + // an array of succesful values + // + success = false // override exit_code==0 above + for ( def sec : opts.success_exit_code ) { + if (exit_code == sec) { + success = true + break + } + } + } else if (opts.success_exit_code instanceof Integer) { + // a single successful value + success = exit_code == opts.success_exit_code + } + + } + } + // + + // TODO if success + // ctx.cmd_exe_success(this, command, opts, c_exit_code, stdout+stderr) do |command| + // return _exec_thread(command, opts, ctx, block) + // end + // else + // ctx.cmd_exe_failure(this, command, opts, c_exit_code, stdout+stderr) do |command| + // return _exec_thread(command, opts, ctx, block) + // end + // end + + } // end if (ctx) + + } catch ( Exception ex ) { + ex.printStackTrace(); + // try to include host name (don't call #name() b/c that may exec! again which could fail) + stderr = command+" "+_name+" "+ex.getMessage() + exit_code = -253 + + def sw = new java.io.StringWriter() + def pw = new java.io.PrintWriter(sw) + + ex.printStrackTrace(pw) + + pw.flush() + + stderr += " "+sw.toString() + + throw ex + } + // ret could be set to be a Command.Actual already. otherwise return STDOUT, STDERR, exit-code + if (ret==null) + ret = [output:stdout, error:stderr, exit_code:exit_code] + return ret + } // def _exec_thread + + static def exec_copy_stream(src, dst, type, lh, block, max_len) { + def buf = new byte[128] + def len = 0 + def total_len = 0 + + try { + while ( ( len = src.read(buf, 0, 128)) != -1 ) { + dst.write(buf, 0, len) + + if (block) { + lh.post(type, buf) + block.call(lh) + } + + if (max_len > 0) { + total_len += len + // when output limit reached, stop copying automatically + if (total_len > max_len) + break + } + } + } finally { + src.close() + } + } // end def exec_copy_stream + + static def exec_copy_lines(input, type, lh, block, max_len) { + def o = '' + + input = new BufferedReader(input) +// +// while true do +// try { +// line = input.readLine() +// } catch ( Ex) +// break +// end +// +// if line.null? +// break +// end +// +// line += "\n" +// +// o += line +// +// if block +// lh.post(type, line) +// block.call(lh) +// end +// +// if max_len > 0 +// // when output limit reached, stop copying automatically +// if o.length > max_len +// break +// end +// end +// end +// begin +// input.close +// rescue +// end + + o + } // end def exec_copy_line + + // cache of information about the host (this info is only retrieved once from the actual host) + protected def dir_stack, cwd, _systeminfo, _name, _osname, _systemdrive, _systemroot, posix, is_windows, _appdata, _appdata_local, _tempdir, _userprofile, _programfiles, _programfilesx86, _homedir + +//def clone(clone) +//// copy host information cache to the clone +//// TODO clone._systeminfo = @_systeminfo +//// //clone.lock = @lock +//// clone._osname = @_osname +//// clone._systemroot = @_systemroot +//// clone._systeminfo = @_systeminfo +//// clone._systemdrive = @_systemdrive +//// clone.posix = @posix +//// clone.is_windows = @is_windows +//// clone._name = @_name +//// clone._appdata = @_appdata +//// clone._appdata_local = @_appdata_local +//// clone._tempdir = @_tempdir +//// clone._userprofile = @_userprofile +//// clone._programfiles = @_programfiles +//// clone._programfilesx86 = @_programfilesx86 +//// clone._homedir = @_homedir +//clone +//end + + def systeminfo_line(target, ctx) { + def out_err = systeminfo(ctx) + + out_err.split("\n").each { line -> + if (line.startsWith("$target:")) { + line = line.substr("$target:".length()) + + return line.strip() + } + } + return null + } + +} // end abstract class Host diff --git a/PFTT/lib/HostList.groovy b/PFTT/lib/HostList.groovy new file mode 100644 index 0000000..a4e9351 --- /dev/null +++ b/PFTT/lib/HostList.groovy @@ -0,0 +1,5 @@ +package com.mostc.pftt + +class HostList extends AbstractHostList { + +} diff --git a/PFTT/lib/HostsManager.groovy b/PFTT/lib/HostsManager.groovy new file mode 100644 index 0000000..96cd04b --- /dev/null +++ b/PFTT/lib/HostsManager.groovy @@ -0,0 +1,5 @@ +package com.mostc.pftt + +class HostManager extends HostList { + +} // end class HostManager diff --git a/PFTT/lib/LocalHost.groovy b/PFTT/lib/LocalHost.groovy new file mode 100644 index 0000000..e367272 --- /dev/null +++ b/PFTT/lib/LocalHost.groovy @@ -0,0 +1,273 @@ +package com.mostc.pftt + +import java.util.Timer +import java.util.TimerTask +import java.io.* +import java.lang.ProcessBuilder +import java.io.BufferedReader +import java.io.FileReader +import java.io.ByteArrayOutputStream +import java.io.BufferedOutputStream +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.BufferedInputStream +import java.util.HashMap +import java.lang.Byte + +import com.STDIN; +import com.mostc.pftt.* + +//LocalHost.metaClass { +// upload = {copy} +// download = {copy} +//} +class LocalHost extends Host { + + @Override + def upload(local_file, remote_path, ctx, opts=[]) { + copy(local_file, remote_path, ctx) + } + + @Override + def env_values(ctx=null) { + //Hash.new(System.getenv()) //ENV.keys + } + + @Override + def env_value(name, ctx=null) { + if (name=='HOSTNAME' && isPosix(ctx)) { + // Linux: for some reason this env var is not available + // + // execute program 'hostname' instead + return line('hostname', ctx) + } + System.getenv(name) // LATER ENV[name] + } // end def env_value + + @Override + def reboot_wait(seconds, ctx) { +// if (ctx) { +// // get approval before rebooting localhost +// if (ctx.new(Tracing::Context::SystemSetup::Reboot).approve_reboot_localhost()) { +// return super.reboot_wait(seconds, ctx) +// } +// } else { + return super.reboot_wait(seconds, ctx) +// } + } + +// def clone +// clone = Host::Local.new() +// super(clone) +// end + + @Override + def close() { + // nothing to close + } + +// def toString() { +// if (isPosix()) +// return 'Localhost (Posix)' +// else if (isWindows()) +// return 'Localhost (Windows)' +// else +// return 'Localhost (Platform-Unknown)' +// } + + @Override + def isAlive() { + // was able to call this method therefore must be true (if false, couldn't have called this method) + true + } + + @Override + def write(string, path, ctx) { + // writes the given string to the given file/path + // overwrites the file if it exists or creates it if it doesn't exist + // +// if (ctx) { +// ctx.write_file(self, string, path) |new_string, new_path| { +// return write(new_string, new_path, ctx) +// } +// } + + mkdir(File.dirname(path), ctx) + + output = BufferedOutputStream.new(FileOutputStream.new(path)) + output.write(string, 0, string.length) + output.close + } + + @Override + def read_lines(path, ctx=null, max_lines=16384) { + def lines = [] + def line + + def reader = new BufferedReader(new FileReader(path)) + while ( ( line = reader.readLine() ) != null && !(lines.size()> max_lines)) { + lines.add(line) + } + reader.close() + + lines + } + + @Override + def read(path) { + def output = new ByteArrayOutputStream(1024) + def input = new BufferedInputStream(new FileInputStream(path)) + + copy_stream(input, output) + + input.close + + output.toString + } + + @Override + def getTime(ctx=null) { + new Date() + } + + @Override + def cwd(ctx=null) { + if (ctx) { + ctx.fs_op0(self, EFSOp.cwd) { + return cwd(ctx) + } + } + return Dir.getwd() + } + + @Override + def cd(path, hsh, ctx=null) { + if (ctx) { + ctx.fs_op1(self, EFSOp.cd, path) |new_path| { + return cd(new_path, hsh, ctx) + } + } + + make_absolute(path) + + if (!path) { + // popd may have been called when @dir_stack empty + throw new IllegalArgumentException("path not specified") + } + +// Dir.chdir(path) + + // TODO dir_stack.clear unless hsh.delete(:no_clear) || false + + return path + } // end def cd + + @Override + def directory(path, ctx=null) { + if (ctx) { + ctx.fs_op1(self, EFSOp.is_dir, path) |new_path| { + return directory(new_path, ctx) + } + } + + make_absolute(path) + + new File(path).isDirectory() + } + + // list the immediate children of the given path + @Override + def list(path, ctx) { + if (ctx) { + ctx.fs_op1(self, EFSOp.list, new_path) |new_path| { + return list(new_path, ctx) + } + } + + make_absolute(path) + +// Dir.entries( path ).map! do |entry| +// next null if ['.','..'].include? entry +// entry +// end.compact + } + +// def glob(path, spec, ctx, &blk) { +// if (ctx) { +// ctx.fs_op2(self, :glob, path, spec) |new_path, new_spec| { +// return glob(new_path, new_spec, ctx, blk) +// } +// } +// +// make_absolute(path) +// +// Dir.glob("//{path}///{spec}", &blk) +// } + + @Override + def isRemote() { + false + } + + @Override + def mtime(file, ctx=null) { + File.mtime(file).to_i + } + + static def copy_stream(input, output) { + def tmp = new byte[1024] + while (true) { + //input.available + def len = input.read(tmp, 0, 1024) + if (len < 0) { + break + } + output.write(tmp, 0, len) +// len = 0// TODO input.read(tmp, 0, 1024) +// if len < 0 + break +// end +// // TODO output.write(tmp, 0, len) +// end + } + } // end def copy_stream + + protected + + def _exist(file, ctx) { + new File(file).exists() + } + + def move_file(from, to, ctx) { + new File(from).renameTo(new File(to)) + } + + def _exec_impl(command, opts, ctx, block) { + // TODO + def lh = STDIN.exec_impl(command, null, null, 0, null) + + if (opts.exec_handle) { + return lh + } else { + def ret = lh.run_read_streams() + return [output:ret[0], error:ret[1], exit_code:ret[2]] + } + } // end def _exec_impl + + def _delete(path, ctx) { + make_absolute(path) + + new File(path).delete() + } + + def _mkdir(path, ctx) { + make_absolute(path) + + new File(path).mkdir() + } + + def _make_absolute(path) { + new File(path).getAbsolutePath() + } + +} // end public class LocalHost diff --git a/PFTT/lib/Middleware.groovy b/PFTT/lib/Middleware.groovy new file mode 100644 index 0000000..1b3c839 --- /dev/null +++ b/PFTT/lib/Middleware.groovy @@ -0,0 +1,132 @@ +package com.mostc.pftt + +abstract class Middleware { + Host host + Build build + Scenario scenario + + def Middleware(Host host, Build build, Scenario scenario) { + this.host = host + this.build = build + this.scenario = scenario + } + + def start_test_case_group(scenario, test_case_group) { + } + + def thread_pool_size(test_case_base_class=null) { + 1 + } + + def isInstalled(ctx=null) { + // Override + true + } + + def uninstall(ctx=null) { + // Override + } + + def ensure_installed(ctx=null) { + unless(isInstalled(ctx)) { + install(ctx) + } + } + + def start(ctx=null) { + // Override + ensure_installed(ctx) + } + + def stop(ctx=null) { + // Override + host.close() + } + + def close(ctx=null) { + stop(ctx) + } + + def restart(ctx=null) { + stop(ctx) + start(ctx) + } + + def group_test_cases(test_cases, scenario) { + [test_cases] + } + + protected def shouldDisableAEDebug() { + true + } + + def disableFirewall(ctx=null) { + } + + def install(ctx=null) { + // Override + // client should do install of all middlewares BEFORE running pftt-host + // because installation may require rebooting and pftt-host doesn't support reboots + // + // (could get around this for php or other projects by running pftt-host between reboots if only one middleware can be installed at + // a time, but for php, all middlewares can be installed at once, so just do it all at once (in 1 stage) because it'll be less bad if any install fails) + // + if (host.isWindows(ctx)) { + // turn on file sharing... (add PFTT) share + // make it easy for user to share files with this windows machine + // (during cleanup or analysis or triage, after tests have run) + // + // user can access PHP_SDK and the system drive ( \\hostname\C$ \\hostname\G$ etc...) + // + host.exec("NET SHARE PHP_SDK=$host.systemdrive()\\php-sdk /Grant:$host.username(),Full", ctx) + + // + def error_mode_change = false + if (shouldDisableAEDebug()) { + // remove application exception debuggers (Dr Watson, Visual Studio, etc...) + // otherwise, if php crashes a dialog box will appear and prevent PHP from exiting + host.registry_delete('HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug', 'Debugger', ctx) + // disable Hard Error Popup Dialog boxes (will still get this even without a debugger) + // see http://support.microsoft.com/kb/128642 + // + def query = host.registry_query('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Windows', 'ErrorMode', 'REG_DWORD', ctx) + if (query['REG_DWORD'] != '0x2') { + // check if registry value is already changed (so we don't reboot if we don't have to) + host.registry_add('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Windows', 'ErrorMode', '2', 'REG_DWORD', ctx) + + // for ErrorMode change to take effect, host must be rebooted! + error_mode_change = true + } + + } + // + + // disable windows firewall + disableFirewall(ctx) + + // show filename extensions + host.exec('REG ADD "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced" /v HideFileExt /d 0 /t REG_DWORD /f', ctx) + + if (error_mode_change) { + // for ErrorMode change to take effect, host must be rebooted! + host.reboot(ctx) + } + + } else { + // LATER add share to samba if posix and samba already installed + } +// if @host.windows?(ctx) + +// if @host.credentials // TODO +// @host.exec!('NET SHARE PFTT='+@host.systemdrive+'\\php-sdk /Grant:"'+@host.credentials[:username]+'",Full', ctx) +// end +// + +// end + } // end def install + + def isRunning(ctx=null) { + true + } + +} // end public abstract class Middleware diff --git a/PFTT/lib/RemoteHost.groovy b/PFTT/lib/RemoteHost.groovy new file mode 100644 index 0000000..20f450b --- /dev/null +++ b/PFTT/lib/RemoteHost.groovy @@ -0,0 +1,77 @@ +package com.mostc.pftt + +abstract class RemoteHost extends Host { + + def isRemote() { + true + } + + @Override + def getTime(ctx=null) { + if (isPosix(ctx)) { + new Date(line('date', ctx)) + } else { + // gets the host's time/date + new Date(unquote_line('date /T', ctx)+' '+unquote_line('time /T', ctx)) + } + } + + def canStream() { + false // ssh can + } + + def ensure_7zip_installed(ctx) { + if (tried_install_7zip) { + return installed_7zip + } + + // TODO Util::Install::7zip.install(self) + + installed_7zip = false + } // end def ensure_7zip_installed ctx + + def isRebooting() { + rebooting + } + + def reboot_wait(seconds, ctx) { + super.reboot_wait(seconds, ctx) + + reboot_reconnect_tries = ( seconds / 60 ).to_i + 1 + if (reboot_reconnect_tries < 3) { + reboot_reconnect_tries = 3 + } + + rebooting = true + // will have to recreate sockets when it comes back up + // session() and sftp() will block until then (so any method + // LATER using sftp() or session() will be automatically blocked during reboot(good)) + close + } + + def RemoteHost(opts={}) { + reconnected_after_reboot + } + +// def clone(clone) { +// clone.rebooting = rebooting +// clone.rebooting_reconnect_tries = rebooting_reconnect_tries +// clone.tried_install_7zip = tried_install_7zip +// clone.installed_7zip = installed_7zip +// super(clone) +// } + + protected + + // for //clone() +// attr_accessor :rebooting, :rebooting_reconnect_tries, :tried_install_7zip, :installed_7zip + + def reconnected_after_reboot() { + tried_install_7zip = false + // maybe reverted to a snapshot (if host is a virtual machine) + installed_7zip = false + rebooting = false + rebooting_reconnect_tries = 0 + } + +} // end public class RemoteHost diff --git a/PFTT/lib/SSHHost.groovy b/PFTT/lib/SSHHost.groovy new file mode 100644 index 0000000..5bf8e82 --- /dev/null +++ b/PFTT/lib/SSHHost.groovy @@ -0,0 +1,690 @@ +package com.mostc.pftt; + +// SSH - Secure Shell +// for Linux/UNIX and Windows systems +// +// Supported SSH Servers: +// * OpenSSH +// * Apache SSHD (Apache MINA) +// * SSH Tools +// +// Note: +// * KTS-SSH may have occasional problems with some SFTP operations + +import java.io.* +import java.util.ArrayList + +import com.sshtools.j2ssh.transport.HostKeyVerification +import com.sshtools.j2ssh.SshClient +import com.sshtools.j2ssh.authentication.AuthenticationProtocolState +import com.sshtools.j2ssh.authentication.PasswordAuthenticationClient + +class SSHHost extends RemoteHost { + + @Override + def canStream() { + true + } + + @Override + def close() { + try { + ssh.close() + sftp.close() + } finally { + ssh = null + sftp = null + } + } + +// attr_reader :credentials +// +// def SSHHost (opts={}) { +// @sftp = opts[:sftp] +// @session = opts[:sftp_client] +// @sftp_client = opts[:sftp_client] +// +// @credentials = { +// :address => opts[:address], +// :port => opts[:port]||22, +// :username => opts[:username], +// :password => opts[:password], +// } +// +// super +// } + +// def clone() { +// clone = Host::Remote::Ssh.new( +// // pass session to clone so it'll share the ssh client (if its already connected) +// :session => @session, +// :address => @credentials[:address], +// :port => @credentials[:port], +// :username => @credentials[:username], +// :password => @credentials[:password] +// ) +// super(clone) +// } + + @Override + def isAlive(ctx=null) { + try { + return exist(cwd(ctx), ctx) + } catch (Throwable t) { + if_closed() + return false + } + } + +// def toString() { +// if (isPosix()) { +// "Remote Posix //{@credentials[:address] || name()}" +// } else { +// "Remote Windows //{@credentials[:address] || name()}" +// } +// } + + // checks for the current working directory + // be aware that a command being executed may possibly change this value at some point + // during its execution (in which case, PFTT will only detect that after the command has + // finished execution) + @Override + def cwd(ctx=null) { + if (ctx) { + ctx.fs_op0(self, EFSOp.cwd) { + return cwd(ctx) + } + } + +// cwd ||= format_path(sftp_client(ctx).pwd) + } +// alias :pwd :cwd + + @Override + def cd(path, ctx=null) { + if (!path) { + // popd may have been called when dir_stack empty + throw new NullPointerException("path not specified") + } + if (ctx) { + ctx.fs_op1(self, EFSOp.cd, path) |new_path| { + return cd(new_path, hsh, ctx) + } + } + + path = format_path(path) + path = make_absolute(path) + + + // e-z same command on both + cmd("cd \"$path\"", ctx) + + // cwd is cleared at start of exec, so while in exec, cwd will be empty unless cwd() called in another thread + cwd = path + +// TODO dir_stack.clear unless hsh.delete(:no_clear) || false + + return path + } + + @Override + def env_values(ctx=null) { + env_str = cmd('set', ctx).output + + def env = [] + for (def line : env_str.split("\n")) { + def i = line.index('=') + if (i) { + def name = line.substring(0, i) + def value = line.substring(i+1, line.length()) + + if (isPosix(ctx)) { + if (value.startsWith('"')||value.startsWith("'")) + value = value.substring(1, value.length()-2) + } + + env[name] = value + } + } + + return env + } // end def env_values + + @Override + def env_value(name, ctx=null) { + // get the value of the named environment variable from the host + if (isPosix(ctx)) { + return unquote_line('echo $'+name, ctx) + } else { + def out = unquote_line("echo %$name%", ctx) + if (out == "%$name%") { + // variable is not defined + return '' + } else { + return out + } + } + } // end def env_value + + @Override + def mtime(file, ctx=null) { + if (ctx) { + ctx.fs_op1(self, EFSOp.mtime, file) |new_path| { + return mtime(new_path, ctx) + } + } + + try { + return sftp(ctx).getAttributes(file).getModifiedTime().longValue() + } catch (Exception ex) { + return 0 + } + } + + @Override + def directory(path, ctx=null) { + if (ctx) { + ctx.fs_op1(self, EFSOp.is_dir, path) |new_path| { + return directory(new_path, ctx) + } + } + + try { + return sftp(ctx).getAttributes(file).isDir + } catch(Exception ex) { + if_closed() + return false + } + } + +// def glob(path, spec, ctx, &blk) { +// if (ctx) { +// ctx.fs_op2(self, :glob, path, spec) |new_path, new_spec| { +// return glob(new_path, new_spec, ctx, blk) +// } +// } +// +// l = list(path, ctx) +// unless spec.nil? or spec.length == 0 +// l.delete_if do |e| +// !(e.include?(spec)) +// end +// end +// return l +// } + + @Override + def list(path, ctx) { + if (ctx) { + ctx.fs_op1(self, EFSOp.list, path) |new_path| { + return list(new_path, ctx) + } + } + + try { + list = [] + _sftp = sftp(ctx) + dir = _sftp.openDirectory(path) + children = ArrayList.new() + + while (_sftp.listChildren(dir, children) > -1) { + } + + dir.close() + + i = 0 + while (i < children.size) { + list.push(format_path(children.get(i).getAbsolutePath())) + i += 1 + } + + return list + } catch (Exception ex) { +// if_closed() +//// if (ctx) { +//// ctx.pftt_exception(self, $!, self) +//// } else { +//// // TODO Tracing::Context::Base.show_exception($!) +//// } + throw ex + } + } // end def list + + @Override + def read_lines(path, ctx=null, max_lines=16384) { + def output = new ByteArrayOutputStream() + + sftp(ctx).get(path, output) + + def reader = new BufferedReader(new InputStreamReader(output)) + while ( ( line = reader.readLine() ) != null && !(lines.size()> max_lines)) { + lines.add(line) + } + reader.close() + + lines + } + + @Override + def read(path, ctx=null) { + def output = new ByteArrayOutputStream() + sftp(ctx).get(path, output) + + if (ctx) { + ctx.read_file(self, out, path) |new_path| { + return read(new_path, ctx) + } + } + + output.toString() + } + + @Override + def write(string, path, ctx) { + if (ctx) { + ctx.write_file(self, string, path) |new_string; new_path| { + return write(new_string, new_path, ctx) + } + } + + mkdir(File.dirname(path), ctx) + + // TODO + try { + def input = new ByteArrayInputStream(string.length) + input.write(string, 0, string.length) + sftp(ctx).put(input, path) + return true + } catch (Exception ex) { + if_closed() +// if (ctx) { +// ctx.pftt_exception(self, $!, self) +// } else { +// Tracing::Context::Base.show_exception($!) +// } + throw ex + } + } + + // uploads file/folder to remote host + // + // local_file - local file/folder to copy + // remote_path - remote path to store at + // ctx - context + // opts - hash of + // + // :mkdir - true|false - default=true + // makes a directory to contain multiple files or a folder when uploading + // + // :transport_archive - true|false - default=false + // will (attempt to) archive the local files/folders into a 7zip archive file, + // upload it to the remote host and then decompress it. will install 7zip on + // the remote host if it is not present. If can't upload and execute 7zip on + // remote host, operation will fail. + // see transport_normal_if_decompress_failed + // + // :skip_cached_archive_mtime_check - true|false - default=false + // if true, if archive is already in cache, will use it no matter what. + // if false, will check if the original file(s)/folder(s) have been modified, + // and if yes, will replace the cached archive with the changes. + // + // :transport_normal_if_decompress_failed - true|false - default=true + // if can't decompress fail on remote host, automatically falls back on + // uploading it without archiving + // + // :prearchived - true|false - default=false + // indicates local file has already been archived AOT. archive will be + // uploaded and decompressed IF :transport_archive=true + // + // :transport_archive_no_local_cache - true|false - default=false + // if true, doesn't keep archive in local cache. otherwise, will keep archive in + // local cache to avoid recompressing it for next time. + // + @Override + def upload(local_file, remote_path, ctx, opts=[]) { + if (ctx) { + ctx.fs_op2(self, EFSOp.upload, local_file, remote_path) |new_local_file; new_remote_path| { + return upload(new_local_file, new_remote_path, ctx, opts) + } + } + + // if remote_path exists operation will fail! + // therefore, you should check if it exists first + // then, depending on need, delete() or skip the upload + // + // if local_file is a file, remote_path MUST be a file too!!!!!!!!!!!!! + // (if local_file is a dir, remote_path can be a dir) + // LATER remove this gotcha/rule/limitation (implement using File.basename) + // + if (manager && manager.local_host.isWindows()) { + local_file = to_windows_path(local_file) + } else { + local_file = to_posix_path(local_file) + } + if (isWindows()) { + remote_path = to_windows_path(remote_path) + } else { + remote_path = to_posix_path(remote_path) + } + // + + remote_path = no_trailing_slash(remote_path) + + // + transport_archive = false + if (opts.transport_archive) { + // ensure 7zip is installed on remote host so it can be decompressed + if (ensure_7zip_installed(ctx)) { + unless (opts.prearchived) { + // archive file + local_path = manager.cache_archive(local_path, remote_path, ctx, opts) + } + transport_archive = true + } + } + // + + // ensure the target directory exists (or we'll get an error) + if (opts.mkdir!=false) { + mkdir(File.dirname(remote_path), ctx) + } + try { + + // TODO + sftp(ctx).put(BufferedInputStream.new(FileInputStream.new(local_file)), remote_path) + + return true + } catch (Exception ex) { + if_closed() +// if ctx +// ctx.pftt_exception(self, $!, self) +// else +// Tracing::Context::Base.show_exception($!) + throw ex + } + + // + if (transport_archive) { + // decompress archive + decompress_ret = exec!("7za a -y -o//{File.dirname(remote_path)} //{remote_path}") + +// if (opts[:transport_archive_no_local_cache]) { +// unless (opts[:prearchived]) { +// if (!opts.has_key?(:multi_host) or is_last?) { +// manager.local_host.delete(local_cache_archive, ctx) +// } +// // TODO sync +// +// } +// // else: leave archive in cache for next time +// } + +// if (!decompress_ret[2]) { +// if (opts[:transport_normal_if_decompress_failed]) { +// // upload normal file +// return upload(local_file, remote_path, ctx, {:mkdir=>opts[:mkdir]!=false}) +// } else { +// raise 'RemoteDecompressFail', decompress_ret +// } +// } + + // file uploaded and decompressed ok + + // leave file on remote server if decompression failed for manual triage purposes later + delete(remote_archive, ctx) + } + // + + } // end def upload + + def download(remote_file, local_path, ctx) { + if (ctx) { +// ctx.fs_op2(self, EFsOp.download, remote_file, local_path) do |new_remote_file, new_local_path| { +// return download(new_remote_file, new_local_path, ctx) +// } + } + + // + try { + // TODO + sftp(ctx).get(remote_file, BufferedOutputStream.new(FileOutputStream.new(local_path))) + + return true + } catch (Exception ex) { + if_closed() +// if ctx +// ctx.pftt_exception(self, $!, self) +// else +// Tracing::Context::Base.show_exception($!) + throw ex + } + // + } // end def download + + protected + + def _exist(path, ctx) { + try { + def a = sftp(ctx).getAttributes(path) + return ( a.isFile() || a.isDirectory() ) + } catch (Exception ex) { + if_closed() + return false + } + } + + def move_file(from, to, ctx) { + move_cmd(from, to, ctx) + } + + def copy_file(from, to, ctx, mkdir) { + to = dirname(to) + if (mkdir) { + mkdir(to, ctx) + } + + copy_cmd(from, to, ctx) + } + + def if_closed() { + try { + if (session && session.isConnected()) { + session.disconnect() + session = null + } + if (sftp_client && sftp_client.isClosed()) { + sftp_client.quit() + sftp_client = null + } + if (sftp && ( sftp.isClosed() || !sftp.isOpen() ) { + sftp.close() + sftp = null + } + } catch (Exception ex) { + } + } // end def is_closed + + class SshExecHandle extends ExecHandle { + def channel, stderr, stdout, stderr_len, stdout_len + def SshExecHandle(channel) { + channel = channel + stderr = '' + stdout = '' + stderr_len = 0 + stdout_len = 0 + } + + def isOpen() { + channel.isOpen + } + + def close() { + // LATER begin + channel.close + // rescue + // end + } + + def write_stdin(stdin_data) { + channel.send_data(stdin_data) + } + def hasStderr() { + stderr.length() > 0 + } + def hasStdout() { + stdout.length() > 0 + } + def read_stderr() { + def x = stderr + stderr = '' + return x + } + def read_stdout() { + def x = stdout + stdout = '' + return x + } + def post_stdout(data) { + stdout = data + stdout_len += data.length + } + def post_stderr(data) { + stderr = data + stderr_len += data.length + } + def stdout_full(opts) { + opts.max_len > 0 && stdout_len >= opts.max_len + } + def stderr_full(opts) { + opts.max_len > 0 && stderr_len >= opts.max_len + } + } // end class SshExecHandle + + def _exec_impl(command, opts, ctx, block) { + // + if (opts.hasProperty('chdir') && opts.chdir) { + if (isWindows(ctx)) + command = "CMD /C pushd $opts[:chdir] & $command & popd" + else + command = "pushd $opts[:chdir] && $command && popd" + } + + // type com.sshtools.j2ssh.session.SessionChannelClient + exec = session(ctx).openSessionChannel + + if (opts.hasProperty('env')) { + for (def k:opts.env.keySet()) { + exec.setEnvironmentVariable(k, opts.env[k]) + } + } + + def output = new ByteArrayOutputStream(1024) + def error = new ByteArrayOutputStream(1024) + + exec.executeCommand(command) + + if (opts.hasProperty('stdin_data')) { + stdin_data = opts.stdin_data + if (stdin_data) { + exec.getOutputStream().write(stdin_data, 0, stdin_data.length) + } + } + + // + def lh = new SshExecHandle(exec) + + // + def o, e + if (opts.hasProperty('by_line') && opts.by_line) { + o = exec_copy_lines(new InputStreamReader(exec.getInputStream()), false, lh, block, opts.max_len) + e = exec_copy_lines(new InputStreamReader(exec.getStderrInputStream()), true, lh, block, opts.max_len) + } else { + exec_copy_stream(new BufferedInputStream(exec.getInputStream()), output, false, lh, block, opts.max_len) + exec_copy_stream(new BufferedInputStream(exec.getStderrInputStream()), error, true, lh, block, opts.max_len) + + o = output.toString() + e = error.toString() + } + // + + exec.close() + + exit_code = exec.getExitCode() + + if (opts.hasProperty('exec_handle') && opts.exec_handle) + lh + else + [output:output.toString(), error: error.toString(), exit_code: exit_code] + } // end def _exec_impl + + def _delete(path, ctx) { + if (isWindows(ctx)) { + path = to_windows_path(path) + + cmd("DEL /Q /F \"//{path}\"", ctx) + } else { + path = to_posix_path(path) + + exec("rm -rf \"//{path}\"", ctx) + } + } + + def _mkdir(path, ctx) { + try { + sftp_client(ctx).mkdir(path) + true + } catch (Exception ex) { + if_closed +// if (ctx) { +// ctx.pftt_exception(self, ex, self) +// } else { +// Tracing::Context::Base.show_exception(ex) +// } + throw ex + } + } + + class AllowAnyHostKeyVerification implements HostKeyVerification { + def verifyHost(host, pk) { + true + } + } + + def session(ctx) { + if (session) { + return session + } + + session = new SshClient() + session.connect(credentials[:address], @credentials[:port], AllowAnyHostKeyVerification.new) + def pwd = new PasswordAuthenticationClient() + pwd.setUsername(credentials[:username]) + pwd.setPassword(credentials[:password]) + + def result = session.authenticate(pwd) + + if (result != AuthenticationProtocolState.COMPLETE) { + throw new IllegalStateException('WrongPassword') + } + + session + } // end def session + + def sftp_client(ctx) { + if (sftp_client) { + return sftp_client + } + + sftp_client = session(ctx).openSftpClient + } + + def sftp(ctx) { + if (sftp) { + return sftp + } + + sftp = session(ctx).openSftpChannel //SftpSubsystemClient + } // end def sftp + +} // end public class SSHHost + \ No newline at end of file diff --git a/PFTT/lib/Scenario.groovy b/PFTT/lib/Scenario.groovy new file mode 100644 index 0000000..f360cc2 --- /dev/null +++ b/PFTT/lib/Scenario.groovy @@ -0,0 +1,136 @@ +package com.mostc.pftt + +// +//class Scenario { +// +// attr_accessor :working_fs, :remote_fs, :date, :database +// attr_reader :id +// +// def initialize(id, working_filesystem_scenario, *optional_other_scenarios) +// @id = id +// @working_fs = working_filesystem_scenario +// optional_other_scenarios.each do |scenario| +// unless (scenario) { +// next // for from_xml +// end +// case scenario.scn_type +// when :remote_file_system +// @remote_fs = scenario +// when :date +// @date = scenario +// when :database +// @database = scenario +// end +// end +// end +// +// //////////////// start message encoding/decoding ////////////////////// +// def toXML serial +// // TODO +// end +// +// def self.msg_type +// 'scenario' +// end +// +// def self.fromXML +// end +// //////////////// end message encoding/decoding ////////////////////// +// +// def execute_script_start(env, test, script_type, deployed_script, php_binary, php_build, current_ini, host) +// values.each do |ctx| +// ctx.execute_script_start(env, test, script_type, deployed_script, php_binary, php_build, current_ini, host) +// end +// end +// +// def execute_script_stop(test, script_type, deployed_script, php_binary, php_build, host) +// values.each do |ctx| +// ctx.execute_script_stop(test, script_type, deployed_script, self.php_binary, php_build, host) +// end +// end +// +// def create_ini(platform, ini=nil) +// if platform != :windows and platform != :posix +// raise ArgumentError, 'platform must be :windows or :posix' +// end +// +// values.each do |scn| +// ini = scn.create_ini(platform, ini) +// end +// +// return ini +// end +// +// def values +// list = [@working_fs] +// if @remote_fs +// list << @remote_fs +// end +// if @date +// list << @date +// end +// if @database +// list << @database +// end +// return list +// end +// +// def teardown(host) +// values.each do |scn| +// scn.teardown(host) +// end +// end +// +// def deploy(host) +// values.each do |scn| +// scn.deploy(host) +// end +// end +// +// def to_s +// "[Set //{@id} //{values.inspect}]" +// end +// +// def == (o) +// o.instance_of?(Scenario::Set) and o.id == @id +// end +// +// class Part +// // TODO include PhpIni::Inheritable +// +// def deploy(host_info) +// end +// +// def teardown(host_info) +// end +// +// def execute_script_start(env, test, script_type, deployed_script, deployed_php, php_build_info, php_ini, host, platform) +// end +// +// def execute_script_stop(test, script_type, deployed_script, deployed_php, php_build_info, host_info) +// end +// +// def docroot middleware +// middleware.docroot +// end +// +// def deployed_php(middleware) +// nil +// end +// +// // subclasses should override this!! +// def scn_type +// return :unknown +// end +// +// def to_s +// scn_name +// end +// +// def self.instantiable +// All << self +// end +// end // class Part +// +//} // end class Scenario +// diff --git a/PFTT/lib/base.groovy b/PFTT/lib/base.groovy new file mode 100644 index 0000000..a6274e5 --- /dev/null +++ b/PFTT/lib/base.groovy @@ -0,0 +1,97 @@ +package com.mostc.pftt + +// +//require 'tracing.rb' +// +//module Base +// +//class RunOptions +// +// def self.msg_type +// 'run_options' +// end +// +//end +// +//class TestPack < Tracing::FileManager +// +// attr_reader :correct_test_count +// +// def initialize correct_test_count +// @correct_test_count = correct_test_count +// end +// +// def self.msg_type +// 'test_pack' +// end +// +// def load_test_case_by_name case_name +// [] +// end +// +// def load_all_test_cases +// [] +// end +// +//end # class TestPack +// +// class CaseName +// +// attr_reader :name_pattern +// +// def initialize name_pattern +// @name_pattern = name_pattern +// end +// +// def self.msg_type +// 'case_name' +// end +// +// def self.fromXML +// end +// +// def toXML +// end +// +// end # class CaseName +// +// class AllCases +// +// attr_reader :correct_test_case_count +// +// def initialize correct_test_case_count +// @correct_test_case_count = correct_test_case_count +// end +// +// def self.msg_type +// 'all_cases' +// end +// +// def self.fromXML +// end +// +// def toXML +// end +// +// end # class AllCases +// +//class Build < Tracing::FileManager +// +// def self.msg_type +// 'build' +// end +// def msg_type # TODO +// 'build' +// end +// +//end # class Build +// +//class Configuration < Tracing::FileManager +// +// def self.msg_type +// 'configuration' +// end +// +//end # class Configuration +// +//end # module Base diff --git a/PFTT/lib/case_runner.groovy b/PFTT/lib/case_runner.groovy new file mode 100644 index 0000000..fd58354 --- /dev/null +++ b/PFTT/lib/case_runner.groovy @@ -0,0 +1,5 @@ +package com.mostc.pftt + +abstract class AbstractCaseRunner { + +} // abstract class AbstractCaseRunner