classdef (Hidden = true) LLDBSILDebugger < 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 2019-2020 The MathWorks, Inc.
    
    properties (SetAccess = 'private', GetAccess = 'private')
        TermPID;
        LldbPID;
        CommandFile;
        StartFile;
    end
    
    methods
        % constructor
        function this = LLDBSILDebugger(varargin)
            this@rtw.connectivity.HostSILDebugger(varargin{:})
        end
        
        function delete(this)
            
            % Kill the parent process (TermPID) and the child process
            % (LldbPID). 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.LldbPID)
                rtw.connectivity.Utils.killProcess(this.LldbPID,...
                    '',...
                    '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.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)
            
            % Tokenize string return by pid query and convert to double(s)
            function pids = tokenizePids(pidStr)
                pids = split(pidStr, newline);
                pids = pids(~cellfun(@isempty,pids));
                pids = cellfun(@str2double, pids);
            end
            
            % A DFS pid search to find LLDB
            function pid = getLLDBPID(parentPID)
                cmd = sprintf('pgrep -P %d \n',parentPID);
                [~, out] = system(cmd);
                pids = tokenizePids(out);
                % pid 1 is always launchd, fine to assume 1 means not
                % found
                pid = 1;
                % Go through all children
                for i = 1 : numel(pids)
                    if pid > 1
                        return;
                    end
                    % Get process name
                    cmd = sprintf('ps -p %d -o comm=', pids(i));
                    [~, out] = system(cmd);
                    % If the name is not lldb, keep diving, otherwise
                    % update pid and return
                    if out == "lldb"
                        pid = getLLDBPID(pids(i));
                    else
                        pid = pids(i);
                        return;
                    end
                end
            end
            
            % Create lldb command file to add breakpoints
            this.createCommandFile(exePID);
            
            % Create a script file to launch Terminal for debugging, which
            % itself launch LLDB as a child process. The method collects the
            % process IDs for both Terminal and LLDB (which will be used for
            % clean-up), and waits for LLDB to indicate that the command
            % file has been run.
            scriptFile = tempname;
            rtw.connectivity.Utils.cleanTempFile(scriptFile);
            
            fid = fopen(scriptFile, 'w');
            % launch LLDB in a terminal emulator and apply command file,
            % return the PID of the terminal process
            % Add a magic sleep before launch lldb to make attach more reliable
            fprintf(fid, 'sleep 3\nlldb -S ''%s''\n', this.CommandFile);
            fclose(fid);
            
            system(['chmod +x ''' scriptFile '''']);
            
            % Create a new Terminal, I know Terminal supports multi-tab,
            % just later it's easier to just kill this Terminal instead of
            % close the tab.
            launchScript = [tempname '_launch.sh'];
            rtw.connectivity.Utils.cleanTempFile(launchScript);
            
            fid = fopen(launchScript, 'w');
            
            % macOS Catalina moved Terminal to /System/Applications...
            terminalPath = '/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal';
            if ~isfile(terminalPath)
                terminalPath = '/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal';
            end
            fprintf(fid, '%s ''%s'' & \n echo $!\n', terminalPath, scriptFile);
            fclose(fid);
            [errorOccurred, ret]= system(['/bin/bash ''' launchScript '''']);
            
            if errorOccurred
                error(message('PIL:pil:TerminalDidNotStart', scriptFile));
            else
                this.TermPID = sscanf(ret, '%d', 1);
                
                if isempty(this.TermPID)
                    % error
                    error(message('PIL:pil:TerminalDidNotStart', scriptFile));
                else
                    % Wait until gdb process has started and command file
                    % executed
                    this.waitForLLDB(scriptFile);
                    
                    % Get the lldb process ID too (child process of Terminal):
                    % Most of the time, lldb is not down in the chain.
                    % Terminal->login->sh->lldb
                    this.LldbPID = getLLDBPID(this.TermPID);
                    
                    if this.LldbPID <= 1
                        error(message('PIL:pil:LLDBDidNotStart',scriptFile));
                    end
                    
                end
            end
            
            % Cleanup the script file
            rtw.connectivity.Utils.cleanTempFile(scriptFile);
            
        end
        
        function createCommandFile(this, exePID)
            
            % 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');
            fprintf(fid, ['process attach --pid ' num2str(exePID) '\n']);
            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, ['breakpoint set --name ',functionNames{i},' \n']);
                end
                
                fprintf(fid,'platform shell touch ''%s'' \n',this.StartFile);
                fprintf(fid,'continue \n');
            else
                % For internal testing only
                this.i_createTestCommandFile(fid);
            end
            
            fclose(fid);
        end
        
        function waitForLLDB(this, scriptFile)
            % Function waits for a file to be created which indicates gdb
            % is executing command file
            timeOutCounter = 300;
            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:LLDBDidNotStart',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};
                
                pyFid = fopen('hitBreakPoint.py', 'w');
                fprintf(pyFid, 'def breakpoint_callback(frame, bp_loc, dict):\n');
                fprintf(pyFid, '\tprint("Hit")\n');
                fprintf(pyFid, '\twith open("%s/SILDebugTestLogs.txt","a+") as myFile:\n', pwd);
                fprintf(pyFid, '\t\tmyFile.write("Hit Breakpoint\\n")\n');
                fprintf(pyFid, '\treturn False\n');
                fclose(pyFid);
                
                fprintf(fid,'command script import ''%s/hitBreakPoint.py''\n', pwd);
                for i = 1:length(functionNames)
                    fprintf(fid, ['breakpoint set --name ',functionNames{i},' \n']);
                     % Write an output whenever a breakpoint is hit
                    fprintf(fid,'breakpoint command add -F hitBreakPoint.breakpoint_callback\n');
                end
                % Ideally the two lines below should work to facilitate
                % automated testing for Out of Process External Debugger,
                % where intentionally introduce a crash in the custom code.
                % Without these lines the debugger will get stuck when
                % encountering an exception and requires user's manual
                % intervention to continue. This theoretically should not
                % affect SIL workflow where application/code is expected
                % not to crash. These two lines are introduced in
                % LinuxSILDebugger as well.
                % Unfortunately, there is a bug in Mac OS that makes the
                % debugger to ignore these signal settings, thus a 'quit'
                % line further down is necessary to close the debugger.
                
                fprintf(fid,'process handle SIGSEGV --stop false\n');
                fprintf(fid,'process handle SIGTERM --stop false\n');
            end
            fprintf(fid,'platform shell touch ''%s'' \n',this.StartFile);
            fprintf(fid,'continue \n');
            
            % As mentioned above this quit line below is necessary
            % otherwise the debugger will not close after encountering an
            % exception.
            fprintf(fid,'q \n');
        end
    end
end
