Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Interfaces of AppModel accessible to Apps so apps can interact with it #931

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 7 additions & 19 deletions src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public async Task TestGetApplicationsLocalWithDisabled()
Assert.Null((MyAppLocalApp?) application.ApplicationContext?.Instance);

// set state to enabled
await application.SetStateAsync(ApplicationState.Enabled).ConfigureAwait(false);
await application.EnableAsync().ConfigureAwait(false);
application.State.Should().Be(ApplicationState.Running);
Assert.NotNull((MyAppLocalApp?) application.ApplicationContext?.Instance);

Expand Down Expand Up @@ -118,7 +118,7 @@ public async Task TestGetApplicationsLocalWithEnabled()
Assert.NotNull((MyAppLocalAppWithDispose?) application.ApplicationContext?.Instance);

// set state to enabled
await application.SetStateAsync(ApplicationState.Disabled).ConfigureAwait(false);
await application.DisableAsync().ConfigureAwait(false);
application.State.Should().Be(ApplicationState.Disabled);
Assert.Null((MyAppLocalAppWithDispose?) application.ApplicationContext?.Instance);
}
Expand All @@ -135,21 +135,6 @@ public async Task TestGetApplicationsWithIdSet()
appContext.Should().NotBeEmpty();
}

[Fact]
public async Task TestSetStateToRunningShouldThrowException()
{
// ARRANGE
var provider = Mock.Of<IServiceProvider>();
var logger = Mock.Of<ILogger<Application>>();
var factory = Mock.Of<IAppFactory>();

// ACT
var app = new Application(provider, logger, factory);

// CHECK
await Assert.ThrowsAsync<ArgumentException>(() => app.SetStateAsync(ApplicationState.Running));
}

[Fact]
public async Task TestGetApplicationsShouldReturnNonErrorOnes()
{
Expand Down Expand Up @@ -181,7 +166,7 @@ public async Task TestGetApplicationsShouldReturnNonErrorOnes()

// CHECK
loadApps.First(n => n.Id == "LocalAppsWithErrors.MyAppLocalAppWithError")
.State.Should().Be(ApplicationState.Error);
.Enabled.Should().BeTrue();

// Verify that the error is logged
loggerMock.Verify(
Expand Down Expand Up @@ -222,10 +207,13 @@ public async Task TestGetApplicationsLocalWithAsyncDisposable()
// check the application instance is init ok
var application = (Application) loadApps.First(n => n.Id == "LocalApps.MyAppLocalAppWithAsyncDispose");
var app = (MyAppLocalAppWithAsyncDispose?) application.ApplicationContext?.Instance;
application.State.Should().Be(ApplicationState.Running);
application.IsRunning.Should().BeTrue();
application.Enabled.Should().BeTrue();
await application.DisposeAsync().ConfigureAwait(false);
app!.AsyncDisposeIsCalled.Should().BeTrue();
app.DisposeIsCalled.Should().BeFalse();
application.IsRunning.Should().BeFalse();
application.Enabled.Should().BeTrue();
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ private static IServiceCollection AddAppModelIfNotExist(this IServiceCollection
services
.AddSingleton<AppModelImpl>()
.AddSingleton<IAppModel>(s => s.GetRequiredService<AppModelImpl>())
.AddTransient<AppModelContext>()
.AddTransient<IAppModelContext>(s => s.GetRequiredService<AppModelContext>())

// IAppModelContext is resolved via AppModelImpl which it itself a Singleton, so it will return the current AppModelContext
.AddTransient<IAppModelContext>(s => s.GetRequiredService<AppModelImpl>().CurrentAppModelContext ?? throw new InvalidOperationException("No AppModelContext is currently loaded"))
.AddTransient<FocusFilter>()
.AddScopedConfigurationBinder()
.AddScopedAppServices()
Expand Down
20 changes: 15 additions & 5 deletions src/AppModel/NetDaemon.AppModel/Common/IApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@ public interface IApplication : IAsyncDisposable
string? Id { get; }

/// <summary>
/// Current state of the application
/// Indicates if this application is currently Enabled
/// </summary>
ApplicationState State { get; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not want to expose the state as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not sure about what states we should have exactly. I was also playing with two booleans, Enabled and IsRunning. But not sure yet. For the state manager actually only enabled and disabled seem to be relevant.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aight, seem like somthing we could add in another PR since this has not been public yet

public bool Enabled { get; }

/// <summary>
/// Sets state for application
/// Enables the App and loads if possible
/// </summary>
/// <param name="state">The state to set</param>
Task SetStateAsync(ApplicationState state);
public Task EnableAsync();

/// <summary>
/// Disable the app and unload (Dispose) it if it is running
/// </summary>
public Task DisableAsync();

/// <summary>
/// The currently running instance of the app (if any)
/// </summary>
public object? Instance { get; }

}
8 changes: 6 additions & 2 deletions src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ public AppModelImpl(IServiceProvider provider)
_provider = provider;
}

public IAppModelContext? CurrentAppModelContext { get; private set; }

public async Task<IAppModelContext> LoadNewApplicationContext(CancellationToken cancellationToken)
{
// Create a new AppModelContext
var appModelContext = _provider.GetRequiredService<IAppModelContext>();
var appModelContext = ActivatorUtilities.CreateInstance<AppModelContext>(_provider);
await appModelContext.InitializeAsync(cancellationToken);

// Assign to CurrentAppModelContext so it can be resolved via DI
CurrentAppModelContext = appModelContext;
return appModelContext;
}
}
29 changes: 12 additions & 17 deletions src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,30 @@ namespace NetDaemon.AppModel.Internal;

internal class AppModelContext : IAppModelContext
{
private readonly List<Application> _applications = new();
private readonly List<Application> _applications;

private readonly IEnumerable<IAppFactoryProvider> _appFactoryProviders;
private readonly IServiceProvider _provider;
private readonly FocusFilter _focusFilter;
private ILogger<AppModelContext> _logger;
private readonly ILogger<AppModelContext> _logger;
private bool _isDisposed;

public AppModelContext(IEnumerable<IAppFactoryProvider> appFactoryProviders, IServiceProvider provider, FocusFilter focusFilter, ILogger<AppModelContext> logger)
{
_appFactoryProviders = appFactoryProviders;
_provider = provider;
_focusFilter = focusFilter;
_logger = logger;

var factories = appFactoryProviders.SelectMany(p => p.GetAppFactories()).ToList();

var filteredFactories = focusFilter.FilterFocusApps(factories);

_applications = filteredFactories.Select(factory => ActivatorUtilities.CreateInstance<Application>(provider, factory))
.ToList();
}

public IReadOnlyCollection<IApplication> Applications => _applications;
public IReadOnlyCollection<IApplication> Applications => _applications.AsReadOnly();

public async Task InitializeAsync(CancellationToken cancellationToken)
{
var factories = _appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList();

var filteredFactories = _focusFilter.FilterFocusApps(factories);

foreach (var factory in filteredFactories)
foreach (var application in _applications)
{
var app = ActivatorUtilities.CreateInstance<Application>(_provider, factory);
await app.InitializeAsync().ConfigureAwait(false);
_applications.Add(app);
await application.InitializeAsync();
}

_logger.LogInformation("Finished loading applications: {state}",
Expand Down
117 changes: 56 additions & 61 deletions src/AppModel/NetDaemon.AppModel/Internal/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,72 +26,48 @@ public Application(IServiceProvider provider, ILogger<Application> logger, IAppF
// Used in tests
internal ApplicationContext? ApplicationContext { get; private set; }

public string Id => _appFactory.Id;

public ApplicationState State
{
get
{
if (_isErrorState)
return ApplicationState.Error;
public bool Enabled { get; private set; }

return ApplicationContext is null ? ApplicationState.Disabled : ApplicationState.Running;
}
}
public bool IsRunning { get; private set; }

public async Task SetStateAsync(ApplicationState state)
{
switch (state)
{
case ApplicationState.Enabled:
await LoadApplication(state);
break;
case ApplicationState.Disabled:
await UnloadApplication(state);
break;
case ApplicationState.Error:
_isErrorState = true;
await SaveStateIfStateManagerExistAsync(state);
break;
case ApplicationState.Running:
throw new ArgumentException("Running state can only be set internally", nameof(state));
}
}
// TODO: see if we can remove this and refactor the possible states
public ApplicationState State =>
_isErrorState ? ApplicationState.Error :
IsRunning ? ApplicationState.Running :
Enabled ? ApplicationState.Enabled :
ApplicationState.Disabled;

public string Id => _appFactory.Id;

public async ValueTask DisposeAsync()
{
if (ApplicationContext is not null) await ApplicationContext.DisposeAsync().ConfigureAwait(false);
}
public object? Instance => ApplicationContext?.Instance;

private async Task UnloadApplication(ApplicationState state)

public async Task InitializeAsync()
{
if (ApplicationContext is not null)
if (await ShouldInstanceApplicationAsync(Id).ConfigureAwait(false))
{
await ApplicationContext.DisposeAsync().ConfigureAwait(false);
ApplicationContext = null;
_logger.LogInformation("Successfully unloaded app {Id}", Id);
await SaveStateIfStateManagerExistAsync(state).ConfigureAwait(false);
Enabled = true;
await LoadAsync().ConfigureAwait(false);
}
}

private async Task LoadApplication(ApplicationState state)
private async Task<bool> ShouldInstanceApplicationAsync(string id)
{
// first we save state "Enabled", this will also
// end up being state "Running" if instancing is successful
// or "Error" if instancing the app fails
await SaveStateIfStateManagerExistAsync(state);
if (ApplicationContext is null)
await InstanceApplication().ConfigureAwait(false);
if (_appStateManager is null)
return true;
return await _appStateManager.GetStateAsync(id).ConfigureAwait(false) == ApplicationState.Enabled;
}

public async Task InitializeAsync()
public async Task EnableAsync()
{
if (await ShouldInstanceApplicationAsync(Id).ConfigureAwait(false))
await InstanceApplication().ConfigureAwait(false);
await SaveStateIfStateManagerExistAsync(ApplicationState.Enabled);
await LoadAsync();
}

private async Task InstanceApplication()
public async Task LoadAsync()
{
if (ApplicationContext is not null) return;

try
{
ApplicationContext = new ApplicationContext(_provider, _appFactory);
Expand All @@ -106,28 +82,47 @@ private async Task InstanceApplication()
Id);

await initAsyncTask; // Continue to wait even if timeout is set so we do not miss errors
IsRunning = true;
_isErrorState = false;

await SaveStateIfStateManagerExistAsync(ApplicationState.Running);
_logger.LogInformation("Successfully loaded app {Id}", Id);
}
catch (Exception e)
{
_isErrorState = true;
_logger.LogError(e, "Error loading app {Id}", Id);
await SetStateAsync(ApplicationState.Error);
}
}

public async Task DisableAsync()
{
await UnloadAsync();
Enabled = false;

await SaveStateIfStateManagerExistAsync(ApplicationState.Disabled).ConfigureAwait(false);
}

private async Task SaveStateIfStateManagerExistAsync(ApplicationState appState)
public async Task UnloadAsync()
{
if (_appStateManager is not null)
await _appStateManager.SaveStateAsync(Id, appState).ConfigureAwait(false);
if (ApplicationContext is not null)
{
await ApplicationContext.DisposeAsync().ConfigureAwait(false);
ApplicationContext = null;
_logger.LogInformation("Successfully unloaded app {Id}", Id);
}

IsRunning = false;
}


public async ValueTask DisposeAsync()
{
await UnloadAsync();
}

private async Task<bool> ShouldInstanceApplicationAsync(string id)
private async Task SaveStateIfStateManagerExistAsync(ApplicationState appState)
{
if (_appStateManager is null)
return true;
return await _appStateManager.GetStateAsync(id).ConfigureAwait(false)
== ApplicationState.Enabled;
if (_appStateManager is not null)
await _appStateManager.SaveStateAsync(Id, appState).ConfigureAwait(false);
}
}
2 changes: 1 addition & 1 deletion src/AppModel/NetDaemon.AppModel/NetDaemon.AppModel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
<PackageReference Include="System.IO.Pipelines" Version="7.0.0" />
<PackageReference Include="Roslynator.Analyzers" Version="4.4.0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ await homeAssistantStateUpdater.InitializeAsync(haConnectionMock.Object, appMode
}.ToJsonElement()
});
// ASSERT
appMock.Verify(n => n.SetStateAsync(ApplicationState.Disabled), Times.Once);
appMock.Verify(n => n.DisableAsync(), Times.Once);
}

[Fact]
Expand Down Expand Up @@ -390,7 +390,7 @@ await homeAssistantStateUpdater.InitializeAsync(haConnectionMock.Object, appMode
});

// ASSERT
appMock.Verify(n => n.SetStateAsync(ApplicationState.Disabled), Times.Never);
appMock.Verify(n => n.DisableAsync(), Times.Never);
}

[Fact]
Expand Down Expand Up @@ -441,7 +441,7 @@ public async Task TestAppOneStateIsNullShouldNotCallSetStateAsync()
});

// ASSERT
appMock.Verify(n => n.SetStateAsync(ApplicationState.Disabled), Times.Never);
appMock.Verify(n => n.DisableAsync(), Times.Never);
}

private (Mock<IHomeAssistantConnection> connection, IServiceProvider serviceProvider) SetupProviderAndMocks()
Expand Down
16 changes: 9 additions & 7 deletions src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,22 @@ await _appStateRepository.RemoveNotUsedStatesAsync(appContext.Applications.Selec
if (changedEvent.NewState.State == changedEvent.OldState.State)
// We only care about changed state
return;

foreach (var app in appContext.Applications)
{
var entityId =
EntityMapperHelper.ToEntityIdFromApplicationId(app.Id ??
throw new InvalidOperationException(), _hostEnvironment.IsDevelopment());
if (entityId != changedEvent.NewState.EntityId) continue;

var appState = changedEvent.NewState?.State == "on"
? ApplicationState.Enabled
: ApplicationState.Disabled;

await app.SetStateAsync(
appState
);
if (changedEvent.NewState?.State == "on")
{
await app.EnableAsync().ConfigureAwait(false);
}
else
{
await app.DisableAsync().ConfigureAwait(false);
}
break;
}
}).Subscribe();
Expand Down
Loading