306 lines
11 KiB
C#
306 lines
11 KiB
C#
using System.Reflection;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json.Linq;
|
|
using Umbraco.Cms.Core;
|
|
using Umbraco.Cms.Core.Install;
|
|
using Umbraco.Cms.Core.Install.Models;
|
|
using Umbraco.Cms.Core.Logging;
|
|
using Umbraco.Cms.Core.Security;
|
|
using Umbraco.Cms.Core.Services;
|
|
using Umbraco.Cms.Infrastructure.Install;
|
|
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
|
using Umbraco.Cms.Web.BackOffice.Security;
|
|
using Umbraco.Cms.Web.Common.ActionsResults;
|
|
using Umbraco.Cms.Web.Common.Attributes;
|
|
using Umbraco.Cms.Web.Common.Filters;
|
|
using Umbraco.Extensions;
|
|
|
|
namespace Umbraco.Cms.Web.BackOffice.Install;
|
|
|
|
[Obsolete("Will be replaced with a new API controller in the new backoffice api")]
|
|
[UmbracoApiController]
|
|
[AngularJsonOnlyConfiguration]
|
|
[InstallAuthorize]
|
|
[Area(Constants.Web.Mvc.InstallArea)]
|
|
public class InstallApiController : ControllerBase
|
|
{
|
|
private readonly DatabaseBuilder _databaseBuilder;
|
|
private readonly InstallStatusTracker _installStatusTracker;
|
|
private readonly InstallStepCollection _installSteps;
|
|
private readonly ILogger<InstallApiController> _logger;
|
|
private readonly IProfilingLogger _proflog;
|
|
private readonly IRuntime _runtime;
|
|
|
|
[Obsolete("Use the constructor without IBackOfficeUserManager & IBackOfficeSignInManager instead, scheduled for removal in v14")]
|
|
public InstallApiController(
|
|
DatabaseBuilder databaseBuilder,
|
|
IProfilingLogger proflog,
|
|
ILogger<InstallApiController> logger,
|
|
InstallHelper installHelper,
|
|
InstallStepCollection installSteps,
|
|
InstallStatusTracker installStatusTracker,
|
|
IRuntime runtime,
|
|
IBackOfficeUserManager backOfficeUserManager,
|
|
IBackOfficeSignInManager backOfficeSignInManager)
|
|
: this(databaseBuilder, proflog, logger, installHelper, installSteps, installStatusTracker, runtime)
|
|
{
|
|
}
|
|
|
|
[ActivatorUtilitiesConstructor]
|
|
public InstallApiController(
|
|
DatabaseBuilder databaseBuilder,
|
|
IProfilingLogger proflog,
|
|
ILogger<InstallApiController> logger,
|
|
InstallHelper installHelper,
|
|
InstallStepCollection installSteps,
|
|
InstallStatusTracker installStatusTracker,
|
|
IRuntime runtime)
|
|
{
|
|
_databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder));
|
|
_proflog = proflog ?? throw new ArgumentNullException(nameof(proflog));
|
|
_installSteps = installSteps;
|
|
_installStatusTracker = installStatusTracker;
|
|
_runtime = runtime;
|
|
InstallHelper = installHelper;
|
|
_logger = logger;
|
|
}
|
|
|
|
|
|
internal InstallHelper InstallHelper { get; }
|
|
|
|
public bool PostValidateDatabaseConnection(DatabaseModel databaseSettings)
|
|
{
|
|
if (_runtime.State.Level != RuntimeLevel.Install)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return _databaseBuilder.ConfigureDatabaseConnection(databaseSettings, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the install setup.
|
|
/// </summary>
|
|
public InstallSetup GetSetup()
|
|
{
|
|
// Only get the steps that are targeting the current install type
|
|
var setup = new InstallSetup
|
|
{
|
|
Steps = _installSteps.GetStepsForCurrentInstallType().ToList()
|
|
};
|
|
|
|
_installStatusTracker.Initialize(setup.InstallId, setup.Steps);
|
|
|
|
return setup;
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<ActionResult> CompleteInstall()
|
|
{
|
|
await _runtime.RestartAsync();
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
public async Task<ActionResult<InstallProgressResultModel>> PostPerformInstall(InstallInstructions installModel)
|
|
{
|
|
if (installModel == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(installModel));
|
|
}
|
|
|
|
// There won't be any statuses returned if the app pool has restarted so we need to re-read from file
|
|
InstallTrackingItem[] status = InstallStatusTracker.GetStatus().ToArray();
|
|
if (status.Any() == false)
|
|
{
|
|
status = _installStatusTracker.InitializeFromFile(installModel.InstallId).ToArray();
|
|
}
|
|
|
|
// Create a new queue of the non-finished ones
|
|
var queue = new Queue<InstallTrackingItem>(status.Where(x => x.IsComplete == false));
|
|
while (queue.Count > 0)
|
|
{
|
|
InstallTrackingItem item = queue.Dequeue();
|
|
InstallSetupStep step = _installSteps.GetAllSteps().Single(x => x.Name == item.Name);
|
|
|
|
// if this step has any instructions then extract them
|
|
var instruction = GetInstruction(installModel, item, step);
|
|
|
|
// if this step doesn't require execution then continue to the next one, this is just a fail-safe check.
|
|
if (StepRequiresExecution(step, instruction) == false)
|
|
{
|
|
// set this as complete and continue
|
|
_installStatusTracker.SetComplete(installModel.InstallId, item.Name);
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
InstallSetupResult? setupData = await ExecuteStepAsync(step, instruction);
|
|
|
|
// update the status
|
|
_installStatusTracker.SetComplete(installModel.InstallId, step.Name, setupData?.SavedStepData);
|
|
|
|
// determine's the next step in the queue and dequeue's any items that don't need to execute
|
|
var nextStep = IterateSteps(step, queue, installModel.InstallId, installModel);
|
|
bool processComplete = string.IsNullOrEmpty(nextStep) && InstallStatusTracker.GetStatus().All(x => x.IsComplete);
|
|
|
|
// check if there's a custom view to return for this step
|
|
if (setupData != null && setupData.View.IsNullOrWhiteSpace() == false)
|
|
{
|
|
return new InstallProgressResultModel(processComplete, step.Name, nextStep, setupData.View, setupData.ViewModel);
|
|
}
|
|
|
|
return new InstallProgressResultModel(processComplete, step.Name, nextStep);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "An error occurred during installation step {Step}", step.Name);
|
|
|
|
if (ex is TargetInvocationException && ex.InnerException != null)
|
|
{
|
|
ex = ex.InnerException;
|
|
}
|
|
|
|
if (ex is InstallException installException)
|
|
{
|
|
return new ValidationErrorResult(new
|
|
{
|
|
view = installException.View,
|
|
model = installException.ViewModel,
|
|
message = installException.Message
|
|
});
|
|
}
|
|
|
|
return new ValidationErrorResult(new { step = step.Name, view = "error", message = ex.Message });
|
|
}
|
|
}
|
|
|
|
_installStatusTracker.Reset();
|
|
return new InstallProgressResultModel(true, string.Empty, string.Empty);
|
|
}
|
|
|
|
private static object? GetInstruction(InstallInstructions installModel, InstallTrackingItem item, InstallSetupStep step)
|
|
{
|
|
object? instruction = null;
|
|
installModel.Instructions?.TryGetValue(item.Name, out instruction); // else null
|
|
|
|
if (instruction is JObject jObject)
|
|
{
|
|
instruction = jObject?.ToObject(step.StepType);
|
|
}
|
|
|
|
return instruction;
|
|
}
|
|
|
|
/// <summary>
|
|
/// We'll peek ahead and check if it's RequiresExecution is returning true. If it
|
|
/// is not, we'll dequeue that step and peek ahead again (recurse)
|
|
/// </summary>
|
|
/// <param name="current"></param>
|
|
/// <param name="queue"></param>
|
|
/// <param name="installId"></param>
|
|
/// <param name="installModel"></param>
|
|
/// <returns></returns>
|
|
private string IterateSteps(InstallSetupStep current, Queue<InstallTrackingItem> queue, Guid installId, InstallInstructions installModel)
|
|
{
|
|
while (queue.Count > 0)
|
|
{
|
|
InstallTrackingItem item = queue.Peek();
|
|
|
|
// if the current step restarts the app pool then we must simply return the next one in the queue,
|
|
// we cannot peek ahead as the next step might rely on the app restart and therefore RequiresExecution
|
|
// will rely on that too.
|
|
if (current.PerformsAppRestart)
|
|
{
|
|
return item.Name;
|
|
}
|
|
|
|
InstallSetupStep step = _installSteps.GetAllSteps().Single(x => x.Name == item.Name);
|
|
|
|
// if this step has any instructions then extract them
|
|
var instruction = GetInstruction(installModel, item, step);
|
|
|
|
// if the step requires execution then return its name
|
|
if (StepRequiresExecution(step, instruction))
|
|
{
|
|
return step.Name;
|
|
}
|
|
|
|
// no longer requires execution, could be due to a new config change during installation
|
|
// dequeue
|
|
queue.Dequeue();
|
|
|
|
// complete
|
|
_installStatusTracker.SetComplete(installId, step.Name);
|
|
|
|
// and continue
|
|
current = step;
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
// determines whether the step requires execution
|
|
internal bool StepRequiresExecution(InstallSetupStep step, object? instruction)
|
|
{
|
|
if (step == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(step));
|
|
}
|
|
|
|
Attempt<object?> modelAttempt = instruction.TryConvertTo(step.StepType);
|
|
if (!modelAttempt.Success)
|
|
{
|
|
throw new InvalidCastException($"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}");
|
|
}
|
|
|
|
var model = modelAttempt.Result;
|
|
Type genericStepType = typeof(InstallSetupStep<>);
|
|
Type[] typeArgs = { step.StepType };
|
|
Type typedStepType = genericStepType.MakeGenericType(typeArgs);
|
|
try
|
|
{
|
|
MethodInfo method = typedStepType.GetMethods().Single(x => x.Name == "RequiresExecution");
|
|
var result = (bool?)method.Invoke(step, new[] { model });
|
|
return result ?? false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Checking if step requires execution ({Step}) failed.", step.Name);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// executes the step
|
|
internal async Task<InstallSetupResult> ExecuteStepAsync(InstallSetupStep step, object? instruction)
|
|
{
|
|
using (!_proflog.IsEnabled(Core.Logging.LogLevel.Verbose) ? null : _proflog.TraceDuration<InstallApiController>($"Executing installation step: '{step.Name}'.", "Step completed"))
|
|
{
|
|
Attempt<object?> modelAttempt = instruction.TryConvertTo(step.StepType);
|
|
if (!modelAttempt.Success)
|
|
{
|
|
throw new InvalidCastException($"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}");
|
|
}
|
|
|
|
var model = modelAttempt.Result;
|
|
Type genericStepType = typeof(InstallSetupStep<>);
|
|
Type[] typeArgs = { step.StepType };
|
|
Type typedStepType = genericStepType.MakeGenericType(typeArgs);
|
|
try
|
|
{
|
|
MethodInfo method = typedStepType.GetMethods().Single(x => x.Name == "ExecuteAsync");
|
|
var task = (Task<InstallSetupResult>?)method.Invoke(step, new[] { model });
|
|
return await task!;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Installation step {Step} failed.", step.Name);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
}
|