function dctEvaluateTask(postFcns, finishFcn)
% This function is used to evaluate a task from any scheduler

%  Copyright 2006-2021 The MathWorks, Inc.

% Get the job and task objects
root = distcomp.getdistcompobjectroot;
job = root.CurrentJob;
task = root.CurrentTask;
runprop = root.CurrentRunprop;

% Allow ourselves one reportable error, which will be the first encountered. 
% This will be returned to the user and the task function will NOT be run.
% All other normal startup will happen
reportableException = [];

import parallel.internal.apishared.EnvVars
% Set any environment variables copied over from the client session
if ~isempty(runprop.EnvironmentVariablesMap)
    environmentVariablesToSet = runprop.EnvironmentVariablesMap.keys();
    variablesAlreadyDefined = EnvVars.checkIfEnvironmentVariableDefined(environmentVariablesToSet{:});
    % Store original value of those environment variables which are changed
    originalEnvironmentVariablesMap = EnvVars.getEnvironmentVariablesMap(environmentVariablesToSet(variablesAlreadyDefined));
    % Store new environment variables to be cleared on clean up
    environmentVariablesToClear = environmentVariablesToSet(~variablesAlreadyDefined);
    iSetEnvironmentVariables(runprop.EnvironmentVariablesMap);
    postFcns = [...
                {{@iSetEnvironmentVariables, originalEnvironmentVariablesMap}} ...
                {{@iUnSetEnvironmentVariables, environmentVariablesToClear}} ...
                postFcns ];
end
            
% Performs MATLAB Parallel Server specific job initialization work,
% such as extracting the FileDependencies zip file and adding the
% appropriate directories to the MATLAB path.
try
    dctSchedulerMessage(4, 'About to pPreJobEvaluate');
    % Tell the job we are about to evaluate it
    job.pPreJobEvaluate(task);
catch e
    if isa(e, 'distcomp.ReportableException') 
        if isempty(reportableException)
            reportableException = e;
        end
    else
        dctSchedulerMessage(1, 'Failed pPreJobEvaluate. Error:', e);
        throw(distcomp.ExitException(e, 'PreJobEvaluate'));
    end
end

try
    dctSchedulerMessage(4, 'About to pPreTaskEvaluate');
    % Tell the task we are about to evaluate it.
    task.pPreTaskEvaluate;
catch e
    if isa(e, 'distcomp.ReportableException') 
        if isempty(reportableException)
            reportableException = e;
        end
    else
        dctSchedulerMessage(1, 'Failed pPreTaskEvaluate. Error:', e);
        throw(distcomp.ExitException(e, 'PreTaskEvaluate'));
    end
end

% Set default outputs
DEFAULT_OUTPUT = {};
DEFAULT_TEXT = {''};
DEFAULT_WARNING = matlab.internal.diagnostic.Warning.empty(1, 0);
taskEvaluatedOK = false;
isAsyncTask = false;
taskPostFcn = @iDoNothing;

% Set number of warnings to store. This uses environment variables so must
% be called after any environment variables are copied from client.
iSetWarningsCollectorBufferSize(runprop);

if isempty(reportableException)
    try
        % Carry out the evaluation
        [resultsFcn, taskPostFcn, taskEvaluatedOK, isAsyncTask] = iEvaluateTask(job, task, runprop);
    catch e
        if isa(e, 'distcomp.ReportableException')
            % Define a suitable results function anonymously
            resultsFcn = @() iReturnResults(DEFAULT_OUTPUT, e.CauseException, DEFAULT_TEXT, DEFAULT_WARNING);
        elseif isa(e, 'distcomp.ExitException')
            rethrow(e);
        else
            throw(distcomp.ExitException(e, 'TaskEvaluate'));        
        end
    end
else
    % Define a suitable results function anonymously
    resultsFcn = @() iReturnResults(DEFAULT_OUTPUT, reportableException.CauseException, DEFAULT_TEXT, DEFAULT_WARNING);
end

try
    postFcns = [...
                {{@iFinishTask, finishFcn, job, task, resultsFcn}} ...
                postFcns ];
    dctSchedulerMessage(4, 'isAsyncTask: %d, taskEvaluatedOK: %d', isAsyncTask, taskEvaluatedOK);
    if isAsyncTask && taskEvaluatedOK
        % Need to use the evaluator's strategy for evaluating functions in pool shutdown
        taskPostFcnEvaluator = runprop.TaskEvaluator.createPoolShutdownEvaluator;
        taskPostFcnWithEvaluator = @() taskPostFcn( taskPostFcnEvaluator );

        postFcns = [...
                    {{taskPostFcnWithEvaluator}} ...
                    postFcns ];
        % If the job is interactive then save the call to iFinishTask in the postFcns
        % and drop back to the base workspace, expecting someone to call
        % dctFinishInteractiveSession at some point in the future.
        dctStoreFunctionArray('set', postFcns);
    else
        taskPostFcnWithEvaluator = @() taskPostFcn( runprop.TaskEvaluator );
        postFcns = [...
                    {{taskPostFcnWithEvaluator}} ...
                    postFcns ];
        dctEvaluateFunctionArray(postFcns);
    end
catch e
    throw(distcomp.ExitException(e, 'TaskFinish'));
end

end

% -------------------------------------------------------------------------
% Wrapper to call the finish function passed into this file on the correct
% data extracted from the results function
% -------------------------------------------------------------------------
function iFinishTask(finishFcn, job, task, resultsFcn)
finishFcn( job, task, resultsFcn() );
end

% -------------------------------------------------------------------------
% Do nothing - needed as task finished function if the task fails
% -------------------------------------------------------------------------
function iDoNothing( varargin )
end

% -------------------------------------------------------------------------
% Internal function to evaluate the task
% -------------------------------------------------------------------------
function [resultsFcn, postTaskFcn, taskEvaluatedOK, isAsyncTask] = iEvaluateTask(job, task, runprop)

% Set default outputs that will be used by the nested functions - this is 
% the only data that should be shared by the nested functions. All the rest
% should remain within the individual functions scope
output = {};
cellTextOutput = {''};
errOutput = ParallelException.empty();
warnOutput = matlab.internal.diagnostic.Warning.empty(1, 0);
% Did we succeed - this will be set to true in nEvaluateTask if we eval OK
taskEvaluatedOK = false;

% The default is for tasks to be synchronous. If a pool is set up, then we
% need to reconsider.
isAsyncTask = false; 

evaluator = runprop.TaskEvaluator;
% Store the paths we added at the start of the task, so we can remove only
% those that were added.
addedPaths = {};

% This function handle will return the correct structure of results for the
% finish function to use
resultsFcn = @nReturnResults;
% Function that will be called when the task really finishes. Until we have
% actually executed the task this is nothing, but later it is the nested
% task finish function
postTaskFcn = @iDoNothing;

% As we have setup the outputs from this function it is now safe to return
% at any time.
nEvaluateTask();
% Did we get to the end?
if taskEvaluatedOK
    % Only once the function is complete should we set the task finish
    % function appropriately.
    postTaskFcn = @( taskFinishEvaluator ) nPostTaskFinish( taskFinishEvaluator );
    % Resolve finish function in case of path change after this
    taskFinishFcn = @taskFinish;
end

return
% Below this there will only be nested functions that use the above
% variables to execute the parts of the task.

    function out = nReturnResults
        out = iReturnResults(output, errOutput, cellTextOutput, warnOutput);
    end

    function nEvaluateTask
        import parallel.internal.types.Variant
        try
            dctSchedulerMessage(4, 'About to add job dependencies');
            % Add any PathDependencies or FileDependencies - this function can
            % throw errors and ReportableException to indicate failure.
            try
                % evalc this command to prevent output from being written
                % to the MATLAB command window (and possibly captured in
                % the diary) while still allowing it to issue warnings. To
                % ensure only output from user code gets captured in diary
                % for MVM, we should really break dctEvaluateTask into
                % multiple fevals. See g2071147.
                [evalcOutput, addedPaths] = evalc('iAddJobDependencies(job, runprop)');
                dctSchedulerMessage(4, 'Captured output from iAddJobDependencies:\n%s', evalcOutput);
            catch e
                dctSchedulerMessage(0, 'Caught an error during nEvaluateTask:', e);
                if isa(e, 'distcomp.ReportableException')
                    throw(e)
                else
                    throw(distcomp.ExitException(e, 'AddJobDependencies'));
                end
            end
            
            if runprop.IsFirstTask
                dctSchedulerMessage(2, 'About to call jobStartup: "%s"', which('jobStartup'));
                % Perform user provided job startup steps for the new job.  Any
                % errors thrown here will appear in the Task's ErrorMessage and
                % ErrorIdentifier fields and text output will be captured
                [~, errOutput, cellTextOutput{end+1}] = evaluator.evaluate(@jobStartup, 0, {job});
                if iThrewError(errOutput)
                    dctSchedulerMessage(0, 'Error during jobStartup: ', errOutput);
                    % Set error message and identifier
                    errOutput = iUpdateError(errOutput, 'parallel:job:JobStartupUserError', which('jobStartup'));
                    % No point in continuing if we threw an error
                    return;
                end
            end
            
            dctSchedulerMessage(2, 'About to call taskStartup: "%s"', which('taskStartup'));
            % Perform task startup steps for the new task.  Any errors thrown
            % here will appear in the Task's ErrorMessage and ErrorIdentifier
            % fields
            [~, errOutput, cellTextOutput{end+1}] = evaluator.evaluate(@taskStartup, 0, {task});
            if iThrewError(errOutput)
                dctSchedulerMessage(0, 'Error during taskStartup:', errOutput);
                % Set error message and identifier
                errOutput = iUpdateError(errOutput, 'parallel:task:TaskStartupUserError', which('taskStartup'));
                % No point in continuing if we threw an error
                return;
            end
            
            try
                dctSchedulerMessage(4, 'About to get evaluation data');
                % Get all data necessary for task evaluation - this might throw errors
                % if the task has been deleted or is not available.
                [fcn, nOut, args] = task.pGetEvaluationData;
            catch errOutput
                dctSchedulerMessage(0, 'Error during pGetEvaluationData: ', errOutput);
                errOutput = iUpdateError(errOutput, 'parallel:task:GetInputData');
                return
            end

            % Decide whether we want to labBarrier before evaluating the task.
            % In some circumstances the isa call can take along time (see
            % G617208). Since we have a limited amount of time after
            % pInstantiatePool (see note below) we do this before the pool is
            % instantiated.
            % 
            % Currently we are only doing this for CJSCommunicatingJob but we
            % might well consider doing this for all jobs. We have held off
            % because this might negatively impact our jobmanager until we are
            % more knowledgeable about mpi library unloading and cleanup.
            %
            % Don't do it for parallel pool jobs because they are already
            % undertaking synchronization in distcomp.nop and session
            % startup. Thus the race condition does not apply.
            if isa( job, 'parallel.job.CJSCommunicatingJob' )
                synchronizeBeforeTaskEvaluation = ...
                    ~isequal( job.Type, Variant.CommunicatingPoolJob.Name );
            else
                synchronizeBeforeTaskEvaluation = false;
            end

            try
                % If we are a parallel pool (interactive or batch) then startup now.
                % It is very important that this happen as close to the end of task
                % evaluation as possible as the timeout for pool connection is only
                % 30s. If this happens too early and there are problems with the
                % scheduler or file system then we will timeout. We were seeing
                % this issue during testing, and hence moved this here. One other
                % point to note is that ANY delay in taskStartup will be part of
                % the pool timeout and hence can STOP pools being built. It is by
                % design that jobStartup occur before pool instantiation and
                % taskStartup after - this allows user code to run in both of the
                % available contexts.
                isPoolJob = job.pIsMatlabPoolJob;
                feval('_mpi_usePollingComms', isPoolJob);
                dctSchedulerMessage(4, 'Set _mpi_usePollingComms to %d', isPoolJob);
                
                if isPoolJob
                    dctSchedulerMessage(4, 'About to pInstantiatePool');
                    job.pInstantiatePool(task, fcn, nOut, args);
                    dctSchedulerMessage(4, 'Pool instantiation complete');

                    % Because currently not all pool evaluations are
                    % synchronous, the pool setup can optionally set up an
                    % environment variable to indicate that the pool execution
                    % is sychronous. job.pCurrentTaskIsPartOfAPool is true
                    % except for the lead worker in a batch-pool (which is
                    % always sycnchronous).
                    isAsyncTask = job.pCurrentTaskIsPartOfAPool() && isempty(getenv('MDCS_SYNC_TASK_FCN'));
                    dctSchedulerMessage(4, 'Is async task? %d "%s"', isAsyncTask, getenv('MDCS_SYNC_TASK_FCN'));
                    
                    % If a pool has been built we need to tell it about the
                    % dependency map so that updates to those files can be
                    % correctly replicated. However in the case where only
                    % 1 worker has been requested there will be no pool and
                    % the sessionObject will be empty.
                    sessionObject = parallel.internal.pool.workerSession();
                    if ~isempty(sessionObject)
                        aff = getAttachedFilesFolder(runprop);
                        sessionObject.setDependencyDir(aff);
                    end
                        
                    dctSchedulerMessage(4, 'About to call poolStartup');
                    % Perform startup steps after the pool is instantiated.  Any errors thrown
                    % here will appear in the Task's ErrorMessage and ErrorIdentifier
                    % fields
                    poolStartupEvaluator = getTaskEvaluatorIfPoolTask();
                    [~, errOutput, cellTextOutput{end+1}] = poolStartupEvaluator.evaluate(@poolStartup, 0, {});
                    if iThrewError(errOutput)
                        % Set error message and identifier
                        errOutput = iUpdateError(errOutput, 'parallel:job:PoolStartupUserError', which('poolStartup'));
                        % No point in continuing if we threw an error
                        return;
                    end
                end
            catch e
                dctSchedulerMessage(0, 'Caught an error during nEvaluateTask:', e);
                if isa(e, 'distcomp.ReportableException')
                    throw(e)
                else
                    throw(distcomp.ExitException(e, 'PoolInstantiation'));
                end
            end

            taskEvaluator = getTaskEvaluatorIfPoolTask();

            % There is a race condition on all schedulers between bits of preceding code and task evaluation
            % where one particular lab might start evaluating the task function much earlier than others.
            % We therefore insert a labBarrier here to ensure that everyone leaves this point at the same time.
            % See comment where synchronizeBeforeTaskEvaluation is assigned for more details.
            if synchronizeBeforeTaskEvaluation
                labBarrier;
            end
            
            % Evaluate the users function here - NOTE that we do not exit the
            % function if this fails - we still might need to release any resources
            % that were created in the taskStartup function.
            dctSchedulerMessage(2, 'Begin task function [%d] = %s(%s %d)', nOut, char(fcn), class(args), numel(args));
            [output, errOutput, cellTextOutput{end+1}] = taskEvaluator.evaluate(fcn, nOut, args);
            errOutput = iBuildParallelException(errOutput);
            dctSchedulerMessage(2, 'End task function');

        catch err
            dctSchedulerMessage(0, 'Error during nEvaluateTask:', err);
            % Catch special error types and simply rethrow - anything else is
            % considered to be a task error.
            if isa(err, 'distcomp.ReportableException') || isa(err, 'distcomp.ExitException')
                rethrow(err)
            end
            % General catch for an unexpected error in the evaluation
            errOutput = iUpdateError(err, 'parallel:task:EvaluateTaskUnexpectedError');
            return
        end
        % The task has now evaluated successfully so indicate as such
        taskEvaluatedOK = true;
    end

    function nPostTaskFinish( taskFinishEvaluator )
        try
            % Initiate stopping of the labs. Do this part only on the client.
            if job.pIsMatlabPoolJob 
                dctSchedulerMessage(4, 'About to shutdown pool');
                poolShutdownOK = job.pShutdownPool;
                dctSchedulerMessage(4, 'Pool shutdown complete - labs shutdown OK == %d', poolShutdownOK);
                iRemovePathDependencies(addedPaths);
                % Need to keep track of the original working directory so we can cd back
                % to it at the end of a pool job. Otherwise the worker labs might prevent
                % the client lab from deleting its working directory when it restarts.
                % See g1369076.
                originalWorkingDirectory = runprop.OriginalWorkingDirectory;
                dctSchedulerMessage(4, 'Changing directory back to original working directory: %s', ...
                    originalWorkingDirectory);
                cd(originalWorkingDirectory);
            end
        catch e
            dctSchedulerMessage(0, 'Error in pool shutdown', e);
            if isa(e, 'distcomp.ReportableException')
                throw(e)
            else
                throw(distcomp.ExitException(e, 'PoolTermination'));
            end
        end
        try
            dctSchedulerMessage(4, 'About to call taskFinish');
            % Perform task finished steps. Note :
            %  1. this will always be evaluated even if the task threw an error.
            %  2. if this errors the task will be treated as having errored
            [~, finishErrOutput, cellTextOutput{end+1}] = taskFinishEvaluator.evaluate(taskFinishFcn, 0, {task});
            
            % If any errors occurred (especially in taskFinish) ensure that there is
            % no output returned to the user
            if iThrewError(finishErrOutput)
                dctSchedulerMessage(0, 'Error in taskFinish:', finishErrOutput);
                errOutput = finishErrOutput;
                output = {};
                % Set error message and identifier
                errOutput = iUpdateError(errOutput, 'parallel:task:TaskFinishUserError', which('taskFinish'));
            end
        catch err
            dctSchedulerMessage(0, 'Error in nPostTaskFinish:', err);
            if isa(err, 'distcomp.ExitException')
                rethrow(err)
            elseif isa(err, 'distcomp.ReportableException') 
                err = err.CauseException;
            end
            % General catch for an unexpected error in the evaluation
            errOutput = iUpdateError(err, 'parallel:task:PostTaskUnexpectedError');
        end
        % The last thing we do is get the warnings, so that all warnings
        % including warnings thrown outside user code are collected.
        warnOutput = iGetWarnings(runprop);
    end

    function eval = getTaskEvaluatorIfPoolTask()
        % Tasks that are part of a pool do their own redirection so we have to
        % use a BasicEvaluator for those workers.
        eval = evaluator;
        if job.pCurrentTaskIsPartOfAPool()
            dctSchedulerMessage(4, 'Pool task. Setting evaluator to BasicEvaluator');
            eval = parallel.internal.evaluator.BasicEvaluator;
        end
    end

end

% -------------------------------------------------------------------------
% This function packs up the output, errOutput and cellTextOutput
% appropriately for result submission.
% -------------------------------------------------------------------------
function out = iReturnResults(output, errOutput, cellTextOutput, warnOutput)
% Now massage the output to concatenate the textual output - remove
% the empty cells and join the rest
cellTextOutput(cellfun(@isempty, cellTextOutput)) = [];
textOutput = sprintf('%s', cellTextOutput{:});
% Package up the output into a structure to pass around easily
out = struct('output', {output}, 'errOutput', {errOutput}, 'textOutput', {textOutput}, 'warnOutput', {warnOutput});
end

% -------------------------------------------------------------------------
% Internal function to correctly add the PathDependencies and
% FileDependencies
% -------------------------------------------------------------------------
function addedPaths = iAddJobDependencies(job, runprop)
addedPaths = {};
if runprop.AppendPathDependencies && ~job.pIsMatlabPoolJob
    % Only add the path dependencies here if this is a non-interactive job.
    % For pool jobs we will add and remove them for every task.
    % Add the paths frozen so MATLAB does not lock them.
    % See g1325877.
    frozen = true;
    addedPaths = parallel.internal.apishared.JobPathHelper.addAdditionalPaths(job.AdditionalPaths, job.NumPathsToAppend, job.MATLABDrivePathMap, frozen);
else
    if job.pIsMatlabPoolJob
        % For interactive jobs we want the path dependencies to be added with change
        % notification available. See g1325877. But actually now we always add
        % the paths in the 'frozen' state, and un-freeze them later if required.
        frozen = true;
        addedPaths = parallel.internal.apishared.JobPathHelper.addAdditionalPaths(job.AdditionalPaths, job.NumPathsToAppend, job.MATLABDrivePathMap, frozen);
    else
        dctSchedulerMessage(4, 'Not adding path dependencies because runprop.AppendPathDependencies is false');
    end
end

if runprop.AppendFileDependencies
    aff = getAttachedFilesFolder(runprop);
    iAddFileDependencies(job, aff);
end

if runprop.AppendFileDependencies || runprop.AppendPathDependencies
    iPostAddDependencies();
end
end

function iRemovePathDependencies(addedPaths)
if ~isempty(addedPaths)
    try
        dctSchedulerMessage(3, 'Removing job''s path dependencies from MATLAB path.');
        
        % Suppress warnings for removing directories that don't exist on
        % the path; this can happen if we failed to add the directory when
        % preparing to execute the task. See g2043235.
        warningSuppressor = parallel.internal.general.WarningSuppressor('MATLAB:rmpath:DirNotFound'); %#ok<NASGU> Cleanup object
        rmpath(addedPaths{:});
    catch err
        % If we fail to remove the path dependencies from this MATLAB this is
        % not fatal, as we will fix the path when the next job runs. Simply log
        % and continue.
        dctSchedulerMessage(4, ...
            'Failed to remove path dependencies. This is not fatal. Original error:', err);
    end
end
end

function iAddFileDependencies(job, attachedFilesFolder)
try
    % Add FileDependencies to the MATLAB path
    dctAddFileDependenciesToPath(job, attachedFilesFolder);
catch err
    if strcmp(err.identifier, 'MATLAB:io:archive:unzip:invalidZipFile')
        msg = message('parallel:task:AddAttachedFilesToPathZipError');
    else
        msg = message('parallel:task:AddAttachedFilesToPath');
    end
    errOutput = MException(msg);
    errOutput = errOutput.addCause(err);
    % Throw a reportable error for the caller to pick up
    throw(distcomp.ReportableException(errOutput));
end
end

function iPostAddDependencies()
% Force MATLAB to pick up the new files - note this takes some time (~0.5s)
dctSchedulerMessage(4, 'Calling clear(''functions''), and closing simulink models');
clear('functions'); %#ok<CLFUNC>
try
    % If Simulink is loaded then ask it to clear it's systems - NOTE we should 
    % NOT load Simulink if it is not already loaded
    if iIsSimulinkLoaded 
        bdclose('all');
    end
catch e
    % Don't worry if this errors, it was just a best effort to not have any
    % simulink models in memory. 
    dctSchedulerMessage(4, 'Failed to close simulink models. This is not fatal. Original error:', e);
end
end

% -------------------------------------------------------------------------
% Internal function to check if an error returned from the evaluator
% actually contains an error.
% -------------------------------------------------------------------------
function OK = iThrewError(errOutput)
% Evaluator returns an MException.empty if no error occurred.
OK = ~isempty(errOutput);
end

% -------------------------------------------------------------------------
% Internal function to create a new error with the original error as a cause
% -------------------------------------------------------------------------
function newErr = iUpdateError(originalErr, newIdentifier, varargin)
if ~isa(originalErr, 'MException')
    dctSchedulerMessage(0, 'Error returned to dctEvaluateTask was not an MException');
    originalErr = MException(originalErr.identifier, '%s', originalErr.message);
end
% Remove PCT stack from the error
causingErr = iBuildParallelException(originalErr);
% Now wrap this in a new exception with the new ID
newErr = ParallelException.hBuildLikeMException(message(newIdentifier, varargin{:}));
newErr = newErr.addCause(causingErr);
end

% -------------------------------------------------------------------------
% Internal function to check if the simulink library is loaded - this is
% done via version rather than a license check because there are some edge
% case situations what a license check misses
% -------------------------------------------------------------------------
function OK = iIsSimulinkLoaded()
moduleOutput = evalc('version -modules');
regexpMatlabroot = strrep(matlabroot, '\', '\\');
OK = ~isempty(regexp(moduleOutput , [regexpMatlabroot '[^\n]*libmwsimulink'], 'once'));
end

% -------------------------------------------------------------------------
% Internal function to create a new error that has no PCT stack
% -------------------------------------------------------------------------
function parErr = iBuildParallelException(err)
if isempty(err)
    parErr = ParallelException.empty();
else
    % Remove any PCT stack from the error that was actually thrown.  Errors will either
    % come from setup code that is in this file or from actual task evaluation.
    stackFilesToIgnore = {mfilename, 'evaluateWithNoErrors', 'executeScript', 'executeFunction'};
    parErr = ParallelException.hBuildFromRemoteException(err, stackFilesToIgnore);
end
end

% -------------------------------------------------------------------------
% Internal function to get warnings from the WarningsCollector
% -------------------------------------------------------------------------
function warnings = iGetWarnings(runprop)
    wc = runprop.WarningsCollector;
    % If the number of warnings is limited by the buffer size we add an
    % additional warning at the end explaining that further warnings were
    % discarded.
    if numel(wc.Warnings) == wc.MaxBufferSize
        wc.MaxBufferSize = wc.MaxBufferSize + 1;
        warning(message('parallel:task:WarningLimitReached'));
    end
    warnings = wc.Warnings;
end

% -------------------------------------------------------------------------
% Internal function to set maximum number of warnings to store
% -------------------------------------------------------------------------
function iSetWarningsCollectorBufferSize(runprop)
    try
        bufferSize = getenv('MDCS_WARNINGS_BUFFER_SIZE');
        if ~isempty(bufferSize)
            value = str2double(bufferSize);
            if ~isnan(value)
                dctSchedulerMessage(4, 'Setting warnings buffer size to %d', value);
                runprop.WarningsCollector.MaxBufferSize = value;
            else
                dctSchedulerMessage(4, ...
                    'Cannot set warnings buffer size to non-numeric MDCS_WARNINGS_BUFFER_SIZE: %s', bufferSize);
            end
        end
    catch err
        dctSchedulerMessage(4, 'Failed to set warnings buffer size. This is not fatal. Original error:', err);
    end
end

%--------------------------------------------------------------------------
% Internal function to set environment variables
%--------------------------------------------------------------------------
function iSetEnvironmentVariables(environmentVariablesMap)
% Set map of environment variable name-value pairs
dctSchedulerMessage(4, 'About to set environment variables: %s', ...
    strjoin(cellfun(@(n, v) [n ' : ' v], environmentVariablesMap.keys, ...
    environmentVariablesMap.values, 'UniformOutput', false), '\n'));
cellfun(@setenv, environmentVariablesMap.keys, environmentVariablesMap.values);
end

%--------------------------------------------------------------------------
% Internal function to clear environment variables
%--------------------------------------------------------------------------
function iUnSetEnvironmentVariables(names)
% Set map of environment variable name-value pairs
dctSchedulerMessage(4, 'About to unset environment variables: %s', ...
    strjoin(cellfun(@(n) n, names, 'UniformOutput', false), '\n'));
cellfun(@pctunsetenv, names);
end
