classdef (Hidden = true) HostLauncher < rtw.connectivity.Launcher
%HOSTLAUNCHER starts or stops an application on the host computer
%   HOSTLAUNCHER(COMPONENTARGS,BUILDER) instantiates a HOSTLAUNCHER object that
%   you can use to control starting and stopping of an application on the host
%   computer.
%
%   This is an undocumented class. Its methods and properties are likely to
%   change without warning from one release to the next.

%   Copyright 2007-2021 The MathWorks, Inc.

    properties(SetAccess = 'protected', GetAccess = 'protected')
        exePID;
        exeOutputFile;
        exeErrorFile;
        % default empty argument string
        argString = '';
        Verbose = false;
    end    
    
    properties (Access = 'private')
        HostSILDebugger;
    end

    methods
        % constructor
        function this = HostLauncher(componentArgs, builder)
            narginchk(1, 2);
            super_args = {};
            super_args{1} = componentArgs;
            if (nargin == 2)
                super_args{2} = builder;
            end
            % call super class constructor
            this@rtw.connectivity.Launcher(super_args{:});                        
            
            if ~isempty(this.getComponentArgs.getComponentPath)
                this.Verbose = strcmp(this.getComponentArgs.getParam('RTWVerbose'), 'on');
            end            
        end

        % destructor
        function delete(this)
            % don't leave zombie processes
            this.stopApplication;
        end

        % specify command line arguments
        function setArgString(this, argString)
            this.argString = argString;
        end

        % Start the application
        function startApplication(this, ...
                                  silDebuggingInfo, ...
                                  topContext)
            
            if nargin < 2
                % codeInfo, topContext are only used when SILDebuggingEnabled
                % is true
                SILDebuggingEnabled = false;
            else
                SILDebuggingEnabled = silDebuggingInfo.IsEnabled;
            end
            
            % it is expected that internal users of HostLauncher make 
            % sure the executable exists before attempting to start it, but
            % this is a catch all
            exe = this.getExe;
            if ~isfile(exe)
                DAStudio.error('Connectivity:target:HostLauncherExeMissing', exe);
            end
                                    
            % kill any running application
            this.i_stopApplication;
                        
            % cleanup now the process has stopped
            this.exePID = [];
            this.exeOutputFile = [];
            
            [command, commandArg] = this.getCommand();
                        
            if SILDebuggingEnabled
                % Instantiate debugger using a factory method.
                this.HostSILDebugger = rtw.connectivity.HostDebuggerManager.createHostSILDebugger(...
                    this.getComponentArgs, topContext, silDebuggingInfo);
                                                                       
                % Launch a debugger with the target application
                [~, plainCommand] = rtw.connectivity.extendedLaunchCommand.getCommandStrings(command);
                [this.exePID,...
                 this.exeOutputFile, this.exeErrorFile] = this.HostSILDebugger.launchProcess(plainCommand,...
                                                                          commandArg);
            else            
                % Launch the target application
                [this.exePID, this.exeOutputFile, this.exeErrorFile] = ...
                    rtw.connectivity.Utils.launchProcess(command, ...
                                                         commandArg);              
            end
            
            if this.Verbose

                this.dispInfo(['rtw.connectivity.HostLauncher: started '...
                      'executable with host process identifier ' ...
                      this.getPIDAsLink]);              
            end
        end

        % Stop the application
        function stopApplication(this)
            
            
            % shut down the debugger
            if ~isempty(this.HostSILDebugger) && this.HostSILDebugger.isvalid
                this.HostSILDebugger.delete;
            end
                     
            if ~isempty(this.exePID)
                % display the content of stdOut and stdErr
                this.dispStdOutAndStdErr;
                currentExePID = this.getExePID;
                pidAsLink = i_stopApplication(this);
                
                % We have to use the cached RTWVerbose setting as stopApplication may be called
                % from the destructor when the model is already in the process
                % of being closed; get_param would fail in this situation
                if this.Verbose
                    this.dispInfo(['rtw.connectivity.HostLauncher: stopped '...
                        'executable with host process identifier ' ...
                        pidAsLink]);
                end
                
                this.processExeArtifacts(currentExePID);
            end
        end

        function status = getApplicationStatus(this)
            isAlive = ~isempty(this.exePID) &&...
                      rtw.connectivity.Utils.isAlive(this.exePID);

            if isAlive
                status = rtw.connectivity.LauncherApplicationStatus.RUNNING;
            else
                status = rtw.connectivity.LauncherApplicationStatus.NOT_RUNNING;
            end
        end
        
        function dispStdOutAndStdErr(this)
            % display the content of stdOut and stdErr
            if ~isempty(this.exeOutputFile)
                stdOutStr = fileread(this.exeOutputFile);
                stdOutStr = regexprep(stdOutStr,'^Server Port Number.*?\n','');
                if ~isempty(stdOutStr)
                    stderrMessageHeader = message('Connectivity:target:HostLauncherStdoutCapture');
                    this.dispInfo(' '); % Blank line
                    this.dispInfo(stderrMessageHeader.getString);
                    this.dispInfo(stdOutStr);
                    this.dispInfo(' '); % Blank line
                end
            end
            
            if ~isempty(this.exeErrorFile)
                stdErrStr = fileread(this.exeErrorFile);
                if ~isempty(stdErrStr)
                    stderrMessageHeader = message('Connectivity:target:HostLauncherStderrCapture');
                    this.dispInfo(' '); % Blank line
                    this.dispInfo(stderrMessageHeader.getString);
                    this.dispInfo(stdErrStr);
                    this.dispInfo(' '); % Blank line
                end
            end
        end
            
        % get the path to the stdout / stderr redirection 
        % file associated with the application
        function outputFile = getOutputFile(this)
            outputFile = this.exeOutputFile;
        end
        
        % get the command line associated with the application
        function commandLine = getCommandLine(this)
            commandLine = [this.getExe ' ' this.argString];
        end        
        
        function exePID = getExePID(this)
            exePID = this.exePID;
        end
        
        %Can be overridden in subclasses to modify the command passed to
        %launcher
        function [command, commandArg] = getCommand(this)            
            command = this.getExe;
            commandArg = this.argString;
        end
    end
    methods (Access = 'protected')
        function waitForGracefulTermination(this)
            % Allow process to terminate itself gracefully
            waitCount=20;
            while waitCount>0 && ...
                    rtw.connectivity.Utils.isAlive(this.exePID)
                waitCount = waitCount-1;
                pause(0.1);
            end
        end

        function processExeArtifacts(~, ~)
            % template method to process artifacts generated
            % by the executable
        end
    end

    methods (Access = 'private')                
        
        % Stop the application
        function pidAsLink = i_stopApplication(this)
            if ~isempty(this.exePID)
                
                % Allow process to terminate itself gracefully
                this.waitForGracefulTermination();
                
                % Kill the process (if it is still alive) and clean up
                % the temporary output file
                rtw.connectivity.Utils.killProcess(this.exePID, ...
                    this.exeOutputFile, 'exeErrorFile', this.exeErrorFile);
                
                pidAsLink = this.getPIDAsLink;
                
                % cleanup now the process has stopped
                this.exePID = [];
                this.exeOutputFile = [];
                this.exeErrorFile = [];
            end
        end
        
        function link = getPIDAsLink(this)
            commandText = ['disp('' '');' ...
                'disp(''Executable command line: ' this.getCommandLine ''');' ...
                'disp(''Executable temporary output file: ' this.exeOutputFile ''');' ...
                'disp('' '');'];
            link = targets_hyperlink_manager('new', int2str(this.exePID), commandText);
        end
    end
    
    methods (Hidden)
        function val = getMSVCUtils(this)
            assert(strcmp(getenv('DebugSILWithMSVCTesting'),'1'),...
                'getMSVCUtils can only be called for internal testing purposes');
            val = this.HostSILDebugger;
        end
    end
end

