using System; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using MonoDevelop.Core; using MonoDevelop.Ide; using MonoDevelop.Projects; namespace MonoDevelop.ConnectedServices { /// /// Base class for implementing IConnectedService. It stores state in a .json file that is created in a sub-folder of the project /// public abstract class ConnectedService : IConnectedService { /// /// Empty array of IConnectedService /// public static readonly IConnectedService[] Empty = new IConnectedService[0]; string solutionPadDisplayName; Status status = (Status)(-1); /// /// Initializes a new instance of the class. /// protected ConnectedService (DotNetProject project) { this.Project = project; this.Dependencies = ConnectedServiceDependency.Empty; this.Sections = ConfigurationSection.Empty; this.DependenciesSection = new DependenciesSection (this); } /// /// Gets the Id of the service /// public string Id { get; protected set; } /// /// Gets the display name of the service to show to the user in the solution pad /// public string DisplayName { get; protected set; } /// /// Gets the display name of the service to show to the user in the solution pad /// public string SolutionPadDisplayName { get { if (this.solutionPadDisplayName != null) return solutionPadDisplayName; return this.DisplayName; } set { solutionPadDisplayName = value; } } /// /// Gets the description of the service to display to the user in the services gallery. /// public string Description { get; protected set; } /// /// Gets the description of the service to display to the user in the service details tab. /// public string DetailsDescription { get; protected set; } /// /// Gets a description of the supported platforms. This is largely just informational as the service provider decides /// whether a project is supported or not. /// public string SupportedPlatforms { get; protected set; } /// /// Gets the project that this service instance is attached to /// public DotNetProject Project { get; private set; } /// /// Gets the icon to display in the services gallery. /// public Xwt.Drawing.Image GalleryIcon { get; protected set; } /// /// Gets the current status of the service. /// public Status Status { get { if ((int)status == -1) status = this.GetIsAddedToProject() ? Status.Added : Status.NotAdded; return status; } } /// /// Gets the dependencies that will be added to the project /// public ImmutableArray Dependencies { get; protected set; } /// /// Gets the dependencies section to be displayed before the configuration section /// public IConfigurationSection DependenciesSection { get; protected set; } /// /// Gets a value indicating whether ALL the depenencies are installed. /// public bool AreDependenciesInstalled { get { return this.Dependencies.All (x => x.Status == Status.Added); } } /// /// Gets the array of sections to be displayed to the user after the dependencies section. /// public ImmutableArray Sections { get; protected set; } /// /// Occurs when the status of the service has changed. /// public event EventHandler StatusChanged; /// /// Adds the service to the project /// public async Task AddToProject () { if (this.GetIsAddedToProject()) { LoggingService.LogWarning ("Skipping adding of the service, it has already been added"); return true; } this.ChangeStatus (Status.Adding); try { await this.AddDependencies (CancellationToken.None).ConfigureAwait (false); await this.OnAddToProject ().ConfigureAwait (false); await this.StoreAddedState ().ConfigureAwait (false); this.ChangeStatus (this.GetIsAddedToProject() ? Status.Added : Status.NotAdded); return true; } catch (Exception ex) { LoggingService.LogError ("An error occurred while adding the service to the project", ex); this.ChangeStatus (Status.NotAdded, ex); return false; } } /// /// Removes the service from the project /// public async Task RemoveFromProject () { if (!this.GetIsAddedToProject()) { LoggingService.LogWarning ("Skipping removing of the service, it is not added to the project"); return true; } this.ChangeStatus (Status.Removing); // try to remove the dependencies first, these may sometimes fail if one of the packages is or has dependencies // that other packages are dependent upon. A common one might be Json.Net for instance // we need to allow for this and continue on afterwards. var dependenciesFailed = false; try { await this.RemoveDependencies (CancellationToken.None).ConfigureAwait (false); } catch (Exception) { LoggingService.LogError ("An error occurred while removing the service dependencies from the project"); dependenciesFailed = true; } // right, now remove the service try { await this.OnRemoveFromProject ().ConfigureAwait (false); await this.RemoveAddedState ().ConfigureAwait (false); this.ChangeStatus (Status.NotAdded); if (dependenciesFailed) { // TODO: notify the user about the dependencies that failed, we might already have this from the package console and that might be enough for now } return true; } catch (Exception ex) { LoggingService.LogError ("An error occurred while removing the service from the project", ex); this.ChangeStatus (Status.Added, ex); return false; } } /// /// Determines if the service has been added to the project. /// protected virtual bool GetIsAddedToProject() { return false; } /// /// Performs the logic of removing the service from the project. This is called after the dependencies have been removed. /// protected virtual Task OnRemoveFromProject() { return Task.FromResult (true); } /// /// Performs the logic of adding the service to the project. This is called after the dependencies have been added. /// protected virtual Task OnAddToProject () { return Task.FromResult (true); } /// /// Adds the dependencies to the project /// protected virtual async Task AddDependencies(CancellationToken token) { return await DependenciesSection.AddToProject (token); } /// /// Removes the dependencies from the project /// protected virtual async Task RemoveDependencies (CancellationToken token) { // ask all the dependencies to add themselves to the project // we'll do them one at a time in case there are interdependencies between them try { // we are going to short circuit package dependencies though and uninstall them in one go var packages = this.Dependencies.Reverse ().OfType ().Cast ().ToList (); foreach (var dependency in this.Dependencies.Reverse ()) { if (packages.Contains (dependency)) { continue; } await dependency.RemoveFromProject (token).ConfigureAwait (false); } await this.Project.RemovePackageDependencies (packages).ConfigureAwait (false); } catch (Exception ex) { LoggingService.LogError ("Could not remove dependency", ex); throw; } } /// /// Stores some state that the service has been added to the project. /// protected virtual Task StoreAddedState() { return IdeApp.ProjectOperations.SaveAsync (this.Project); } /// /// Stores some state that the service has been added to the project. /// protected virtual Task RemoveAddedState () { return IdeApp.ProjectOperations.SaveAsync (this.Project); } /// /// Changes the status of the service and notifies subscribers /// void ChangeStatus(Status newStatus, Exception error = null) { var oldStatus = this.Status; this.status = newStatus; this.NotifyStatusChanged (newStatus, oldStatus, error); } /// /// Notifies subscribers that the service status has changed /// void NotifyStatusChanged(Status newStatus, Status oldStatus, Exception error = null) { var handler = this.StatusChanged; if (handler != null) { handler (this, new StatusChangedEventArgs (newStatus, oldStatus, error)); } } } }