Skip to content

Commit

Permalink
Console: Add --listdevices option
Browse files Browse the repository at this point in the history
  • Loading branch information
cyanfish committed Jan 11, 2024
1 parent 6817125 commit 90c5916
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 27 deletions.
5 changes: 5 additions & 0 deletions NAPS2.Internals/Threading/AsyncProducers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ namespace NAPS2.Util;

public static class AsyncProducers
{
public static async IAsyncEnumerable<T> Empty<T>()

Check warning on line 6 in NAPS2.Internals/Threading/AsyncProducers.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 6 in NAPS2.Internals/Threading/AsyncProducers.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 6 in NAPS2.Internals/Threading/AsyncProducers.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 6 in NAPS2.Internals/Threading/AsyncProducers.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 6 in NAPS2.Internals/Threading/AsyncProducers.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 6 in NAPS2.Internals/Threading/AsyncProducers.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 6 in NAPS2.Internals/Threading/AsyncProducers.cs

View workflow job for this annotation

GitHub Actions / build (macos-12)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 6 in NAPS2.Internals/Threading/AsyncProducers.cs

View workflow job for this annotation

GitHub Actions / build (macos-12)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 6 in NAPS2.Internals/Threading/AsyncProducers.cs

View workflow job for this annotation

GitHub Actions / build (macos-12)

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
yield break;
}

public static IAsyncEnumerable<T> RunProducer<T>(ItemProducer<T> producer) where T : class
{
return RunProducer(new AsyncItemProducer<T>(produce =>
Expand Down
19 changes: 19 additions & 0 deletions NAPS2.Lib.Tests/Automation/CommandLineIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,25 @@ await _automationHelper.RunCommand(
AssertRecoveryCleanedUp();
}

[Fact]
public async Task ListDevices()
{
var (_, scanDriverFactoryMock) = CreateDriverMocks();

var outputWriter = new StringWriter();
await _automationHelper.WithContainerBuilder(container =>
{
container.RegisterInstance(new ConsoleOutput(outputWriter));
}).RunCommand(
new AutomatedScanningOptions
{
ListDevices = true
}, scanDriverFactoryMock);

Assert.Equal("test_name1\r\ntest_name2\r\n", outputWriter.ToString());
AssertRecoveryCleanedUp();
}

private static (IScanDriver, IScanDriverFactory) CreateDriverMocks()
{
var scanDriverMock = Substitute.For<IScanDriver>();
Expand Down
78 changes: 54 additions & 24 deletions NAPS2.Lib/Automation/AutomatedScanning.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ public async Task Execute()
}
}

if (_options.ListDevices)
{
await ListDevices();
return;
}

if (!PreCheckOverwriteFile())
{
return;
Expand Down Expand Up @@ -217,6 +223,21 @@ private async Task InstallComponents()
await downloadController.StartDownloadsAsync();
}

private async Task ListDevices()
{
if (!GetProfile(out var profile))
{
profile = new ScanProfile();
}
await SetProfileOverrides(profile);
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, _) => cts.Cancel();
await foreach (var device in _scanPerformer.GetDevices(profile, cts.Token))
{
_output.Writer.WriteLine(device.Name);
}
}

private void ReorderScannedImages()
{
var sep = _options.SplitPatchT ? SaveSeparator.PatchT
Expand Down Expand Up @@ -439,7 +460,7 @@ public bool ValidateOptions()
{
// Most validation is done by the CommandLineParser library, but some constraints that can't be represented by that API need to be checked here
if (_options.OutputPath == null && _options.EmailFileName == null && _options.Install == null &&
!_options.AutoSave)
!_options.AutoSave && !_options.ListDevices)
{
_errorOutput.DisplayError(ConsoleResources.OutputOrEmailRequired);
return false;
Expand All @@ -450,6 +471,15 @@ public bool ValidateOptions()
return false;
}

if (_options.Driver != null)
{
if (ScanPerformer.ParseDriver(_options.Driver) == Driver.Default)
{
_errorOutput.DisplayError(ConsoleResources.InvalidDriver);
return false;
}
}

if (_options.PageSize != null)
{
var pageSize = PageSize.Parse(_options.PageSize);
Expand Down Expand Up @@ -744,33 +774,11 @@ private bool GetProfile(out ScanProfile profile)

private async Task<bool> SetProfileOverrides(ScanProfile profile)
{
var driver = Driver.Default;
if (!string.IsNullOrEmpty(_options.Driver))
{
driver = ScanPerformer.ParseDriver(_options.Driver);
var driver = ScanPerformer.ParseDriver(_options.Driver);
profile.DriverName = driver.ToString().ToLowerInvariant();
}
if (!string.IsNullOrEmpty(_options.Device))
{
var scanController = new ScanController(_scanningContext, _scanBridgeFactory);
var cts = new CancellationTokenSource();
bool foundDevice = false;
await foreach (var device in scanController.GetDevices(driver, cts.Token))
{
if (device.Name.ContainsInvariantIgnoreCase(_options.Device!))
{
cts.Cancel();
profile.Device = new ScanProfileDevice(device.ID, device.Name);
foundDevice = true;
break;
}
}
if (!foundDevice)
{
_errorOutput.DisplayError(SdkResources.DeviceNotFound);
return false;
}
}
if (_options.Source != null)
{
profile.PaperSource = _options.Source.Value;
Expand Down Expand Up @@ -802,6 +810,28 @@ private async Task<bool> SetProfileOverrides(ScanProfile profile)
{
profile.RotateDegrees = _options.RotateDegrees.Value;
}

if (!string.IsNullOrEmpty(_options.Device))
{
var cts = new CancellationTokenSource();
bool foundDevice = false;
await foreach (var device in _scanPerformer.GetDevices(profile, cts.Token))
{
if (device.Name.ContainsInvariantIgnoreCase(_options.Device!))
{
cts.Cancel();
profile.Device = new ScanProfileDevice(device.ID, device.Name);
foundDevice = true;
break;
}
}
if (!foundDevice)
{
_errorOutput.DisplayError(SdkResources.DeviceNotFound);
return false;
}
}

return true;
}

Expand Down
3 changes: 3 additions & 0 deletions NAPS2.Lib/Automation/AutomatedScanningOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public class AutomatedScanningOptions
[Option("device", HelpText = "Scanning device name (can be inexact).")]
public string? Device { get; set; }

[Option("listdevices", HelpText = "Instead of scanning, list available devices.")]
public bool ListDevices { get; set; }

#endregion

#region Scan Options
Expand Down
9 changes: 9 additions & 0 deletions NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions NAPS2.Lib/Lang/ConsoleResources/ConsoleResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,7 @@ Use the "--importpassword" option.</value>
<data name="InvalidDpi" xml:space="preserve">
<value>The DPI option was not valid (must be 100/150/200/300/400/600/800/1200/2400/4800).</value>
</data>
<data name="InvalidDriver" xml:space="preserve">
<value>The driver option was not valid (must be wia/twain/escl/sane/apple).</value>
</data>
</root>
2 changes: 2 additions & 0 deletions NAPS2.Lib/Scan/IScanPerformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ public interface IScanPerformer
{
Task<ScanDevice?> PromptForDevice(ScanProfile scanProfile, IntPtr dialogParent = default);

IAsyncEnumerable<ScanDevice> GetDevices(ScanProfile scanProfile, CancellationToken cancelToken = default);

IAsyncEnumerable<ProcessedImage> PerformScan(ScanProfile scanProfile, ScanParams scanParams, IntPtr dialogParent = default, CancellationToken cancelToken = default);
}
27 changes: 24 additions & 3 deletions NAPS2.Lib/Scan/ScanPerformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ public ScanPerformer(IDevicePrompt devicePrompt, Naps2Config config, OperationPr
}
}

public IAsyncEnumerable<ScanDevice> GetDevices(ScanProfile scanProfile, CancellationToken cancelToken = default)
{
try
{
var options = BuildOptions(scanProfile, new ScanParams(), IntPtr.Zero);
var controller = CreateScanController(new ScanParams());
return controller.GetDevices(options, cancelToken);
}
catch (Exception error)
{
HandleError(error);
return AsyncProducers.Empty<ScanDevice>();
}
}

public async IAsyncEnumerable<ProcessedImage> PerformScan(ScanProfile scanProfile, ScanParams scanParams,
IntPtr dialogParent = default, [EnumeratorCancellation] CancellationToken cancelToken = default)
{
Expand All @@ -81,9 +96,7 @@ public async IAsyncEnumerable<ProcessedImage> PerformScan(ScanProfile scanProfil
yield break;
}

var localPostProcessor = new LocalPostProcessor(_scanningContext, ConfigureOcrController(scanParams));
var controller = new ScanController(_scanningContext, localPostProcessor, _scanOptionsValidator,
_scanBridgeFactory);
var controller = CreateScanController(scanParams);
var op = new ScanOperation(options);

controller.PageStart += (sender, args) => op.NextPage(args.PageNumber);
Expand Down Expand Up @@ -135,6 +148,14 @@ public async IAsyncEnumerable<ProcessedImage> PerformScan(ScanProfile scanProfil
}
}

private ScanController CreateScanController(ScanParams scanParams)
{
var localPostProcessor = new LocalPostProcessor(_scanningContext, ConfigureOcrController(scanParams));
var controller = new ScanController(_scanningContext, localPostProcessor, _scanOptionsValidator,
_scanBridgeFactory);
return controller;
}

private OcrController ConfigureOcrController(ScanParams scanParams)
{
OcrController ocrController = new OcrController(_scanningContext);
Expand Down

0 comments on commit 90c5916

Please sign in to comment.