% HPCServer - Perform discovery for HPC Server

%   Copyright 2012-2020 The MathWorks, Inc.

classdef HPCServer < parallel.internal.discover.AbstractDiscoverer
    properties (Access = private)
        % Whether or not we can actually discover on this client. 
        % (i.e. is it Windows with the Client Utilities installed?)
        CanDiscover
    end

    methods
        function obj = HPCServer()
            import parallel.internal.types.SchedulerType
            obj = obj@parallel.internal.discover.AbstractDiscoverer(SchedulerType.HPCServer);
            try
                % The easiest way to test if the HPC Server utilities are installed
                % is to try to get the client version.
                parallel.internal.apishared.HPCServerUtils.getDefaultClientVersion();
                obj.CanDiscover = true;
            catch err
                if strcmp(err.identifier, 'parallel:cluster:HPCServerClientUtilitiesNotInstalled')
                    obj.CanDiscover = false;
                else
                    rethrow(err);
                end
            end
        end
    end
    
    methods % overrides of Abstract methods in AbstractDiscoverer
        function discover(obj, terminateEarlyFcn)
        % discover - discover HPC Server clusters
        % Fires ClusterFound events whenever a new cluster 
        % is found.  Terminates discovery if the supplied
        % terminateEarlyFcn returns true.
        % Only those HPC Server clusters that have been correctly
        % set up by the sys-admin will qualify as discoverable
        % clusters.

            validateattributes(terminateEarlyFcn, {'function_handle'}, {'scalar'});
            import parallel.internal.apishared.HPCServerUtils
            
            if ~obj.CanDiscover
                return;
            end
            
            [~, hpcserverVersions] = enumeration('parallel.internal.types.HPCServerClusterVersion');

            for ii = 1:numel(hpcserverVersions)
                if terminateEarlyFcn()
                    return;
                end

                currVersion = hpcserverVersions{ii};
                
                try
                    headNodes = HPCServerUtils.findHeadNodesFromAD(currVersion);
                catch err %#ok<NASGU>
                    % Just move on if AD search fails 
                    continue;
                end

                if terminateEarlyFcn()
                    return;
                end
                
                for jj = 1:numel(headNodes)
                    obj.notifyFoundIfDiscoverable(headNodes{jj}, currVersion);
                    if terminateEarlyFcn()
                        return;
                    end
                end
            end
        end
    end
    
    methods (Access = private)
        function notifyFoundIfDiscoverable(obj, headNode, hpcserverVersion)
            import parallel.internal.apishared.HPCServerUtils

            try
                connection = HPCServerUtils.getServerConnection(headNode, hpcserverVersion);
                propMap = HPCServerUtils.getAllValuesFromClusterEnvironment(connection);
            catch err %#ok<NASGU>
                % Do nothing if we can't get a connection to the cluster
                % or if we can't get values out of the environment as this 
                % indicates that the cluster isn't contactable or configured 
                % correctly.
                return; 
            end
            
            allPCTVersions = propMap.keys();
            for ii = 1:numel(allPCTVersions)
                pctVersion = allPCTVersions{ii};
                matlabRelease = parallel.internal.version.Version.fromNum(pctVersion).Release;
                if isempty(matlabRelease)
                    matlabRelease = getString(message('parallel:cluster:DiscoverUnknownRelease'));
                end
                isCompatible = isequal(pctVersion, parallel.internal.version.Version.Current.Num);
                
                propNames = propMap(pctVersion).Names;
                propVals = propMap(pctVersion).Values;
                % ClusterMatlabRoot, JobStorageLocation and Name must have been specified 
                % in the cluster environment in order for the cluster to be considered
                % to be "discoverable".
                if ~any(strcmp(propNames, 'ClusterMatlabRoot')) || ...
                    ~any(strcmp(propNames, 'JobStorageLocation')) || ...
                    ~any(strcmp(propNames, 'Name'))
                    continue;
                end

                % If num workers is defined in the values from the cluster environment, then
                % use that value.  Otherwise, use the value directly out of the connection.
                numWorkersIdx = strcmp(propNames, 'NumWorkers');
                if ~any(numWorkersIdx)
                    propNames = [propNames, 'NumWorkers']; %#ok<AGROW> not actually growing on every iteration
                    propVals  = [propVals,  connection.MaximumNumberOfWorkersPerJob]; %#ok<AGROW> not actually growing on every iteration
                end
                
                % Make sure the props and values have the actual host and version stored in them.  
                % The only properties that we should store in the profile are 'Host' and 
                % 'ClusterVersion' because we got everything else from the cluster environment.
                propNames = [propNames, 'Host',   'ClusterVersion']; %#ok<AGROW> not actually growing on every iteration
                propVals  = [propVals,  headNode, hpcserverVersion]; %#ok<AGROW> not actually growing on every iteration

                evtData = parallel.internal.discover.ClusterFoundEventData.createForHPCServer(...
                    propNames, propVals, {'Host', 'ClusterVersion'}, matlabRelease, isCompatible);
                obj.notify('ClusterFound', evtData);
            end
        end
    end
end
