void ADUC_Workflow_WorkCompletionCallback()

in src/adu_workflow/src/agent_workflow.c [867:1050]


void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_Result result, bool isAsync)
{
    // We own these objects, so no issue making them non-const.
    ADUC_MethodCall_Data* methodCallData = (ADUC_MethodCall_Data*)workCompletionToken;
    ADUC_WorkflowData* workflowData = methodCallData->WorkflowData;

    // NOLINTNEXTLINE(misc-redundant-expression)
    if (AducResultCodeIndicatesInProgress(result.ResultCode))
    {
        Log_Error("WorkComplete received InProgress result code - should not happen!");
        goto done;
    }

    // Need to avoid deadlock b/c main thread typically takes lock higher in the callstack above
    // TransistionWorkflow and processing DeploymentInProgress state is synchronous
    if (isAsync)
    {
        s_workflow_lock();
    }

    ADUCITF_WorkflowStep currentWorkflowStep = workflow_get_current_workflowstep(workflowData->WorkflowHandle);

    const ADUC_WorkflowHandlerMapEntry* entry = GetWorkflowHandlerMapEntryForAction(currentWorkflowStep);
    if (entry == NULL)
    {
        Log_Error("Invalid UpdateAction %u -- ignoring", currentWorkflowStep);
        goto done;
    }

    if (ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_Cancel)
    {
        Log_Error("workflow data current action should not be Cancel.");
        goto done;
    }

    Log_Info(
        "Action '%s' complete. Result: %d (%s), %d (0x%x)",
        ADUCITF_WorkflowStepToString(entry->WorkflowStep),
        result.ResultCode,
        result.ResultCode == 0 ? "failed" : "succeeded",
        result.ExtendedResultCode,
        result.ExtendedResultCode);

    entry->OperationCompleteFunc(methodCallData, result);

    if (IsAducResultCodeSuccess(result.ResultCode))
    {
        // Operation succeeded -- go to next state.

        const ADUCITF_State nextUpdateStateOnSuccess = entry->NextStateOnSuccess;

        Log_Info(
            "WorkCompletionCallback: %s succeeded. Going to state %s",
            ADUCITF_WorkflowStepToString(entry->WorkflowStep),
            ADUCITF_StateToString(nextUpdateStateOnSuccess));

        ADUC_Workflow_SetUpdateState(workflowData, nextUpdateStateOnSuccess);

        // Transitioning to idle (or failed) state frees and nulls-out the WorkflowHandle as a side-effect of
        // setting the update state.
        if (ADUC_WorkflowData_GetLastReportedState(workflowData) != ADUCITF_State_Idle)
        {
            // Operation is now complete. Clear both inprogress and cancel requested.
            workflow_clear_inprogress_and_cancelrequested(workflowData->WorkflowHandle);

            //
            // We are now ready to transition to the next step of the workflow.
            //
            ADUC_Workflow_AutoTransitionWorkflow(workflowData, true);
            goto done;
        }
    }
    else
    {
        // Operation (e.g. Download) failed or was cancelled - both are considered AducResult failure codes.

        if (workflow_get_operation_cancel_requested(workflowData->WorkflowHandle))
        {
            ADUC_WorkflowCancellationType cancellationType =
                workflow_get_cancellation_type(workflowData->WorkflowHandle);
            const char* cancellationTypeStr = ADUC_Workflow_CancellationTypeToString(cancellationType);

            Log_Warn("Handling cancel completion, cancellation type '%s'.", cancellationTypeStr);

            if (cancellationType == ADUC_WorkflowCancellationType_Replacement
                || cancellationType == ADUC_WorkflowCancellationType_Retry
                || cancellationType == ADUC_WorkflowCancellationType_ComponentChanged)
            {
                Log_Info("Starting process of deployment for '%s'", cancellationTypeStr);

                // Note: Must NOT call linux platform layer Idle method to reset cancellation request to false in the
                // platform layer because that would destroy and NULL out the WorkflowHandle in the workflowData.

                if (cancellationType == ADUC_WorkflowCancellationType_Replacement)
                {
                    // Cleanup the download sandbox for the current workflowId
                    // since it will not be transitioning to Idle state (where
                    // sandbox cleanup is normally done)

                    char* workflowId = ADUC_WorkflowData_GetWorkflowId(workflowData); // see workflow_free_string below
                    char* workFolder = ADUC_WorkflowData_GetWorkFolder(workflowData); // see workflow_free_string below

                    if (workflowId != NULL && workFolder != NULL)
                    {
                        Log_Info("Cleanup sandbox before replacement workflow");

                        const ADUC_UpdateActionCallbacks* updateActionCallbacks =
                            &(workflowData->UpdateActionCallbacks);

                        updateActionCallbacks->SandboxDestroyCallback(
                            updateActionCallbacks->PlatformLayerHandle, workflowId, workFolder);
                    }

                    workflow_free_string(workflowId);
                    workflow_free_string(workFolder);

                    // Reset workflow state to process deployment and transfer
                    // the deferred workflow to current.
                    workflow_update_for_replacement(workflowData->WorkflowHandle);

                }
                else
                {
                    // it's a retry. Reset workflow state to reprocess deployment.
                    workflow_update_for_retry(workflowData->WorkflowHandle);
                }

                ADUC_WorkflowData_SetLastReportedState(ADUCITF_State_Idle, workflowData);

                // ProcessDeployment's OperationFunc called by TransitionWorkflow is synchronous so it kicks off the
                // download worker thread after reporting DeploymentInProgress ACK for the replacement/retry, so we
                // return instead of goto done to avoid the redundant ADUC_Workflow_AutoTransitionWorkflow call.
                ADUC_Workflow_TransitionWorkflow(workflowData);

                goto done;
            }

            if (cancellationType != ADUC_WorkflowCancellationType_Normal)
            {
                Log_Error("Invalid cancellation Type '%s' when cancel requested.", cancellationTypeStr);
                goto done;
            }

            // Operation cancelled.
            //
            // We are now at the completion of the operation that was cancelled via a Cancel update action
            // and will just return to Idle state.
            //
            // Ignore the result of the operation, which most likely is cancelled, e.g. ADUC_Result_Failure_Cancelled.
            Log_Warn("Operation cancelled - returning to Idle state");

            result.ResultCode = ADUC_Result_Failure_Cancelled;
            result.ExtendedResultCode = 0;
            ADUC_Workflow_SetUpdateStateWithResult(workflowData, ADUCITF_State_Idle, result);
        }
        else
        {
            // Operation failed.

            const ADUCITF_State nextUpdateStateOnFailure = entry->NextStateOnFailure;

            Log_Info(
                "WorkCompletionCallback: %s failed. Going to state %s",
                ADUCITF_WorkflowStepToString(entry->WorkflowStep),
                ADUCITF_StateToString(nextUpdateStateOnFailure));

            // Reset so that a Retry/Replacement avoids cancel and instead properly starts processing.
            workflow_set_operation_in_progress(workflowData->WorkflowHandle, false);

            ADUC_Workflow_SetUpdateState(workflowData, nextUpdateStateOnFailure);

            ADUC_Workflow_AutoTransitionWorkflow(workflowData, false);
        }
    }

done:
    // lifetime of methodCallData now ends as the operation work has completed.
    free(methodCallData);

    if (isAsync)
    {
        s_workflow_unlock();
    }
}