module Sprout::System # The abstract base class for all supported system/platform types. # In general, users are created by calling the +create+ factory method # on the +System+ module. # # System.create # # Assuming you call the create method, you should wind up with # a concrete system that matches your system, and these concrete # users will generally be derived from this base class. # class BaseSystem ## # Get the home path for a system on a particular operating system. # # This path will be different, depending on which system owns # the curren process, and which operating system they are on. # def home @home ||= find_home end ## # Set the home path for a system on a particular operating system. # # If you request the home path before setting it, we will # attempt to determine the home path of the current system for # the current operating system. # # This is just a simple way to override the default behavior. # def home=(home) @home = home end ## # Some operating systems (like OS X and Windows) have a # specific location where applications are expected to store # files for a particular system. This location is generally # a subdirectory of +home+. # # The value of this location will usually be overridden in # concrete System classes. # def library return home end ## # Instantiate and return a new Sprout::ProcessRunner so # that we can execute it. # def get_process_runner Sprout::ProcessRunner.new end def can_execute? platform platform == :universal end ## # Creates a new process, executes the command # and returns whatever the process wrote to stdout, or stderr. # # Raises a +Sprout::Errors::ExecutionError+ if the process writes to stderr # def execute(tool, options='') Sprout.stdout.puts("#{tool} #{options}") runner = get_and_execute_process_runner(tool, options) error = runner.read_err result = runner.read if(result.size > 0) Sprout.stdout.puts result end if(error.size > 0) raise Sprout::Errors::ExecutionError.new("[ERROR] #{error}") end result || error end ## # Creates and returns the process without # attempting to read or write to the stream. # This is useful for interacting with # long-lived CLI processes like FCSH or FDB. # def execute_silent(tool, options='') get_and_execute_process_runner(tool, options) end ## # Execute a new process in a separate thread and yield whatever output # is written to its stderr and stdout. # # @return [Sprout::ProcessRunner] # @param tool [File] Path to the executable. # @param options [String] The command line options that the executable accepts. # @param prompt [Regex] The prompt that will trigger the listener block to be called. # @yield [String] Message that was received from the process, called when #prompt is encountered. # def execute_thread tool, options='', prompt=nil, &block t = Thread.new do Thread.current.abort_on_exception = true runner = execute_silent(tool, options) Thread.current['runner'] = runner out = read_from runner.r, prompt, &block err = read_from runner.e, prompt, &block out.join && err.kill end # Wait for the runner to be created # before returning a nil reference # that never gets populated... while t['runner'].nil? do sleep(0.1) end if !t.alive? raise Sprout::Errors::UsageError.new(t['runner'].read_err) end t end def read_from pipe, prompt, &block Thread.new do Thread.current.abort_on_exception = true lines = '' line = '' pipe.sync = true pipe.each_char do |char| break if pipe.closed? line << char if line.match prompt yield line if block_given? lines << line lines = '' line = '' next end if char == "\n" lines << line yield line if block_given? line = '' end end end end ## # Clean the provided +path+ String for the current # operating system. # # Each operating system behaves differently when we # attempt to execute a file with spaces in the +path+ # to the file. # # Subclasses will generally override this method and # clean the path appropriately for their operating # system. # def clean_path(path) end ## # Different operating systems will store Application data # different default locations. # # Subclasses will generally override this method and # return the appropriate location for their operating system. # # +name+ String value of the Application name for which we'd # like to store data. # def application_home(name) return File.join(library, format_application_name(name.to_s)); end ## # Template method that should be overridden by # subclasses. # def format_application_name(name) name end protected def env_homedrive ENV['HOMEDRIVE'] end def env_homepath ENV['HOMEPATH'] end def env_homedrive_and_homepath drive = env_homedrive path = env_homepath "#{drive}:#{path}" if drive && path end def env_userprofile ENV['USERPROFILE'] end def env_home ENV['HOME'] end def tilde_home File.expand_path("~") end def alt_separator? File::ALT_SEPARATOR end def worst_case_home return "C:\\" if alt_separator? return "/" end def find_home [:env_userprofile, :env_home, :env_homedrive_and_homepath].each do |key| value = self.send(key) return value unless value.nil? end begin return tilde_home rescue StandardError worst_case_home end end protected ## # Get a process runner and execute the provided +executable+, # with the provided +options+. # # +executable+ String path to the external executable file. # # +options+ String commandline options to send to the +executable+. # def get_and_execute_process_runner tool, options=nil runner = get_process_runner runner.execute_open4 clean_path(tool), options runner end end class ThreadMock def alive? return false end end end