Skip to content
Merged
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
6 changes: 5 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
<!-- Suppress VSTHRD analyzers that are too strict for this project -->
<!-- These supressions should be addressed and removed. See https://github.com/jodavis/AdaptiveRemote/issues/41 -->
<!-- Suppress documentation warnings - we enable GenerateDocumentationFile only to get IDE0005 -->
<NoWarn>$(NoWarn);VSTHRD011;VSTHRD002;VSTHRD003;VSTHRD103;VSTHRD105;VSTHRD110;VSTHRD200;MSTEST0045;CS1591;CS1573;CS1584;CS1658</NoWarn>
<NoWarn>$(NoWarn);CS1591;CS1573;CS1584;CS1658</NoWarn>

<!-- This warning is about using a JoinableTaskFactory, which is intended to solve a specific
class of issues that this application does not have. -->
<NoWarn>$(NoWarn);VSTHRD012</NoWarn>
</PropertyGroup>
</Project>
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

<!-- Code Analysis -->
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.101" />
<PackageVersion Include="Microsoft.VisualStudio.Threading" Version="17.10.48" />
</ItemGroup>

<ItemGroup Label="Test-Only Packages">
Expand Down
1 change: 1 addition & 0 deletions src/AdaptiveRemote.App/AdaptiveRemote.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="I8Beef.TiVo" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.VisualStudio.Threading" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
Expand Down
4 changes: 2 additions & 2 deletions src/AdaptiveRemote.App/Components/BlazorAppScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ public BlazorAppScope(IApplicationScopeContainer scopeContainer, IServiceProvide
_logger = logger;
_scopeContainer = scopeContainer;

_ = PushSelfToScopeContainer();
_ = PushSelfToScopeContainerAsync();
}

private async Task PushSelfToScopeContainer()
private async Task PushSelfToScopeContainerAsync()
{
try
{
Expand Down
2 changes: 1 addition & 1 deletion src/AdaptiveRemote.App/Mvvm/MvvmObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class MvvmObject : INotifyPropertyChanging, INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
public event PropertyChangingEventHandler? PropertyChanging;

private Dictionary<object, object?> _values = new();
private readonly Dictionary<object, object?> _values = new();

public IDisposable Bind<PropertyType>(MvvmProperty<PropertyType> targetProperty, MvvmObject source, MvvmProperty<PropertyType> sourceProperty)
=> new Binding<PropertyType>(source, sourceProperty, this, targetProperty);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ protected override Command.ExecuteDelegate CreateHandler(IRCommand command)
{
byte[] data = Convert.FromBase64String(command.Data);

return cancellationToken => _connection!.SendData(data, cancellationToken);
return cancellationToken => _connection!.SendDataAsync(data, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task<bool> AuthenticateAsync(CancellationToken cancellationToken)
return true;
}

public async Task SendData(byte[] data, CancellationToken cancellationToken)
public async Task SendDataAsync(byte[] data, CancellationToken cancellationToken)
{
CommandPayload payload = new(0x2, data);
ResponsePacket response = await SendPacketAsync(SendDataCommandCode, payload, cancellationToken);
Expand Down
2 changes: 1 addition & 1 deletion src/AdaptiveRemote.App/Services/Broadlink/DeviceLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async Task<ScanResponsePacket> IDeviceLocator.FindDeviceAsync(CancellationToken
}
finally
{
stop.Cancel();
await stop.CancelAsync();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal interface IDeviceConnection
/// </summary>
/// <param name="data">IR signal data</param>
/// <returns>A Task that completes when the IR signal has been sent</returns>
Task SendData(byte[] data, CancellationToken cancellationToken);
Task SendDataAsync(byte[] data, CancellationToken cancellationToken);

internal interface Factory
{
Expand Down
6 changes: 2 additions & 4 deletions src/AdaptiveRemote.App/Services/CommandServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,15 @@ public virtual Task InitializeAsync(ILifecycleActivity activity, CancellationTok
return Task.CompletedTask;
}

public virtual Task CleanUpAsync(ILifecycleActivity activity, CancellationToken cancellationToken)
public virtual async Task CleanUpAsync(ILifecycleActivity activity, CancellationToken cancellationToken)
{
_stop.Cancel();
await _stop.CancelAsync();

foreach (Command command in _commands)
{
command.IsEnabled = false;
command.ExecuteAsync = CreateWasShutDownHandler(command);
}

return Task.CompletedTask;
}

private Command.ExecuteDelegate CreateWrappedHandler(CommandType command)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private static RespondContext RespondTo(this RespondContext context)
.StopUnless(CommandExists)
.StopUnless(SpeechIsHighConfidence, ifStopped: AskForConfirmation)
.StopUnless(CommandEnabled, ifStopped: RespondCommandDisabled)
.StopUnless(CommandHasExecuteAsync, ifStopped: RespondCommandDisabled)
.StopUnless(CommandHasExecuted, ifStopped: RespondCommandDisabled)
.Apply(AddCommands)
.Apply(SpeakDescriptionOfCommands);
}
Expand Down Expand Up @@ -302,7 +302,7 @@ private static bool CommandEnabled(RespondContext context)
=> (context.DecodedCommand?.IsEnabled == true)
.LogErrorIf(false, context.Logger, Message.ConversationController_CommandDisabled, context.DecodedCommand?.Name);

private static bool CommandHasExecuteAsync(RespondContext context)
private static bool CommandHasExecuted(RespondContext context)
=> (context.DecodedCommand?.ExecuteAsync is not null)
.LogErrorIf(false, context.Logger, Message.ConversationController_CommandMissingExecuteAction, context.DecodedCommand?.Name);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ internal class FakeSpeechRecognitionEngine : ISpeechRecognitionEngine
private readonly Dictionary<string, IGrammar> _grammars = new();

private event EventHandler<RecognizedSpeechEventArgs>? _recognized;
private TaskCompletionSource _pause = new();
private CancellationTokenSource _pause = new();

public FakeSpeechRecognitionEngine()
{
_ = RecognitionLoop();
_ = RecognitionLoopAsync();
}

event EventHandler<RecognizedSpeechEventArgs> ISpeechRecognitionEngine.SpeechRecognized
Expand All @@ -29,18 +29,18 @@ event EventHandler<RecognizedSpeechEventArgs> ISpeechRecognitionEngine.SpeechRej
void ISpeechRecognitionEngine.LoadGrammar(IGrammar grammar) => _grammars.Add(grammar.Name ?? string.Empty, grammar);
void ISpeechRecognitionEngine.UnloadGrammar(IGrammar grammar) => _grammars.Remove(grammar.Name ?? string.Empty);
void ISpeechRecognitionEngine.UnloadAllGrammars() => _grammars.Clear();
void ISpeechRecognitionEngine.RecognizeAsync() => _pause.TrySetResult();
void ISpeechRecognitionEngine.Recognize() => _pause.Cancel();
void ISpeechRecognitionEngine.RecognizeAsyncCancel() => _pause = new();
void ISpeechRecognitionEngine.SetConfidenceThreshold(int threshold) { }

private async Task RecognitionLoop()
private async Task RecognitionLoopAsync()
{
IEnumerator<FakeRecognitionResult> commands = CommandLoop();
int ticks = 0;

while (true)
{
await _pause.Task;
await _pause.Token.WaitForCancelledAsync();
await Task.Delay(1000);
ticks++;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public interface ISpeechRecognitionEngine
/// <summary>
/// Performs one or more asynchronous speech recognition operations.
/// </summary>
void RecognizeAsync();
void Recognize();

/// <summary>
/// Terminates asynchronous recognition without waiting for the current recognition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public interface ISpeechSynthesizer
/// <summary>
/// Generates speech output asynchronously from a string.
/// </summary>
void SpeakAsync(string phrase);
void Speak(string phrase);

/// <summary>
/// Cancels all queued, asynchronous, speech synthesis operations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private void UpdateListenState(int listenDelta = 0, int pauseDelta = 0, bool und
{
errorMessage = Message.ListeningController_RecognizeAsyncError;

_engine.RecognizeAsync();
_engine.Recognize();
_isListening = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async Task StartlisteningAsync()
{
_engine.SpeechRecognized += handler;

await stopToken.WaitForCancelled();
await stopToken.WaitForCancelledAsync();

_engine.SpeechRecognized -= handler;
}
Expand Down
45 changes: 32 additions & 13 deletions src/AdaptiveRemote.App/Services/Conversation/SpeechSynthesis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal class SpeechSynthesis : ISpeechSynthesis
private readonly IListeningController _listeningController;
private readonly ILogger<SpeechSynthesis> _logger;

private TaskCompletionSource _tcs = new();
private int _isSpeaking = 0;

public SpeechSynthesis(ISpeechSynthesizer synthesizer, IListeningController listeningController, IOptionsSnapshot<ConversationSettings> settings, ILogger<SpeechSynthesis> logger)
{
Expand All @@ -20,13 +20,8 @@ public SpeechSynthesis(ISpeechSynthesizer synthesizer, IListeningController list

SelectVoice(settings.Value.Voice);
SetSpeakingRate(settings.Value.SpeakingRate);

_synthesizer.SpeakCompleted += OnSpeakCompleted;
_tcs.SetResult();
}

private void OnSpeakCompleted(object? sender, EventArgs e) => _tcs.TrySetResult();

private void SelectVoice(string[] voiceNames)
{
foreach (string voiceName in voiceNames)
Expand All @@ -52,22 +47,46 @@ private void SetSpeakingRate(int speakingRate)

async Task ISpeechSynthesis.SayAsync(string phrase, CancellationToken cancellationToken)
{
if (!_tcs.Task.IsCompleted)
if (Interlocked.Exchange(ref _isSpeaking, 1) == 1)
{
_logger.LogAndThrowError(Message.SpeechSynthesis_AlreadySpeaking, phrase);
}

_tcs = new();

using (cancellationToken.Register(() => CancelSpeaking(_tcs, phrase)))
using (_listeningController.Pause())
{
_logger.LogInformation(Message.SpeechSynthesis_Saying, phrase);
_synthesizer.SpeakAsync(phrase);
await _tcs.Task;
try
{
_logger.LogInformation(Message.SpeechSynthesis_Saying, phrase);
await SpeakAndWaitAsync(phrase, cancellationToken);
}
finally
{
Interlocked.Exchange(ref _isSpeaking, 0);
}
}
}

private Task SpeakAndWaitAsync(string phrase, CancellationToken cancellationToken)
{
TaskCompletionSource tcs = new();

CancellationTokenRegistration registration = cancellationToken.Register(() => CancelSpeaking(tcs, phrase));

EventHandler onSpeakCompleted = null!; // This is set on the next line, so it won't be null
onSpeakCompleted = (sender, e) =>
{
_synthesizer.SpeakCompleted -= onSpeakCompleted;
registration.Dispose();
tcs.TrySetResult();
};

_synthesizer.SpeakCompleted += onSpeakCompleted;

_synthesizer.Speak(phrase);

return tcs.Task;
}

private void CancelSpeaking(TaskCompletionSource tcs, string phrase)
{
_logger.LogInformation(Message.SpeechSynthesis_CancelledSaying, phrase);
Expand Down
2 changes: 1 addition & 1 deletion src/AdaptiveRemote.App/Services/INetworking.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ internal interface INetworking
{
Task<IPHostEntry> GetDnsEntryAsync(string hostNameOrAddress, CancellationToken cancellationToken);

Task<IEnumerable<(IPAddress, IPAddress)>> GetOperationalNetworkInterfaceAddresses(CancellationToken cancellationToken);
Task<IEnumerable<(IPAddress, IPAddress)>> GetOperationalNetworkInterfaceAddressesAsync(CancellationToken cancellationToken);

Task<PingReply> SendPingAsync(IPAddress address, CancellationToken cancellationToken);
}
32 changes: 21 additions & 11 deletions src/AdaptiveRemote.App/Services/Lifecycle/ApplicationLifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,34 @@ public ApplicationLifecycle(IApplicationScopeProvider scopeProvider, ILifecycleV

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_ = _scopeProvider.InvokeInScopeAsync(InitializeLifecycle, stoppingToken);
try
{
await _scopeProvider.InvokeInScopeAsync(InitializeLifecycleAsync, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Do nothing, shutdown was requested
}
catch
{
// An error occurred, so stop all the services
_ = _scopeProvider.InvokeInScopeAsync(CleanUpLifecycleAsync, default);
}

await stoppingToken.WaitForCancelled();
await stoppingToken.WaitForCancelledAsync();

_logger.LogInformation(Message.ApplicationLifecycle_ShuttingDown);

await _scopeProvider.InvokeInScopeAsync(CleanUpLifecycle, default);
await _scopeProvider.InvokeInScopeAsync(CleanUpLifecycleAsync, default);
}

private async Task InitializeLifecycle(IServiceProvider provider, CancellationToken cancellationToken)
private async Task InitializeLifecycleAsync(IServiceProvider provider, CancellationToken cancellationToken)
{
ScopedLifecycleContainer? container = SafeGetContainer(provider);
_currentContainer = SafeGetContainer(provider);

if (container is not null)
if (_currentContainer is not null)
{
await container.InitializeAllAsync(cancellationToken);

_currentContainer = container;
await _currentContainer.InitializeAllAsync(cancellationToken);
}

ScopedLifecycleContainer? SafeGetContainer(IServiceProvider provider)
Expand All @@ -56,13 +66,13 @@ private async Task InitializeLifecycle(IServiceProvider provider, CancellationTo
}
}

private async Task CleanUpLifecycle(IServiceProvider provider, CancellationToken token)
private async Task CleanUpLifecycleAsync(IServiceProvider provider, CancellationToken token)
{
ScopedLifecycleContainer? scope = Interlocked.Exchange(ref _currentContainer, null);

if (scope != null)
{
await scope.CleanUpAllAsync(token);
await scope.CleanUpInitializedServicesAsync(token);
}
}
}
Loading
Loading