% HPCServer Interact with a Microsoft HPC Server Cluster
%   The HPC Server cluster type provides access to your Windows HPC 
%   Server (including CCS) cluster, which controls the job queue, and 
%   distributes job tasks to workers for execution.
%
%   parallel.cluster.HPCServer methods:
%      HPCServer              - Create an HPCServer cluster instance
%      batch                  - Run MATLAB script or function as batch job
%      createCommunicatingJob - Create a new communicating job
%      createJob              - Create a new independent job
%      findJob                - Find job objects belonging to a cluster object
%      getDebugLog            - Read output messages from communicating job or independent task
%      isequal                - True if clusters have same property values
%      parpool                - Create a parallel pool of workers on a cluster and return a pool object
%      saveAsProfile          - Save modified properties to a profile
%      saveProfile            - Save modified properties to the current profile
%
%   parallel.cluster.HPCServer properties:
%      ClusterMatlabRoot       - Path to MATLAB on the cluster
%      ClusterVersion          - Version of Microsoft Windows HPC Server running on the cluster
%      HasSharedFilesystem     - Specify whether the client and cluster nodes share JobStorageLocation
%      Host                    - Head node of the cluster
%      JobDescriptionFile      - Name of XML job description file to use when creating jobs
%      JobStorageLocation      - Folder where cluster stores job and task information
%      JobTemplate             - Name of job template to use for jobs submitted to HPC Server
%      Jobs                    - List of jobs contained in this cluster
%      LicenseNumber           - License number to use with online licensing
%      Modified                - True if any properties in this cluster have been modified
%      Name                    - Name of this cluster
%      NumThreads              - Number of computational threads for workers
%      NumWorkers              - Number of workers in the cluster
%      OperatingSystem         - Operating System of the cluster
%      PreferredPoolNumWorkers - Preferred number of workers in a parallel pool
%      Profile                 - Profile used to build this cluster
%      RequiresOnlineLicensing - Cluster uses online licensing
%      Type                    - Type of this cluster
%      UseSOAJobSubmission     - Allow service-oriented architecture (SOA) submission on HPC Server
%      UserData                - Data associated with a cluster object in this client session
%
%   See also parcluster, batch, parpool.

%   Copyright 2011-2022 The MathWorks, Inc.

classdef (ConstructOnLoad = true, Sealed) HPCServer < parallel.cluster.CJSCluster
    properties (PCTGetSet, Transient, SetAccess = immutable, PCTConstraint = 'string')
        %Host Head node of the cluster
        Host = '';

        %Name Name of this cluster 
        Name = '';
    end
    properties (PCTGetSet, Transient, ...
                PCTConstraint = 'enum:parallel.internal.types.HPCServerClusterVersion', ...
                SetAccess = immutable)
        %ClusterVersion Version of Microsoft Windows HPC Server running on the cluster
        %   This property can have the value 'HPCServer' (for HPC
        %   Server) or 'CCS' (for CCS).
        ClusterVersion
    end
    properties (PCTGetSet, Transient, PCTConstraint = 'string')
        %JobTemplate Name of job template to use for jobs submitted to HPC Server
        %   JobTemplate is an empty string by default. Job templates apply
        %   only for HPC Server clusters, and the ClusterVersion
        %   property must be set to 'HPCServer'. If ClusterVersion is
        %   set to any other value, and you attempt to set JobTemplate to a
        %   nonempty string, an error is generated and the property value
        %   remains as a nonempty string. If you do not specify a value for
        %   the JobTemplate property, HPC Server uses the default job
        %   template to run the job.
        %
        %   Job templates enable system administrators to provide sets of
        %   defaults and constraints for the different jobs submitted to
        %   the cluster.  Ask your system administrator which job template
        %   you should use.
        %
        %   See also parallel.cluster.HPCServer.ClusterVersion.
        JobTemplate
        
        %JobDescriptionFile Name of XML job description file to use when creating jobs
        %   The XML file you specify by the JobDescriptionFile property
        %   defines the base state from which the job is created. The file
        %   must exist on the MATLAB path or the property must specify the
        %   full path name to the file.
        %
        %   Any job properties that are specified on MATLAB job objects
        %   (e.g., NumWorkersRange, etc., for communicating or MATLAB pool
        %   jobs) override the values specified in the job description
        %   file. Cluster properties (e.g., nonempty JobTemplate) also
        %   override the values specified in the job description file.
        %
        %   For SOA jobs, the values in the job description file are
        %   ignored.
        %
        %   See also parallel.cluster.HPCServer.UseSOAJobSubmission.
        JobDescriptionFile
    end
    properties (PCTGetSet, Transient, PCTConstraint = 'logicalscalar')
        %UseSOAJobSubmission Allow service-oriented architecture (SOA) submission on HPC Server
        %   The value you assign to the UseSOAJobSubmission property
        %   specifies whether to use SOA job submissions for the cluster
        %   object representing a Microsoft Windows HPC Server 
        %   cluster. If you enable SOA submission, MATLAB worker sessions
        %   can each evaluate multiple tasks in succession. If you disable
        %   SOA submission, a separate MATLAB worker starts for each task.
        %
        %   Ensure that HPC Server is correctly configured to run SOA
        %   jobs on MATLAB Parallel Server. For more details,
        %   see the online installation instructions at
        %   https://www.mathworks.com/distconfig.
        %
        %   Note that the MATLAB client from which you submit SOA jobs to
        %   the HPC Server cluster must remain open for the duration
        %   of these jobs. Closing the MATLAB client session while SOA jobs
        %   are in the pending, queued, or running state causes the cluster
        %   to cancel these jobs.
        %
        %   UseSOAJobSubmission is false by default. SOA job submission
        %   works only for HPC Server clusters, and the ClusterVersion
        %   property must be set to 'HPCServer'. If ClusterVersion is
        %   set to any other value, and you attempt to set
        %   UseSOAJobSubmission to true, an error is generated and the
        %   property value remains false.
        %
        %   See also parallel.cluster.HPCServer.ClusterVersion.
        UseSOAJobSubmission
    end

    properties (Transient, GetAccess = private, SetAccess = immutable)
        ServerConnection
        IsSoaSupported = false
        IsJobTemplateSupported = false
    end
    
    properties ( Constant, Access = private )
        %MinimalMementoProperties A string array that defines the
        % properties needed for a minimal cluster memento (i.e. one that
        % allows a cluster object to be constructed that can find the
        % original cluster's jobs). A property should only be included if
        % it is required in the memento for this class and every sub-class.
        %
        % A sub-class can define additional properties to be included in
        % the memento in a constant, private property of the same name. A
        % sub-class should not include properties that are included by any
        % of its super-classes.  A super-class should only define a
        % property if it should be included in the memento of each of its
        % sub-classes.
        %
        % The full list of properties required by any class can be gathered
        % using hDetermineMementoProperties.
        MinimalMementoProperties = [ ...
            "Host", ...
            "ClusterVersion" ];
        
        %FullMementoProperties A string array that defines the properties
        % needed, in addition to those listed in MinimalMementoProperties,
        % for a full cluster memento (i.e. one that allows a cluster object
        % to be constructed that can find the original cluster's jobs). A
        % property should only be included if it is required in the memento
        % for this class and every sub-class.
        %
        % A sub-class can define additional properties to be included in
        % the memento in a constant, private property of the same name. A
        % sub-class should not include properties that are included by any
        % of its super-classes.  A super-class should only define a
        % property if it should be included in the memento of each of its
        % sub-classes.
        %
        % The full list of properties required by any class can be gathered
        % using hDetermineMementoProperties.
        FullMementoProperties =  [ ...
            "Name", ...
            "ClusterMatlabRoot", ...
            "JobTemplate", ...
            "JobDescriptionFile", ...
            "UseSOAJobSubmission" ];
        
        % A string array that stores all the properties needed for a
        % minimal cluster memento for this class. It should include all
        % those defined in MinimalMementoProperties in this class and in 
        % all superclasses.
        AllPropertiesForMinimalMemento = parallel.Cluster.hDetermineMementoProperties( ...
            ?parallel.cluster.HPCServer, parallel.internal.types.ClusterMementoType.Minimal );
        
        % A string array that stores all the properties needed for a full
        % cluster memento for this class. It should include all those
        % defined in MinimalMementoProperties and FullMementoProperties in
        % this class and in all superclasses.
        AllPropertiesForFullMemento = parallel.Cluster.hDetermineMementoProperties( ...
            ?parallel.cluster.HPCServer, parallel.internal.types.ClusterMementoType.Full );
    end
   
    methods % constructor
        function obj = HPCServer(varargin)
        %HPCServer Create an HPCServer cluster instance
        %   parallel.cluster.HPCServer(p1, v1, p2, v2, ...) builds an
        %   HPCServer cluster with the specified property values. The
        %   properties can include any of the properties of the HPCServer
        %   cluster class, or a profile.
        %
        %   Example:
        %   % Create an HPCServer cluster using the 'myHpcServer' profile.
        %   myCluster = parallel.cluster.HPCServer('Profile', 'myHpcServer');
        %
        %   See also parcluster, parallel.Cluster.        
            import parallel.internal.apishared.HPCServerUtils
            import parallel.internal.apishared.OsType
            import parallel.internal.cluster.ConstructorArgsHelper
            import parallel.internal.settings.ValueType
            
            % Storage input variant is only intended to be used on the
            % Workers, with a single AbstractStorage object argument
            if nargin == 1 && isa(varargin{1}, 'parallel.internal.cluster.AbstractStorage') 
                
                storage = varargin{1};
                
                cProf = '';
                propsFromArgs = containers.Map;
                clusterVersion = HPCServerUtils.getDefaultClientVersion();
                versionValType = parallel.internal.settings.ValueType.Factory;
                host = iDeduceHostFromVersion(clusterVersion);
                hostValType = parallel.internal.settings.ValueType.Factory;
                
                serverConnection = HPCServerUtils.getServerConnection(host, clusterVersion);
                [namesFromEnv, valsFromEnv, propFilename] = ...
                    HPCServerUtils.getValuesFromClusterEnvironmentForCurrentVersion(serverConnection);
                [~, storLocValType, namesFromEnv, valsFromEnv] = ...
                    iDeduceDataLocation(propsFromArgs, namesFromEnv, valsFromEnv);
                dataLoc = [];
                
            else
                try
                    [propsFromArgs, cProf] = ConstructorArgsHelper.interpretClusterCtorArgs(...
                        ?parallel.cluster.HPCServer, varargin{:});
                catch err
                    throw(err);
                end

                try
                    [host, hostValType, clusterVersion, versionValType] = ...
                        iDeduceHostAndClusterVersion(propsFromArgs);
                    serverConnection = HPCServerUtils.getServerConnection(host, clusterVersion);
                catch err
                    throw(err);
                end
                % Get the factory values from the cluster environment - the DataLocation to use
                % may be defined here.
                try
                    [namesFromEnv, valsFromEnv, propFilename] = ...
                        HPCServerUtils.getValuesFromClusterEnvironmentForCurrentVersion(serverConnection);
                catch err
                        throw(err); 
                end

                [dataLoc, storLocValType, namesFromEnv, valsFromEnv] = ...
                    iDeduceDataLocation(propsFromArgs, namesFromEnv, valsFromEnv);

                 storage = parallel.internal.cluster.FileStorage.createFileStorage(dataLoc);
            end

            % Call the CJSCluster constructor now that we have storage
            obj@parallel.cluster.CJSCluster('HPCServer', cProf, storage, storLocValType, dataLoc);
            obj.ServerConnection = serverConnection;
            isHPCServer = isa(obj.ServerConnection, 'distcomp.HPCServerSchedulerConnection');
            obj.IsSoaSupported         = isHPCServer;
            obj.IsJobTemplateSupported = isHPCServer;

            % Factory values for HPCServer properties
            obj.PropStorage.addFactoryValue('Host',                host);
            obj.PropStorage.setValueType(   'Host',                hostValType);
            obj.PropStorage.addFactoryValue('ClusterVersion',      clusterVersion);
            obj.PropStorage.setValueType(   'ClusterVersion',      versionValType);
            obj.PropStorage.addFactoryValue('Name',                '');
            % Factory values that come from the server connection
            obj.setOrAddFactoryPropsFromConnection();
            % Factory values from the cluster environment
            obj.setFactoryPropsFromClusterEnvironment(namesFromEnv, valsFromEnv, propFilename);
            % Factory values for CJSCluster properties
            obj.PropStorage.setValue('OperatingSystem', ValueType.Factory, OsType.PC.Api2Name);
            % Ensure UseSOAJobSubmission is false for workers because we can't 
            % create the SOA client properly on a worker
            if system_dependent('isdmlworker')
                obj.IsSoaSupported = false;
                obj.UseSOAJobSubmission = false;
                obj.PropStorage.setValueType('UseSOAJobSubmission', ValueType.Factory);
            end

            obj.applyConstructorArgs(propsFromArgs);
        end
    end
    
    methods (Hidden) % Overrides of Public, Hidden methods on CJSCluster
        function hSetProperty(obj, names, values)
        % Extra checks for properties that this object does not allow to be set.
            if ~iscell(names)
                names  = {names};
                values = {values};
            end
            for ii = 1:length(names)
                name  = names{ii};
                value = values{ii};
                switch name
                  case {'HasSharedFilesystem', 'OperatingSystem'}
                    if obj.isNotCurrentValue( name, value )
                        throwAsCaller( obj.buildCannotModify( name ) );
                    end
                  case 'NumWorkers'
                    obj.ServerConnection.MaximumNumberOfWorkersPerJob = value;
                  case 'JobTemplate'
                    if obj.IsJobTemplateSupported
                        obj.ServerConnection.JobTemplate = value;
                    else
                        if ~isempty(value)
                            % Error only if user is setting a non-empty value and 
                            % job templates aren't supported
                            throwAsCaller(MException(message('parallel:cluster:HPCServerJobTemplatesUnsupported', obj.Host, obj.ClusterVersion)));
                        end
                    end
                  case 'JobDescriptionFile'
                    obj.ServerConnection.JobDescriptionFile = value;
                  case 'UseSOAJobSubmission'
                    if obj.IsSoaSupported || ~value
                        obj.ServerConnection.UseSOAJobSubmission = value;
                    else
                        if system_dependent('isdmlworker')
                            % Error only if user is setting to true and SOA isn't supported
                            throwAsCaller(MException(message('parallel:cluster:HPCServerSoaJobsUnsupportedOnWorker')));
                        else
                            % Error only if user is setting to true and SOA isn't supported
                            throwAsCaller(MException(message('parallel:cluster:HPCServerSoaJobsUnsupported', obj.Host, obj.ClusterVersion)));
                        end
                    end
                end
                obj.hSetProperty@parallel.cluster.CJSCluster(name, value);
            end
        end
        % Add Cluster Properties to Display
        function [clusterPropertyMap, propNames]  = hGetDisplayItems(obj, diFactory)
            clusterPropertyMap = hGetDisplayItems@parallel.Cluster(obj, diFactory);

            % The order the properties appear in the propNames array is the
            % order in which they will be displayed. 
            propNames = {...
                'Name', ...
                'Profile', ...
                'Modified', ...
                'Host', ...
                'NumWorkers', ...
                'NumThreads', ...
                'Separator', ...
                'JobStorageLocation', ...
                'ClusterMatlabRoot', ...
                'ClusterVersion', ...
                'JobTemplate', ...
                'JobDescriptionFile', ...
                'UseSOAJobSubmission', ...
                'RequiresOnlineLicensing', ...
                'PreferredPoolNumWorkers'};
            
            % Add cluster specific items to map for display
            names = {...
                'Name', ...
                'ClusterVersion',...
                'JobTemplate', ...
                'JobDescriptionFile',...
                'UseSOAJobSubmission'};
            values = diFactory.makeMultipleItems(@createDefaultItem, obj.hGetDisplayPropertiesNoError(names));
            clusterPropertyMap = [clusterPropertyMap; containers.Map(names, values)];
        end
        
    end
    
    methods (Hidden) % Implementation of Public, Hidden, Abstract methods on CJSCluster
        % hDestroyJob - contact the underlying scheduler to remove the job
        function hDestroyJob(cluster, job)
            cluster.cancelOrDestroyJob(job);
        end

        % hDestroyTask - contact the underlying scheduler to remove the task
        function hDestroyTask(cluster, task)
            cluster.cancelOrDestroyTask(task);
        end

        % hCancelJob - contact the underlying scheduler to cancel execution of a
        % job. Return true if execution was cancelled.
        function didCancel = hCancelJob(cluster, job)
            didCancel = cluster.cancelOrDestroyJob(job);
        end

        % hCancelTask - contact the underlying scheduler to cancel execution of
        % a task. Return true if execution was cancelled.
        function didCancel = hCancelTask(cluster, task)
            didCancel = cluster.cancelOrDestroyTask(task);
        end

        % hSubmit*Job - contact the underlying scheduler to schedule execution
        % of the job.
        hSubmitIndependentJob(cluster, job, jobSupport, jobSupportID)
        hSubmitCommunicatingJob(cluster, job, jobSupport, jobSupportID)
    end

    methods (Access = protected) % Overrides of Protected methods on CJSCluster
        function logText = getSpecificDebugLogForCommunicatingJob(obj, cJob, varargin)
            import parallel.internal.types.States
            if cJob.StateEnum == States.Failed
                % Just check for the correct host before getting failed info because 
                % there is no SOA specific stuff that needs to happen.
                [~, correctHost, ~, jobData] = obj.getJobSchedDataIfAvailable(cJob);
                if correctHost
                    logText = obj.ServerConnection.getSchedulerDetailsForFailedJob(...
                        jobData.SchedulerJobID, jobData.SchedulerTaskIDs(1));
                end
            else 
                logText = '';
            end
        end

        function logText = getSpecificDebugLogForIndependentTask(obj, iJob, task, varargin)
        % Return IndependentJob.Task debug log
            import parallel.internal.types.States

            if iJob.StateEnum == States.Failed
                % Just check for the correct host before getting failed info because 
                % there is no SOA specific stuff that needs to happen.
                [~, correctHost, ~, jobData] = obj.getJobSchedDataIfAvailable(iJob);
                if correctHost
                    if jobData.IsSOAJob
                        ccsTaskID = [];
                    else
                        ccsTaskID = jobData.getMicrosoftTaskIDFromMatlabID(task.Id);
                        % Did we find the relevant ccsTaskID?
                        if isempty(ccsTaskID) || numel(ccsTaskID) > 1
                            warning(message('parallel:cluster:HPCServerGetCannotFindTaskID'));
                            ccsTaskID = [];
                        end
                    end
                end
                logText = obj.ServerConnection.getSchedulerDetailsForFailedJob(...
                    jobData.SchedulerJobID, ccsTaskID);
            else
                logText = '';
            end
        end
    end

    methods (Access = protected) % Implementation of Protected, Abstract methods on CJSCluster
        % Use the underlying cluster mechanisms to see if there's a better value
        % for the jobState given the state in the storage
        function jobState = getJobState(obj, job, ~)
            import parallel.internal.types.States
            
            % Default is just to return unknown
            jobState = States.Unknown;
            
            % Just check for the correct host before getting state because there
            % is no SOA specific stuff that needs to happen.
            [~, correctHost, ~, jobData] = obj.getJobSchedDataIfAvailable(job);
            if ~correctHost
                return;
            end
            
            try
                schedulerState = obj.ServerConnection.getJobStateByID(jobData.SchedulerJobID);
            catch err %#ok<NASGU>
                % Failed to retrieve the schedulerState, so leave the state alone
                return;
            end

            jobState = States.fromName(schedulerState);
        end

        % This check simply tests whether a job was submitted by this Cluster
        % type.
        function tf = wasSubmittedByThisType(obj, job)
            tf = obj.getJobSchedDataIfAvailable(job);
        end
        
        function [names, values] = getClusterSpecificEnvVars( ~ )
        % Return names/values of environment variables that this cluster must
        % send down to workers
            names  = {};
            values = {};
        end
        
        % Get all the properties needed for the ClusterMemento
        function props = getAllMementoProperties( obj, clusterMementoType )
            import parallel.internal.types.ClusterMementoType

            switch clusterMementoType
                case ClusterMementoType.Minimal
                    props = obj.AllPropertiesForMinimalMemento;
                case ClusterMementoType.Full
                    props = obj.AllPropertiesForFullMemento;
                otherwise
                    % Default to the full memento
                    props = obj.AllPropertiesForFullMemento;
            end
        end
    end
    
    methods (Access = private)
        function delete( obj )
            delete@handle( obj );
        end

        function setOrAddFactoryPropsFromConnection(obj)
            import parallel.internal.settings.ValueType
            if obj.IsJobTemplateSupported
                jobTemplate = obj.ServerConnection.JobTemplate;
            else
                jobTemplate = '';
            end
            jobDescFile = obj.ServerConnection.JobDescriptionFile;
            if obj.IsSoaSupported
                useSoa = obj.ServerConnection.UseSOAJobSubmission;
            else
                useSoa = false;
            end
            numWorkers = obj.ServerConnection.MaximumNumberOfWorkersPerJob;
            
            obj.PropStorage.addFactoryValue('JobTemplate',         jobTemplate);
            obj.PropStorage.addFactoryValue('JobDescriptionFile',  jobDescFile);
            obj.PropStorage.addFactoryValue('UseSOAJobSubmission', useSoa);
            obj.PropStorage.setValue('NumWorkers', ValueType.Factory, numWorkers);
        end
        
        function setFactoryPropsFromClusterEnvironment(obj, names, vals, propFilename)
        % Set the sys-admin defined properties from the cluster environment
        % and as the as factory values.
            import parallel.internal.settings.ValueType
            
            erroredNameIdx = false(numel(names), 1);
            for ii = 1:numel(names)
                currName = names{ii};
                try
                    obj.hSetProperty(currName, vals{ii});
                catch err %#ok<NASGU>
                    % This is OK - we'll just stick with the factory values
                    % that we've got already.
                    erroredNameIdx{ii} = true;
                end
                obj.PropStorage.setValueType(currName, ValueType.Factory);
            end
            
            if any(erroredNameIdx)
                erroredNames = names(erroredNameIdx);
                warning(message('parallel:cluster:HPCServerSetFactoryValuesFromEnvironmentFailed', propFilename, strtrim( sprintf( '%s ', erroredNames{ : } ) )));
            end
        end
        
        function [correctType, correctHost, correctHostAndVersion, jobData] = getJobSchedDataIfAvailable(obj, job)
            jobData = job.JobSchedulerData;
            correctType           = ~isempty(jobData) && isa(jobData, 'distcomp.MicrosoftJobSchedulerData');
            correctHost           = correctType       && strcmpi(jobData.SchedulerName, obj.Host);
            correctHostAndVersion = correctHost       && strcmp(jobData.APIVersion, obj.ClusterVersion);
        end
        
        function didCancel = cancelOrDestroyJob(obj, job)
            import parallel.internal.types.States
            didCancel = false;
            % Must have correct host and version to cancel/destroy because there may be
            % some SOA specific cleanup required.
            [~, ~, correctHostAndVersion, jobData] = obj.getJobSchedDataIfAvailable(job);
            if ~correctHostAndVersion
                return
            end
            
            jobState = job.StateEnum; % Fully derive job state
            % TODO:later Move this logic into States?
            jobNeedsCancelling = jobState > States.Pending && jobState < States.Finished;
            if ~jobNeedsCancelling
                return
            end

            % Cancel the job using the scheduler.
            try
                obj.ServerConnection.cancelJob(jobData);
                % We succeeded
                didCancel = true;
            catch err
                warning(message('parallel:cluster:HPCServerCancelOrDestroyJobFailed', err.message));
            end
        end
        
        function didCancel = cancelOrDestroyTask(obj, task)
            didCancel = false;
            job = task.Parent;
            % Must have correct host and version to cancel/destroy because there may be
            % some SOA specific cleanup required.
            [~, ~, correctHostAndVersion, jobData] = obj.getJobSchedDataIfAvailable(job);
            if ~correctHostAndVersion
                return
            end
            % TODO:later Do a State check on the task (like Local does?)

            % Was this task from an SOA job?
            if jobData.IsSOAJob
                % We don't know how to cancel individual tasks on an SOA job
                warning(message('parallel:cluster:HPCServerSoaTaskCancel'));
                return
            end

            % Finally let's get the actual jobID and taskID on the scheduler.
            ccsJobID = jobData.SchedulerJobID;
            ccsTaskID = jobData.getMicrosoftTaskIDFromMatlabID(task.Id);
            % Did we find the relevant ccsTaskID?
            if isempty(ccsTaskID) || numel(ccsTaskID) > 1
                warning(message('parallel:cluster:HPCServerCannotFindTask'));
                return
            end

            try
                obj.ServerConnection.cancelTaskByID(ccsJobID, ccsTaskID);
                didCancel = true;
            catch err
                warning(message('parallel:cluster:HPCServerCancelOrDestroyTaskFailed', err.message));
            end
        end
    end
    
    methods (Static, Access = private)
        function [username, password] = getDefaultUserCredentials()
            import parallel.internal.apishared.HPCServerUtils
            username = HPCServerUtils.getUsername();
            % If we specify an empty password, the user should get prompted for a password
            % by Windows
            password = '';
        end
    end
    
    methods ( Static, Hidden )
        function SchedulerIDs = hSimplifySchedulerIDs( ~, SchedulerIDs )
            % Simplifies the list of task SchedulerIDs for this job
            
            % if empty, job does not have an ID so should also return empty
            if isempty(SchedulerIDs)
                return;
            end
            
            % For independent job, using SOA:
            %   we have == {'123,456'}
            %   we want == {'123,456'}
            % we like this as str2num converts to a numeric vector
            % For independent job, non-SOA:
            %   we have == {'123.1', '123.2', ...}
            %   we want == {'123'}
            % For communicating job:
            %   we have == {'123.1'}
            %   we want == {'123'}
            % so in any case can select everything up to the first dot
            SchedulerIDs = {regexp(SchedulerIDs{1},'[^\.]+','match','once')};
        end
    end
end
%#ok<*ATUNK>
%---------------------------------------------------------
function [val, valType] = iGetAndRemovePropFromArgs(propsFromArgs, propName)
% [val, valType] = iGetAndRemovePropFromArgs(propsFromArgs, propName)
% Get a property and its value type from the propsFromArgs map and
% then remove that property from the map.
valAndType = propsFromArgs(propName);
val        = valAndType{1};
valType    = valAndType{2};
propsFromArgs.remove(propName);
end

%---------------------------------------------------------
function [host, hostValType, clusterVersion, versionValType] = ...
    iDeduceHostAndClusterVersion(propsFromArgs)
import parallel.internal.apishared.HPCServerUtils

% How to deduce Host and ClusterVersion
% 1. If both Host and ClusterVersion are in the propsFromArgs, then 
% just use those values.
% 2. If the propsFromArgs contains only Host, then deduce the cluster 
% version by trying to create a server connection to that host from 
% most recent API to oldest API.
% 3. If propsFromArgs contains only ClusterVersion, then use Host from 
% the environment variable or perform AD search using that ClusterVersion
% 4. If neither Host nor ClusterVersion are in propsFromArgs, then
% first deduce the ClusterVersion from the version of the HPC Server 
% client utilities then use this Cluster Version to deduce the Host as
% in (3).

haveHost    = false;
haveVersion = false;
if propsFromArgs.isKey('Host')
    haveHost = true;
    [host, hostValType] = iGetAndRemovePropFromArgs(propsFromArgs, 'Host');
end
if propsFromArgs.isKey('ClusterVersion')
    haveVersion = true;
    [clusterVersion, versionValType] = iGetAndRemovePropFromArgs(...
        propsFromArgs, 'ClusterVersion');
end
if haveHost && haveVersion
    return;
end

if haveHost
    clusterVersion = iDeduceClusterVersionFromHost(host);
    versionValType = parallel.internal.settings.ValueType.Factory;
else
    if ~haveVersion
        clusterVersion = HPCServerUtils.getDefaultClientVersion();
        versionValType = parallel.internal.settings.ValueType.Factory;
    end
    
    host = iDeduceHostFromVersion(clusterVersion);
    hostValType = parallel.internal.settings.ValueType.Factory;
end

end

%---------------------------------------------------------
function val = iDeduceHostFromVersion(clusterVersion)
import parallel.internal.apishared.HPCServerUtils
val = HPCServerUtils.findHeadNodeFromEnvironment();
if ~isempty(val)
    return;
end

headNodesFromAD = HPCServerUtils.findHeadNodesFromAD(clusterVersion);
if numel(headNodesFromAD) == 1
    val = headNodesFromAD{1};
elseif isempty(headNodesFromAD)
    error(message('parallel:cluster:HPCServerCannotDetermineDefaultHost'));
else
    error(message('parallel:cluster:HPCServerFoundTooManyHeadNodes', sprintf( '\t%s\n', headNodesFromAD{ : } )));
end
end

%---------------------------------------------------------
function val = iDeduceClusterVersionFromHost(host)
import parallel.internal.apishared.HPCServerUtils
% Try the cluster versions in order until we succeed
% in getting a connection
sortedVersions = iGetSortedVersions();
for ii = 1:numel(sortedVersions)
    currVersion = sortedVersions(ii).Name;
    try
        HPCServerUtils.getServerConnection(host, currVersion);
        val = currVersion;
        return;
    catch err %#ok<NASGU>
    end
end

error(message('parallel:cluster:HPCServerCannotDeduceClusterVersion', host));
end

%---------------------------------------------------------
function sortedVers = iGetSortedVersions()
persistent sortedVersions
if isempty(sortedVersions)
    m = enumeration('parallel.internal.types.HPCServerClusterVersion');
    [~, sortedIdx] = sort([m.VersionNumber], 'descend');
    sortedVersions = m(sortedIdx);
end
sortedVers = sortedVersions;
end

%---------------------------------------------------------
function [dataLoc, dataLocValType, namesFromCluster, valsFromCluster] = ...
    iDeduceDataLocation(propsFromArgs, namesFromCluster, valsFromCluster)

import parallel.cluster.CJSCluster
import parallel.internal.settings.ValueType

[dataLoc, dataLocValType] = CJSCluster.chooseDataLocation(propsFromArgs);

% Check to see if we have JobStorageLocation defined in the cluster
% environment.  If it isn't, then just stick with whatever we got
% from the propsFromArgs.  Otherwise, if the dataLocValType is factory, 
% then we should use the cluster environment value as the factory value
% instead and then strip it out from the cluster environment value. 
% If dataLocValType is something other than factory, then we 
% should strip out the cluster environment value to stop it from overriding
% the non-factory value..
dataLocIdx = strcmp('JobStorageLocation', namesFromCluster);
if ~any(dataLocIdx)
    return;
end

if dataLocValType == ValueType.Factory
    dataLoc = valsFromCluster{dataLocIdx};
    % Create the data location if necessary as it may not exist yet.
    if exist(dataLoc, 'dir') ~= 7
        [~, msg, messageID] = mkdir(dataLoc);
        
        if exist(dataLoc, 'dir') ~= 7
            mkdirError = MException(messageID, '%s', msg);
            err = MException(message('parallel:cluster:HPCServerUtilsFailedToCreateDataLocation', dataLoc));
            err = err.addCause(mkdirError);
            throw(err);
        end
    end
end
namesFromCluster(dataLocIdx) = [];
valsFromCluster(dataLocIdx) = [];

end
