classdef (Hidden = true) MSVCSILDebugger < rtw.connectivity.HostSILDebugger
    %MSVCSILDEBUGGER launch SIL process in MSVC debugger class using COM
    %interface by default
    %
    %   This is an undocumented class. Its methods and properties are
    %   likely to change without warning from one release to the next.
    
    %   Copyright 2013-2021 The MathWorks, Inc.
    
    % properties
    properties (Access = 'protected')
        MSVCSrvrHandle;
        TempSlnFile;
        IDEVersion;
    end
    
    properties (Constant, Access = 'private')
        MSVCSolutionStr = 'VisualStudio.Solution.';
    end
    
    methods
        % constructor
        function this = MSVCSILDebugger(componentArgs, topContext, silDebuggingInfo, ideVersion)           
            this@rtw.connectivity.HostSILDebugger(componentArgs, topContext, silDebuggingInfo);
            assert(str2double(ideVersion) >= 9, 'MSVC version should be >= 9.');
            this.IDEVersion = ideVersion;
        end
        
        function delete(this)
                       
            % shut down MSVC
            svrHandle = this.MSVCSrvrHandle;
            if ishandle(svrHandle)
                this.deleteHook(svrHandle);
                this.pollMSVCServer(@()svrHandle.delete);
            end
            
            % delete the temp solution file            
            if exist(this.TempSlnFile, 'file')
                delete(this.TempSlnFile);
            end
            
            delete@rtw.connectivity.HostSILDebugger(this);   
        end
    end
    
    methods (Static) 
       function varargout = pollMSVCServer(fcnHandle, ...
                                           inputArgs)
            % Forward on to MSVCUtils with an
            % XIL specific number of retries
            if nargin < 2; inputArgs = {}; end
            maxNumTries = 120;            
            [varargout{1:nargout}] = coder.internal.MSVCUtils.pollMSVCServer(fcnHandle, ...
                                                                             inputArgs, ...
                                                                             maxNumTries);
        end 
    end
    
    methods (Access = 'public')                        
        function [exePID, ...
            exeOutputFile, exeErrorFile] = launchProcess(this,...
                                               exe,...
                                               argString)
            
            % For MSVC 10.0, 11.0 and 12.0 and Express 2013, we launch 
            % the MSVC server via activeX, then use the activeX handle 
            % to launch the exe process, set the command line arguments 
            % by writing them to the .sln file, open source files, and 
            % place breakpoints.
           
            exeOutputFile = [tempname '.out'];
            rtw.connectivity.Utils.cleanTempFile(exeOutputFile);
            exeErrorFile = [tempname '.out'];
            rtw.connectivity.Utils.cleanTempFile(exeErrorFile);
            
            % Add matlab/bin/<arch> to the path to ensure MATLAB DLLs are
            % available. G1318166
            restorePathEnvVal = rtw.connectivity.PsUtils.addMATLABBinFolderToPath; %#ok<NASGU> clean up object
            
            % Create MSVC ActiveX server
            this.MSVCSrvrHandle = this.actxCreateMSVCServer;
            
            % open the .exe project in MSVC
            this.openExeProject(exe, argString, exeOutputFile, exeErrorFile);
            
            % open the model.c/.cpp and place breakpoints
            this.OpenSrcFileAndInsertBreakpoints;
            
            % Run the application
            blocking = false;
            svrHandle = this.MSVCSrvrHandle;
            this.pollMSVCServer(@()svrHandle.get('DTE').get('Debugger').invoke('Go', blocking));
            
            % get the PID for the exe that we just ran
            exePID = this.getPIDForDebuggedProcess;
                        
            % Finally, make the IDE visible
            this.pollMSVCServer(@()svrHandle.get('DTE').get('MainWindow').set('Visible',1));
            
            % Notify user that the debug session is now ready
            if ishandle(this.MSVCSrvrHandle)
                this.notifyUserDebugSessionReady();
            end
        end                  
    end

    methods (Access = 'protected')
        
        function MSVCSrvrHandle = actxCreateMSVCServer(this)       
            try
                    MSVCSrvrHandle = this.pollMSVCServer(@actxserver,...
                    {this.getProgID});
            catch actxException
                if strcmp(actxException.identifier, 'PIL:pil:MSVCIDEInstanceAlreadyRunning')
                    rethrow(actxException);
                else
                    errorMsgID = 'PIL:pil:MSVCFailedToLaunch';
                    excObj     = MException(errorMsgID, DAStudio.message(errorMsgID, this.IDEVersion));
                    excObj     = excObj.addCause(actxException);
                    throw(excObj);
                end
            end
        end
        
        function openExeProject(this, exe, argString, exeOutputFile, exeErrorFile)
            % create a solution file in MSVC and add the .exe as a
            % project.
            solutionNameFullPath = [tempname '.sln'];
            [solutionPath, solutionName, ext] = fileparts(solutionNameFullPath);
            solutionName = [solutionName ext];  
            svrHandle = this.getHandleToExeProject;
            this.pollMSVCServer(@svrHandle.invoke, {'Create', solutionPath, solutionName});
            this.pollMSVCServer(@svrHandle.invoke, {'AddFromFile', exe});
            this.pollMSVCServer(@svrHandle.invoke, {'SaveAs', solutionNameFullPath});
            this.pollMSVCServer(@svrHandle.invoke, {'Close'});
            
            % remember this temp sln file to make sure we delete it upon
            % end of simulation
            this.TempSlnFile = solutionNameFullPath;
            
            % Set the command line arguments (arguments for the
            % .exe). Microsoft recommends to do this by changing the
            % .sln file. and modifying the ProjectSection section by
            % specifying "Arguments".
            solutionOrigContents = fileread(solutionNameFullPath);
            commandLineArgs = sprintf('\tArguments = %s > "%s" 2>"%s"',...
                argString, strrep(exeOutputFile, filesep, [filesep filesep]), ...
                strrep(exeErrorFile, filesep, [filesep filesep]));
            markEndProjectSection = 'EndProjectSection';
            solutionNewContents = regexprep(solutionOrigContents,...
                markEndProjectSection,...
                sprintf('%s\n\t%s',commandLineArgs,markEndProjectSection));
            %Fix Starting Dir. So solution can find data files in build
            %folder
            silFolder = fileparts(exe);
            startInDir = strrep(sprintf('StartingDirectory = %s',silFolder),filesep,[filesep filesep]);
            newStartInDir = strrep(sprintf('StartingDirectory = %s', fileparts(silFolder)),filesep,[filesep filesep]);
            solutionNewContents=regexprep(solutionNewContents,startInDir,newStartInDir);

            % write the new contents
            fid = fopen(solutionNameFullPath, 'w');
            fwrite(fid, solutionNewContents);
            fclose(fid);
            
            % open the sln file in MSVC
            this.pollMSVCServer( @()...
                svrHandle.get('DTE').invoke('ExecuteCommand', ...
                'File.OpenProject', sprintf('"%s"',solutionNameFullPath)));
        end
        
        function OpenSrcFileAndInsertBreakpoints(this)

            % place breakpoints at the initialize and output functions
            componentArgs = this.getComponentArgs;
            
            % open component.c/.cpp file
            svrHandle = this.MSVCSrvrHandle;
            breakpointsInfo = this.getBreakpointsInfo;
            if (~isfield(breakpointsInfo, 'FileFullPath'))
                componentBuildInfoFile = fullfile(componentArgs.getComponentCodePath, ...
                    'buildInfo.mat');
                componentBuildInfo = targets_load_buildinfo(componentBuildInfoFile);
                componentMainSourceFolder = getSourcePaths...
                    (componentBuildInfo, 1, 'BuildDir');
                for idx = 1:length(breakpointsInfo)
                    breakpointsInfo(idx).FileFullPath = fullfile(componentMainSourceFolder{1}, ...
                        breakpointsInfo(idx).FileName);
                end
            end
            
            fileFullPathList = unique({breakpointsInfo.FileFullPath});
            for ii=1:length(fileFullPathList) 
                assert(~isempty(dir(fileFullPathList{ii})), ...
                    'Component source file %s must exist.', fileFullPathList{ii});                
                this.pollMSVCServer(@()...
                    svrHandle.get('DTE').get('ItemOperations').invoke('OpenFile', fileFullPathList{ii}));
            end
                        
            % 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
                functionNames = {breakpointsInfo.FunctionName};
                for i = 1:length(functionNames)
                    this.pollMSVCServer( @() svrHandle.get('DTE').get('Debugger').get('Breakpoints').invoke('Add', ...
                        functionNames{i}));
                end
            end
        end
        
        function exePID = getPIDForDebuggedProcess(this)
            
            timeWaited = 0;
            exePID = [];
            timeout = 30;
            pause_amt = 0.5;
            while isempty(exePID) && ...
                    timeWaited < timeout
                
                pause(pause_amt);
                timeWaited = timeWaited + pause_amt;
                
                % return the PID
                svrHandle = this.MSVCSrvrHandle;
                % Casting to double because it will eventually be used in
                % psUtils which supports this datatype at the moment
                exePID = double(this.pollMSVCServer(@() ...
                    svrHandle.get('DTE').get('Debugger').get('DebuggedProcesses').invoke('Item', 1).get('ProcessID')));
            end
            
            this.verifyExePID(exePID);
        end
        
        function verifyExePID(~, exePID)
            % Error if no exePID was returned
            if isempty(exePID)
                DAStudio.error('PIL:pil:MSVCNoDebuggedProcessID');
            end
        end
        
        function notifyUserDebugSessionReady(this)
            % call super-class first
            notifyUserDebugSessionReady@rtw.connectivity.XILDebugger(this);
            
            % display an extra message for MATLAB Coder DLL target build to
            % inform users about the extra breakpoints at the call sites of
            % the entry points.
            clientInterface = this.getComponentArgs.getClientInterface;
            if clientInterface.isDLLTarget(this.getComponentArgs.getConfigInterface)
                notificationID = 'PIL:pil:SILDebuggingDLLExtraBreakpoints';
                notificationMsg = DAStudio.message(notificationID, this.getComponentArgs.getComponentCodeName);
                this.deliverNotification(notificationID, notificationMsg);
            end
        end                
        
        function deleteHook(this, svrHandle)
            this.pollMSVCServer(@()svrHandle.get('Parent').invoke('Quit'));
        end
        
        function svrHandle = getHandleToExeProject(this)
            svrHandle = this.MSVCSrvrHandle;
        end
        
        function progID = getProgID(this)
            progID = [this.MSVCSolutionStr this.IDEVersion];
        end
    end
        
    methods (Hidden)
        function val = getMSVCSrvrHandle(this)
            assert(strcmp(getenv('DebugSILWithMSVCTesting'),'1'),...
                'getMSVCSrvrHandle can only be called for internal testing purposes');
            val = this.MSVCSrvrHandle;
        end
    end
end
