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-2023 The MathWorks, Inc.

    properties(SetAccess = 'protected', GetAccess = 'protected')
        % default empty argument string
        argString = '';
        ArgCell = {};
        Verbose = false;
    end       

    properties (SetAccess = 'private', Hidden)
        % AppExecutionService is initialized in startApplication
        % Launched executable lifetime is tied to this object; if it is
        % empty then there is no launched executable.
        AppExecutionService;
    end

    properties (Access = 'private')        
        IsDebuggerExecution = false;
    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       

        % specify command line arguments
        function setArgString(this, argString)
            arguments
                this
                argString {mustBeTextScalar}
            end            
            assert(isempty(this.ArgCell), 'setArgs has already been called');
            this.argString = argString;
        end

        function setArgs(this, argCell)
            arguments    
                this
                argCell cell {mustBeText}
            end                       
            assert(isempty(this.argString), 'setArgString has already been called');
            this.ArgCell = argCell;
        end

        % Start the application
        function startApplication(this, ...
                                  silDebuggingInfo, ~)
            arguments
                this                
                silDebuggingInfo = [] % SIL debugging support
                ~
            end

            if ~isempty(silDebuggingInfo)
                this.IsDebuggerExecution = true;
            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

            if ispc || ismac
                % G878103: Add path to matlabroot/bin/arch as some
                % executables like rsim load some dlls in the bin/arch
                % folder.
                restorePathEnvVal = rtw.connectivity.PsUtils.addMATLABBinFolderToPath(); %#ok<NASGU> clean up object
            end

            % creating a new execution service will stop any running
            % application
            this.createAppExecutionService(silDebuggingInfo);

            dispMessages = onCleanup(@() this.dispStdOutAndStdErr());
            try
                % start the application
                this.AppExecutionService.open();
                this.AppExecutionService.load();
                this.AppExecutionService.start();
            catch Me
                clear dispMessages
                this.dispInfo(message('Connectivity:targetservices:NoApplicationWithException', Me.message).string());
                rethrow(Me);
            end

            if this.AppExecutionService.ApplicationStatus == target.internal.ApplicationStatus.Stopped
                this.dispInfo(message('Connectivity:targetservices:NoApplication').string());
            end

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

        % Stop the application
        function stopApplication(this)                       
            if ~isempty(this.AppExecutionService)
                % Allow process to terminate itself gracefully

                this.AppExecutionService.stop();
                this.AppExecutionService.unload();
                this.AppExecutionService.close();

                % 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.IsDebuggerExecution
                    this.dispInfo(['rtw.connectivity.HostLauncher: stopped '...
                        'executable with host process identifier ' ...
                        this.getPIDAsLink]);
                end
                this.dispStdOutAndStdErr();

                this.processExeArtifacts(this.getExePID());
            end
        end

        function status = getApplicationStatus(this)
            if ~isempty(this.AppExecutionService) && ...
                    (this.AppExecutionService.ApplicationStatus ~= target.internal.ApplicationStatus.Stopped)
                status = rtw.connectivity.LauncherApplicationStatus.RUNNING;
            else
                status = rtw.connectivity.LauncherApplicationStatus.NOT_RUNNING;
            end
        end
        
        function dispStdOutAndStdErr(this)
            if ~isempty(this.AppExecutionService)
                % display the content of stdOut and stdErr
                stdout = this.AppExecutionService.getStandardOutput();
                stdout = regexprep(stdout,'^Server Port Number.*?\n','', 'lineanchors');
                if ~isempty(stdout)
                    this.dispInfo('');
                    this.dispInfo(message('Connectivity:target:HostLauncherStdoutCapture').string)
                    this.dispInfo(stdout);
                end
                stderr = this.AppExecutionService.getStandardError();
                if ~isempty(stderr)
                    this.dispInfo('');
                    this.dispInfo(message('Connectivity:target:HostLauncherStderrCapture').string)
                    this.dispInfo(stderr);
                end
            end
        end
                   
        % get the command line associated with the application
        function commandLine = getCommandLine(this)
            [command, commandArgs] = this.getCommand;
            if iscell(commandArgs)
                args = strjoin(commandArgs);
            else
                args = commandArgs;
            end
            commandLine = [command ' ' args];
        end        
        
        function exePID = getExePID(this)
            exePID = [];
            if ~isempty(this.AppExecutionService) && ~this.IsDebuggerExecution
                exePID = str2double(this.AppExecutionService.getManagerState.PID);
            end
        end
        
        % Can be overridden in subclasses to modify the command passed to
        % launcher
        function [command, commandArgs] = getCommand(this)            
            command = this.getExe;
            if ~isempty(this.argString) || isempty(this.ArgCell)
                commandArgs = this.argString;
            else
                commandArgs = this.ArgCell;
            end
        end

        function serverPort = pollForServerPortNumber(this, extendTimeout)
            arguments
                this
                extendTimeout = false;
            end
            serverPort = coder.connectivity.PILCommunication.pollForServerPortNumber(...
                this.AppExecutionService, extendTimeout);
            if isempty(serverPort)
                % display the content of stdOut and stdErr
                this.dispStdOutAndStdErr;

                rtw.connectivity.ProductInfo.error('target', 'UnknownServerPort', ...
                    this.AppExecutionService.getStandardOutput(), this.getCommandLine());
            end
        end

        function timeOut = getTimeoutForTermination(~)
	    % This resolves to a 2s timeout, this is useful for Bullseye Codecoverage
	    % as it gives time to SIL application to emit the .cov file
            timeOut = 20;
        end
    end

    methods (Access = 'protected')
        function processExeArtifacts(~, ~)
            % template method to process artifacts generated
            % by the executable
        end
    end

    methods (Access = 'protected')                               
        
        function link = getPIDAsLink(this)
            commandText = ['disp('' '');' ...
                'disp(''Executable command line: ' this.getCommandLine ''');' ...                
                'disp('' '');'];
            link = targets_hyperlink_manager('new', int2str(this.getExePID()), commandText);
        end

        function createAppExecutionService(this, debuggingInfo)   
            import targetframework.internal.model.foundation.ServiceMethodRequirement;
            [command, commandArgs] = this.getCommand;
                        
            % we prefer args in cell array format for handling spaces, etc., 
            % but if we have string arguments, apply a simple transformation
            % which works in cases when arguments do not contain spaces or
            % quotes
            if ~iscell(commandArgs) 
                if isempty(commandArgs)
                    commandArgs = {};
                else
                    % simple transform of command args to cell array
                    commandArgs = strsplit(strtrim(commandArgs));
                end
            end
            
            % Create the execution tool & service
            if ~isempty(debuggingInfo) && debuggingInfo.IsEnabled                                        
                % Specify options
                options = targetframework.services.appexecution.ExecutionOptions();
                options.Deploy = true;
                options.Execute = true;
                options.ApplicationArguments = commandArgs;
                options.ServiceInterfaceRequirements.setMethodRequirement('open', ServiceMethodRequirement.Optional);
                options.ServiceInterfaceRequirements.setMethodRequirement('close', ServiceMethodRequirement.Optional);
                options.ServiceInterfaceRequirements.setMethodRequirement('loadApplication', ServiceMethodRequirement.Optional);
                options.ServiceInterfaceRequirements.setMethodRequirement('unloadApplication', ServiceMethodRequirement.Optional);
                options.ServiceInterfaceRequirements.setMethodRequirement('stopApplication', ServiceMethodRequirement.Optional);
                options.ServiceInterfaceRequirements.setMethodRequirement('getStandardOutput', ServiceMethodRequirement.Optional);
                options.ServiceInterfaceRequirements.setMethodRequirement('getStandardError', ServiceMethodRequirement.Optional);
                options.ServiceInterfaceRequirements.setMethodRequirement('getApplicationStatus', ServiceMethodRequirement.Optional);
                if debuggingInfo.IsSimulationStepperEnabled
                    % do not add breakpoints, the user can manually add
                    % them once the simulation pauses in Simulink
                    breakpoints = target.internal.Breakpoint.empty();
                else
                    breakpoints = coder.connectivity.debugger.Utils.createBreakpointsFromStruct(...
                        debuggingInfo.BreakpointsInfo);
                end               
                debugService = rtw.connectivity.HostDebuggerManager.getDebugExecutionService;
                this.AppExecutionService = targetframework.services.appexecution.createDebugManager(...
                    command, breakpoints, debugService, options);
            else
                this.AppExecutionService = coder.internal.connectivity.createHostExecutionManager(command, commandArgs);
            end                            
        end
    end       
end

