• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

net-daemon / netdaemon / 6656960231

26 Oct 2023 04:22PM UTC coverage: 80.727% (-0.2%) from 80.941%
6656960231

push

github

web-flow
Better handling of faulty unloading of appa (#961)

* handle unloading of faulty apps better and prohibit apps instancing if previous unloading fails

* refactor

* test for slow dispose

808 of 1148 branches covered (0.0%)

Branch coverage included in aggregate %.

26 of 26 new or added lines in 3 files covered. (100.0%)

2945 of 3501 relevant lines covered (84.12%)

51.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.54
/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs
1
using System.Reactive.Linq;
2
using NetDaemon.AppModel;
3
using NetDaemon.HassModel;
4

5
namespace NetDaemon.Runtime.Internal;
6

7
internal class NetDaemonRuntime : IRuntime
8
{
9
    private const string Version = "custom_compiled";
10
    private const int TimeoutInSeconds = 30;
11
    private readonly ICacheManager _cacheManager;
12

13
    private readonly HomeAssistantSettings _haSettings;
14
    private readonly IHomeAssistantRunner _homeAssistantRunner;
15
    private readonly IOptions<AppConfigurationLocationSetting> _locationSettings;
16

17
    private readonly ILogger<NetDaemonRuntime> _logger;
18
    private readonly IServiceProvider _serviceProvider;
19
    private IAppModelContext? _applicationModelContext;
20
    private CancellationToken? _stoppingToken;
21
    private CancellationTokenSource? _runnerCancelationSource;
22

23
    public bool IsConnected;
24

25
    public NetDaemonRuntime(
7✔
26
        IHomeAssistantRunner homeAssistantRunner,
7✔
27
        IOptions<HomeAssistantSettings> settings,
7✔
28
        IOptions<AppConfigurationLocationSetting> locationSettings,
7✔
29
        IServiceProvider serviceProvider,
7✔
30
        ILogger<NetDaemonRuntime> logger,
7✔
31
        ICacheManager cacheManager)
7✔
32
    {
33
        _haSettings = settings.Value;
7✔
34
        _homeAssistantRunner = homeAssistantRunner;
7✔
35
        _locationSettings = locationSettings;
7✔
36
        _serviceProvider = serviceProvider;
7✔
37
        _logger = logger;
7✔
38
        _cacheManager = cacheManager;
7✔
39
    }
7✔
40

41
    // These internals are used primarily for testing purposes
42
    internal IReadOnlyCollection<IApplication> ApplicationInstances =>
43
        _applicationModelContext?.Applications ?? Array.Empty<IApplication>();
1!
44

45
    private readonly TaskCompletionSource _startedAndConnected = new();
7✔
46

47
    private Task _runnerTask = Task.CompletedTask;
7✔
48

49
    public async Task StartAsync(CancellationToken stoppingToken)
50
    {
51
        _logger.LogInformation($"Starting NetDaemon runtime version {Version}.");
7✔
52

53
        _stoppingToken = stoppingToken;
7✔
54

55
        _homeAssistantRunner.OnConnect
7✔
56
            .Select(async c => await OnHomeAssistantClientConnected(c, stoppingToken).ConfigureAwait(false))
6✔
57
            .Subscribe();
7✔
58
        _homeAssistantRunner.OnDisconnect
7✔
59
            .Select(async s => await OnHomeAssistantClientDisconnected(s).ConfigureAwait(false))
1✔
60
            .Subscribe();
7✔
61
        try
62
        {
63
            _runnerCancelationSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
7✔
64

65
            _runnerTask = _homeAssistantRunner.RunAsync(
7✔
66
                _haSettings.Host,
7✔
67
                _haSettings.Port,
7✔
68
                _haSettings.Ssl,
7✔
69
                _haSettings.Token,
7✔
70
                _haSettings.WebsocketPath,
7✔
71
                TimeSpan.FromSeconds(TimeoutInSeconds),
7✔
72
                _runnerCancelationSource.Token);
7✔
73

74
            // Make sure we only return after the connection is made and initialization is ready
75
            await _startedAndConnected.Task;
7✔
76
        }
6✔
77
        catch (OperationCanceledException)
×
78
        {
79
            // Ignore and just stop
80
        }
×
81
    }
6✔
82

83
    private async Task OnHomeAssistantClientConnected(
84
        IHomeAssistantConnection haConnection,
85
        CancellationToken cancelToken)
86
    {
87
        _logger.LogInformation("Successfully connected to Home Assistant");
6✔
88

89
        if (_applicationModelContext is not null)
6!
90
        {
91
            // Something wrong with unloading and disposing apps on restart of HA, we need to prevent apps loading multiple times
92
            _logger.LogWarning("Applications were not successfully disposed during restart, skippin loading apps again");
×
93
            return;
×
94
        }
95

96
        IsConnected = true;
6✔
97

98
        await _cacheManager.InitializeAsync(cancelToken).ConfigureAwait(false);
6✔
99

100
        await LoadNewAppContextAsync(haConnection, cancelToken);
6✔
101

102
        // Now signal that StartAsync may return
103
        _startedAndConnected.SetResult();
6✔
104
    }
6✔
105

106
    private async Task LoadNewAppContextAsync(IHomeAssistantConnection haConnection, CancellationToken cancelToken)
107
    {
108
        var appModel = _serviceProvider.GetService<IAppModel>();
6✔
109
        if (appModel == null) return;
6!
110

111
        try
112
        {
113
            // this logging is a bit weird in this class
114
            if (!string.IsNullOrEmpty(_locationSettings.Value.ApplicationConfigurationFolder))
6✔
115
                _logger.LogDebug("Loading applications from folder {Path}",
2✔
116
                    Path.GetFullPath(_locationSettings.Value.ApplicationConfigurationFolder));
2✔
117
            else
118
                _logger.LogDebug("Loading applications with no configuration folder");
4✔
119

120
            _applicationModelContext = await appModel.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false);
6✔
121

122
            // Handle state change for apps if registered
123
            var appStateHandler = _serviceProvider.GetService<IHandleHomeAssistantAppStateUpdates>();
6✔
124
            if (appStateHandler == null) return;
12!
125

126
            await appStateHandler.InitializeAsync(haConnection, _applicationModelContext);
×
127
        }
×
128
        catch (Exception e)
×
129
        {
130
            _logger.LogError(e, "   Failed to initialize apps");
×
131
            throw;
×
132
        }
133
    }
6✔
134

135
    private async Task OnHomeAssistantClientDisconnected(DisconnectReason reason)
136
    {
137
        if (_stoppingToken?.IsCancellationRequested == true || reason == DisconnectReason.Client)
1!
138
        {
139
            _logger.LogInformation("HassClient disconnected cause of user stopping");
1✔
140
        }
141
        else
142
        {
143
            var reasonString = reason switch
×
144
            {
×
145
                DisconnectReason.Remote => "home assistant closed the connection",
×
146
                DisconnectReason.Error => "unknown error, set loglevel to debug to view details",
×
147
                DisconnectReason.Unauthorized => "token not authorized",
×
148
                DisconnectReason.NotReady => "home assistant not ready yet",
×
149
                _ => "unknown error"
×
150
            };
×
151
            _logger.LogInformation("Home Assistant disconnected due to {Reason}, connect retry in {TimeoutInSeconds} seconds",
×
152
                reasonString, TimeoutInSeconds);
×
153
        }
154

155
        try
156
        {
157
            await DisposeApplicationsAsync().ConfigureAwait(false);
1✔
158
        }
1✔
159
        catch (Exception e)
×
160
        {
161
            _logger.LogError(e, "Error disposing applications");
×
162
        }
×
163
        IsConnected = false;
1✔
164
    }
1✔
165

166
    private async Task DisposeApplicationsAsync()
167
    {
168
        if (_applicationModelContext is not null)
6✔
169
        {
170
            await _applicationModelContext.DisposeAsync();
3✔
171

172
            _applicationModelContext = null;
3✔
173
        }
174
    }
6✔
175

176
    private bool _isDisposed;
177
    public async ValueTask DisposeAsync()
178
    {
179
        if (_isDisposed) return;
9✔
180
        _isDisposed = true;
5✔
181

182
        await DisposeApplicationsAsync().ConfigureAwait(false);
5✔
183
        _runnerCancelationSource?.Cancel();
5!
184
        try
185
        {
186
            await _runnerTask.ConfigureAwait(false);
5✔
187
        }
2✔
188
        catch (OperationCanceledException) { }
6✔
189
    }
7✔
190
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc