classdef (Hidden = true) LinuxSILDebugger < rtw.connectivity.HostSILDebugger
    %LINUXSILDEBUGGER launch SIL process in Linux utility class
    %
    %   This is an undocumented class. Its methods and properties are
    %   likely to change without warning from one release to the next.
    
    %   Copyright 2014-2020 The MathWorks, Inc.
    
    properties (SetAccess = 'private', GetAccess = 'private')
        TermPID;
        GdbPID;
        PreCommandFile;
        CommandFile;
        StartFile;
    end
    
    methods
        % constructor
        function this = LinuxSILDebugger(varargin)           
            this@rtw.connectivity.HostSILDebugger(varargin{:})
        end
        
        function delete(this)   
               
            % Kill the parent process (TermPID) and the child process
            % (GdbPID). The child process is terminated first to ensure
            % both processes can be terminated, otherwise there can be
            % cases where the child process becomes defunct, blocking the
            % parent from exiting normally.  
            
            if ~isempty(this.GdbPID)
                rtw.connectivity.Utils.killProcess(this.GdbPID,...
                                                   '',...
                                                   'WaitTime',20,...
                                                   'ForceClose',true);
            end
            
            if ~isempty(this.TermPID)
                rtw.connectivity.Utils.killProcess(this.TermPID,...
                                                   '',...
                                                   'WaitTime',20,...
                                                   'ForceClose',true);
            end
                        
            % Cleanup the command file used to set breakpoints. This is 
            % done now to avoid the case where gdb hasn't processed this 
            % before it is deleted.
            rtw.connectivity.Utils.cleanTempFile(this.PreCommandFile);
            rtw.connectivity.Utils.cleanTempFile(this.CommandFile);
            
            delete@rtw.connectivity.HostSILDebugger(this);
        end
        
    end
    
    methods (Access = 'public')
        function [exePID, ...
                 exeOutputFile, exeErrorFile] = launchProcess(this,...
                                                exe,...
                                                argString)
                                                     
            % Launch the target application
            [exePID, exeOutputFile, exeErrorFile] = ...
                rtw.connectivity.Utils.launchProcess(exe, ...
                                                     argString); 
            
            % Launch the debugger and attach to process
            this.launchAndAttachDebugger(exePID);
            
            % Notify user that debug session is now ready
            this.notifyUserDebugSessionReady();                                                                        
        end
    end
    
    methods (Hidden, Access = 'private')
        function launchAndAttachDebugger(this, exePID)
            
            % Create gcb pre-command file to control ddd setup
            this.createPreCommandFile;
            
            % Create gdb command file to add breakpoints
            this.createCommandFile;
            
            % Create a script file to launch DDD for debugging, which
            % itself launch GDB as a child process. The method collects the
            % process IDs for both DDD and GDB (which will be used for
            % clean-up), and waits for GDB to indicate that the command
            % file has been run.
            scriptFile = tempname;
            rtw.connectivity.Utils.cleanTempFile(scriptFile);
            
            fid = fopen(scriptFile, 'w');
            % launch gdb in a terminal emulator and apply command file,
            % return the PID of the terminal process
            fprintf(fid, 'ddd --gdb -ix "%s" -x "%s" -p %d > /dev/null 2>&1 & \n',...
                this.PreCommandFile, this.CommandFile, exePID);
            fprintf(fid, 'echo $!\n'); % output the process id
            fclose(fid);
            
            %Pass script to shell. See g1944828
            [errorOccurred, result]= system(['/bin/sh "' scriptFile '" < /dev/null']);

            if errorOccurred
                error(message('PIL:pil:DDDDidNotStart', scriptFile));
            else
                % parse process id
                this.TermPID = sscanf(result, '%d', 1);
                
                if isempty(this.TermPID)
                    % error
                    error(message('PIL:pil:DDDDidNotStart', scriptFile));
                else
                    % Wait until gdb process has started and command file
                    % executed
                    this.waitForGDB(scriptFile);
                    
                    % Get the gdb process ID too (child process of ddd):
                    systemCommand = sprintf('pgrep -P %d \n',this.TermPID);
                    [~, result] = system(systemCommand);
                    this.GdbPID = sscanf(result,'%d',1);
                    
                    if isempty(this.GdbPID)
                        error(message('PIL:pil:GDBDidNotStart',scriptFile));
                    end
                end
                
            end        
            
            % Cleanup the script file
            rtw.connectivity.Utils.cleanTempFile(scriptFile);
                                  
        end
        
        function createPreCommandFile(this)
            
            % Create a command file
            this.PreCommandFile = tempname;
            rtw.connectivity.Utils.cleanTempFile(this.PreCommandFile);
                        
            fid = fopen(this.PreCommandFile, 'w');
            fprintf(fid,'set pagination off \n'); %Disable pagination            
            fclose(fid);
        end
        
        function createCommandFile(this)
            
            % Create a command file
            this.CommandFile = tempname;
            rtw.connectivity.Utils.cleanTempFile(this.CommandFile);
            
            % Create a file to signal when the process has started
            this.StartFile = tempname;
            rtw.connectivity.Utils.cleanTempFile(this.StartFile);
            
            fid = fopen(this.CommandFile, 'w');
            if ~strcmp(getenv('DebugSILWithMSVCTesting'),'1')

                % Don't place breakpoints if simulation stepper is enabled
                if this.isSimulationStepperEnabled
                    error(message('PIL:pil:LinuxSILDebugStepperNotSupported'));
                end
                % get the initialize and output function names from the
                % superclass private property Breakpoints
                breakpointsInfo = this.getBreakpointsInfo;
                functionNames = {breakpointsInfo.FunctionName};
                
                for i = 1:length(functionNames)
                    fprintf(fid, ['break ',functionNames{i},' \n']);
                end
                
                fprintf(fid,'shell echo Process started > "%s" \n',this.StartFile);
                fprintf(fid,'continue \n');
            else
                % For internal testing only
                this.i_createTestCommandFile(fid);
            end
            
            fclose(fid);
        end       
        
        function waitForGDB(this, scriptFile)
            % Function waits for a file to be created which indicates gdb
            % is executing command file
            timeOutCounter = 200;
            while(exist(this.StartFile,'file')==0 && timeOutCounter > 0)
                pause(0.1)
                timeOutCounter = timeOutCounter - 1;
            end
            if exist(this.StartFile,'file')==0
                error(message('PIL:pil:GDBDidNotStart',scriptFile));
            end
            
            % Clean up start notification file
            rtw.connectivity.Utils.cleanTempFile(this.StartFile);
                 
        end
        
        function i_createTestCommandFile(this, fid)
            % Create a command file that will hit the model_step
            % breakpoint ten times and then exit
            
            % Don't place breakpoints if simulation stepper is enabled
            if ~this.isSimulationStepperEnabled
                % get the initialize and output function names from the
                % superclass dependent property Breakpoints
                breakpointsInfo = this.getBreakpointsInfo;
                functionNames = {breakpointsInfo.FunctionName};
                for i = 1:length(functionNames)
                    fprintf(fid, ['break ',functionNames{i},' \n']);
                    % Write an output whenever a breakpoint is hit
                    fprintf(fid,'commands \n');
                    fprintf(fid,'silent \n');
                    fprintf(fid,'shell echo Hit Breakpoint >> "%s/SILDebugTestLogs.txt" \n',pwd);
                    fprintf(fid,'continue \n');
                    fprintf(fid,'end \n');
                end
                % The two lines below are necessary to automate the Out of
                % Process External Debugger test in Linux, where we
                % intentionally introduce a crash in the custom code.
                % Without these lines the debugger will get stuck when
                % encountering an exception and requires user manual
                % intervention to continue. This theoretically should not
                % affect SIL workflow where application/code is expected not to crash.
                fprintf(fid,'handle SIGSEGV nostop \n');
                fprintf(fid,'handle SIGTERM nostop \n');
            end

            fprintf(fid,'shell echo Process started > "%s" \n',this.StartFile);
            fprintf(fid,'continue \n');
            
        end
    end
end
