diff --git a/starsky/starsky.foundation.video/GetDependencies/FfMpegDownload.cs b/starsky/starsky.foundation.video/GetDependencies/FfMpegDownload.cs index d798c590a..c3f2f2f25 100644 --- a/starsky/starsky.foundation.video/GetDependencies/FfMpegDownload.cs +++ b/starsky/starsky.foundation.video/GetDependencies/FfMpegDownload.cs @@ -18,12 +18,14 @@ public class FfMpegDownload : IFfMpegDownload private readonly FfmpegExePath _ffmpegExePath; private readonly IStorage _hostFileSystemStorage; private readonly IWebLogger _logger; + private readonly IFfMpegPreflightRunCheck _preflightRunCheck; private readonly IFfMpegPrepareBeforeRunning _prepareBeforeRunning; public FfMpegDownload(ISelectorStorage selectorStorage, AppSettings appSettings, IWebLogger logger, IFfMpegDownloadIndex downloadIndex, - IFfMpegDownloadBinaries downloadBinaries, IFfMpegPrepareBeforeRunning prepareBeforeRunning) + IFfMpegDownloadBinaries downloadBinaries, IFfMpegPrepareBeforeRunning prepareBeforeRunning, + IFfMpegPreflightRunCheck preflightRunCheck) { _appSettings = appSettings; _hostFileSystemStorage = @@ -33,6 +35,7 @@ public FfMpegDownload(ISelectorStorage selectorStorage, _ffmpegExePath = new FfmpegExePath(_appSettings); _downloadBinaries = downloadBinaries; _prepareBeforeRunning = prepareBeforeRunning; + _preflightRunCheck = preflightRunCheck; } public async Task DownloadFfMpeg() @@ -78,6 +81,11 @@ public async Task DownloadFfMpeg() return FfmpegDownloadStatus.PrepareBeforeRunningFailed; } + if ( !await _preflightRunCheck.TryRun(currentArchitecture) ) + { + return FfmpegDownloadStatus.PreflightRunCheckFailed; + } + return FfmpegDownloadStatus.Ok; } diff --git a/starsky/starsky.foundation.video/GetDependencies/FfMpegDownloadBackgroundService.cs b/starsky/starsky.foundation.video/GetDependencies/FfMpegDownloadBackgroundService.cs index 205f87ccd..b05124476 100644 --- a/starsky/starsky.foundation.video/GetDependencies/FfMpegDownloadBackgroundService.cs +++ b/starsky/starsky.foundation.video/GetDependencies/FfMpegDownloadBackgroundService.cs @@ -29,8 +29,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) scope.ServiceProvider.GetRequiredService(); var prepareBeforeRunning = scope.ServiceProvider.GetRequiredService(); + var preflightBeforeRunning = + scope.ServiceProvider.GetRequiredService(); - await new FfMpegDownload(selectorStorage, appSettings, logger, downloadIndex, - downloadBinaries, prepareBeforeRunning).DownloadFfMpeg(); + var service = new FfMpegDownload(selectorStorage, appSettings, logger, downloadIndex, + downloadBinaries, prepareBeforeRunning, preflightBeforeRunning); + await service.DownloadFfMpeg(); } } diff --git a/starsky/starsky.foundation.video/GetDependencies/FfMpegExePath.cs b/starsky/starsky.foundation.video/GetDependencies/FfMpegExePath.cs index 8d4de4663..516206f93 100644 --- a/starsky/starsky.foundation.video/GetDependencies/FfMpegExePath.cs +++ b/starsky/starsky.foundation.video/GetDependencies/FfMpegExePath.cs @@ -1,3 +1,4 @@ +using starsky.foundation.platform.Architecture; using starsky.foundation.platform.Models; namespace starsky.foundation.video.GetDependencies; @@ -15,6 +16,20 @@ internal string GetExeParentFolder(string currentArchitecture) : $"{FfmpegDependenciesFolder}-{currentArchitecture}"); } + /// + /// Get the path to the ffmpeg executable (assume current architecture) + /// + /// Full path of executable + internal string GetExePath() + { + return GetExePath(CurrentArchitecture + .GetCurrentRuntimeIdentifier()); + } + + /// + /// Get the path to the ffmpeg executable + /// + /// Full path of executable internal string GetExePath(string currentArchitecture) { var exeFile = Path.Combine(GetExeParentFolder(currentArchitecture), diff --git a/starsky/starsky.foundation.video/GetDependencies/FfMpegPreflightRunCheck.cs b/starsky/starsky.foundation.video/GetDependencies/FfMpegPreflightRunCheck.cs new file mode 100644 index 000000000..ef87b734e --- /dev/null +++ b/starsky/starsky.foundation.video/GetDependencies/FfMpegPreflightRunCheck.cs @@ -0,0 +1,57 @@ +using Medallion.Shell; +using starsky.foundation.injection; +using starsky.foundation.platform.Architecture; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.platform.Models; +using starsky.foundation.video.GetDependencies.Interfaces; + +namespace starsky.foundation.video.GetDependencies; + +[Service(typeof(IFfMpegPreflightRunCheck), InjectionLifetime = InjectionLifetime.Scoped)] +public class FfMpegPreflightRunCheck(AppSettings appSettings, IWebLogger logger) + : IFfMpegPreflightRunCheck +{ + public async Task TryRun() + { + var currentArchitecture = CurrentArchitecture.GetCurrentRuntimeIdentifier(); + return await TryRun(currentArchitecture); + } + + public async Task TryRun(string currentArchitecture) + { + var exePath = new FfmpegExePath(appSettings).GetExePath(currentArchitecture); + + try + { + var result = await Command.Run(exePath, "-version").Task; + + // Check if the command was successful + if ( result.Success ) + { + var output = result.StandardOutput; + if ( output.Contains("ffmpeg", StringComparison.OrdinalIgnoreCase) ) + { + return true; + } + + logger.LogError($"[{nameof(FfMpegPreflightRunCheck)}] Invalid application"); + } + else + { + logger.LogError($"[{nameof(FfMpegPreflightRunCheck)}] " + + $"Command failed with exit code " + + $"{result.ExitCode}: {result.StandardError}"); + } + + return false; + } + catch ( Exception exception ) + { + logger.LogError($"[{nameof(FfMpegPreflightRunCheck)}] " + + $"An error occurred while checking FFMpeg: " + + $"{exception.Message}"); + + return false; + } + } +} diff --git a/starsky/starsky.foundation.video/GetDependencies/Interfaces/IFfMpegPreflightRunCheck.cs b/starsky/starsky.foundation.video/GetDependencies/Interfaces/IFfMpegPreflightRunCheck.cs new file mode 100644 index 000000000..5c2824bf9 --- /dev/null +++ b/starsky/starsky.foundation.video/GetDependencies/Interfaces/IFfMpegPreflightRunCheck.cs @@ -0,0 +1,7 @@ +namespace starsky.foundation.video.GetDependencies.Interfaces; + +public interface IFfMpegPreflightRunCheck +{ + Task TryRun(); + Task TryRun(string currentArchitecture); +} diff --git a/starsky/starsky.foundation.video/GetDependencies/Models/FfmpegDownloadStatus.cs b/starsky/starsky.foundation.video/GetDependencies/Models/FfmpegDownloadStatus.cs index 77a4a3212..200fa85a0 100644 --- a/starsky/starsky.foundation.video/GetDependencies/Models/FfmpegDownloadStatus.cs +++ b/starsky/starsky.foundation.video/GetDependencies/Models/FfmpegDownloadStatus.cs @@ -9,5 +9,6 @@ public enum FfmpegDownloadStatus DownloadBinariesFailedMissingFileName, DownloadBinariesFailedSha256Check, DownloadBinariesFailedZipperNotExtracted, - PrepareBeforeRunningFailed + PrepareBeforeRunningFailed, + PreflightRunCheckFailed } diff --git a/starsky/starskytest/FakeMocks/FakeIFfMpegPreflightRunCheck.cs b/starsky/starskytest/FakeMocks/FakeIFfMpegPreflightRunCheck.cs new file mode 100644 index 000000000..89395ed0d --- /dev/null +++ b/starsky/starskytest/FakeMocks/FakeIFfMpegPreflightRunCheck.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using starsky.foundation.platform.Architecture; +using starsky.foundation.platform.Models; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.video.GetDependencies; +using starsky.foundation.video.GetDependencies.Interfaces; + +namespace starskytest.FakeMocks; + +public class FakeIFfMpegPreflightRunCheck : IFfMpegPreflightRunCheck +{ + private readonly AppSettings? _appSettings; + private readonly IStorage? _storage; + + public FakeIFfMpegPreflightRunCheck(IStorage? storage = null, AppSettings? appSettings = null) + { + _storage = storage; + _appSettings = appSettings; + } + + public async Task TryRun() + { + var currentArchitecture = CurrentArchitecture.GetCurrentRuntimeIdentifier(); + return await TryRun(currentArchitecture); + } + + public Task TryRun(string currentArchitecture) + { + if ( _appSettings == null || _storage == null ) + { + return Task.FromResult(false); + } + + var exePath = new FfmpegExePath(_appSettings).GetExePath(currentArchitecture); + return Task.FromResult(_storage.ExistFile(exePath)); + } +} diff --git a/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegDownloadBackgroundServiceTests.cs b/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegDownloadBackgroundServiceTests.cs index 46dd092aa..95a8aaf9b 100644 --- a/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegDownloadBackgroundServiceTests.cs +++ b/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegDownloadBackgroundServiceTests.cs @@ -27,6 +27,7 @@ public FfMpegDownloadBackgroundServiceTests() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); _serviceScopeFactory = serviceProvider.GetRequiredService(); diff --git a/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegDownloadTest.cs b/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegDownloadTest.cs index 3a88fd012..b2a4273b5 100644 --- a/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegDownloadTest.cs +++ b/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegDownloadTest.cs @@ -121,7 +121,7 @@ public async Task DownloadFfMpeg_ShouldSkipDueToSettings(string settingName) var ffmpegDownload = new FfMpegDownload(new FakeSelectorStorage(), appSettings, logger, new FakeIFfMpegDownloadIndex(), new FakeIFfMpegDownloadBinaries(), - new FakeIFfMpegPrepareBeforeRunning()); + new FakeIFfMpegPrepareBeforeRunning(), new FakeIFfMpegPreflightRunCheck()); var result = await ffmpegDownload.DownloadFfMpeg(); @@ -138,7 +138,7 @@ public async Task DownloadFfMpeg_MissingIndex() new FfMpegDownload(new FakeSelectorStorage(), appSettings, logger, new FakeIFfMpegDownloadIndex(), new FakeIFfMpegDownloadBinaries(), - new FakeIFfMpegPrepareBeforeRunning()); + new FakeIFfMpegPrepareBeforeRunning(), new FakeIFfMpegPreflightRunCheck()); var resultMissingIndex = await ffmpegDownload.DownloadFfMpeg(); @@ -158,7 +158,7 @@ public async Task DownloadFfMpeg_MissingIndex_DownloadBinariesFailedMissingFileN new FakeIFfMpegDownloadIndex(new FfmpegBinariesContainer { Success = true }), new FfMpegDownloadBinaries(new FakeSelectorStorage(_storage), _httpClientHelper, appSettings, logger, new Zipper(new FakeIWebLogger())), - new FakeIFfMpegPrepareBeforeRunning()); + new FakeIFfMpegPrepareBeforeRunning(), new FakeIFfMpegPreflightRunCheck()); var resultMissingIndex = await ffmpegDownload.DownloadFfMpeg(); @@ -180,7 +180,8 @@ public async Task DownloadFfMpeg_DownloadBinariesFail() Data = new FfmpegBinariesIndex { Binaries = new List() } }), new FakeIFfMpegDownloadBinaries(FfmpegDownloadStatus - .DownloadBinariesFailedMissingFileName), new FakeIFfMpegPrepareBeforeRunning()); + .DownloadBinariesFailedMissingFileName), new FakeIFfMpegPrepareBeforeRunning(), + new FakeIFfMpegPreflightRunCheck()); var resultBinaryFail = await ffmpegDownload.DownloadFfMpeg(); @@ -207,7 +208,8 @@ public async Task DownloadFfMpeg_FileAlreadyExists() { Success = true, Data = new FfmpegBinariesIndex { Binaries = new List() } - }), new FakeIFfMpegDownloadBinaries(), new FakeIFfMpegPrepareBeforeRunning()); + }), new FakeIFfMpegDownloadBinaries(), new FakeIFfMpegPrepareBeforeRunning(), + new FakeIFfMpegPreflightRunCheck()); var resultFileAlreadyExists = await ffmpegDownload.DownloadFfMpeg(); @@ -232,7 +234,7 @@ public async Task DownloadFfMpeg_DownloadFail_InvalidShaHash() }), new FfMpegDownloadBinaries(new FakeSelectorStorage(storage), _httpClientHelper, appSettings, logger, new Zipper(new FakeIWebLogger())), - new FakeIFfMpegPrepareBeforeRunning()); + new FakeIFfMpegPrepareBeforeRunning(), new FakeIFfMpegPreflightRunCheck()); var resultInvalidHash = await ffmpegDownload.DownloadFfMpeg(); @@ -258,7 +260,7 @@ public async Task DownloadFfMpeg_DownloadFail_ZipFileNotFound() }), new FfMpegDownloadBinaries(new FakeSelectorStorage(storage), _httpClientHelper, appSettings, logger, new Zipper(new FakeIWebLogger())), - new FakeIFfMpegPrepareBeforeRunning()); + new FakeIFfMpegPrepareBeforeRunning(), new FakeIFfMpegPreflightRunCheck()); var resultZipFail = await ffmpegDownload.DownloadFfMpeg(); @@ -295,7 +297,7 @@ public async Task DownloadFfMpeg_PrepareBeforeRunningFail() new FfMpegPrepareBeforeRunning(new FakeSelectorStorage(storage), new FakeIMacCodeSign(), new FfMpegChmod(new FakeSelectorStorage(storage), logger), appSettings, - logger)); + logger), new FakeIFfMpegPreflightRunCheck()); var resultPrepFail = await ffmpegDownload.DownloadFfMpeg(); @@ -308,6 +310,55 @@ public async Task DownloadFfMpeg_PrepareBeforeRunningFail() Assert.AreEqual(FfmpegDownloadStatus.PrepareBeforeRunningFailed, resultPrepFail); } + + [TestMethod] + public async Task DownloadFfMpeg_PreflightRunCheckFailed() + { + var appSettings = new AppSettings { DependenciesFolder = DependencyFolderName }; + var logger = new FakeIWebLogger(); + var storage = new FakeIStorage(["/"], + new List + { + $"FfMpegDownloadTest{Path.DirectorySeparatorChar}mock_test.zip", "/bin/chmod" + }, + new List + { + new CreateAnZipfileFakeFfMpeg().Bytes.ToArray(), + CreateAnZipFileMacOs.Bytes.ToArray() + }); + var zipper = new FakeIZipper(new List> + { + new($"FfMpegDownloadTest{Path.DirectorySeparatorChar}mock_test.zip", + [.. new CreateAnZipfileFakeFfMpeg().Bytes.ToArray()]) + }, storage); + + const string hash = "31852c0b33f35ff16e96d53be370ce86df92db6d4633ab0a8dae38acbf393ead"; + + var ffmpegDownload = + new FfMpegDownload(new FakeSelectorStorage(storage), appSettings, + logger, + new FakeIFfMpegDownloadIndex(new FfmpegBinariesContainer + { + Success = true, + Data = CreateExampleFile(hash), + BaseUrls = new List { new("https://qdraw.nl/") } + }), new FfMpegDownloadBinaries(new FakeSelectorStorage(storage), + _httpClientHelper, + appSettings, logger, zipper), new FfMpegPrepareBeforeRunning( + new FakeSelectorStorage(storage), + new FakeIMacCodeSign(new Dictionary + { + { + $"FfMpegDownloadTest/ffmpeg-{CurrentArchitecture.GetCurrentRuntimeIdentifier()}/ffmpeg", + true + } + }), new FakeIFfmpegChmod(storage), appSettings, logger), + new FfMpegPreflightRunCheck(appSettings, new FakeIWebLogger())); + + var resultAllStages = await ffmpegDownload.DownloadFfMpeg(); + + Assert.AreEqual(FfmpegDownloadStatus.PreflightRunCheckFailed, resultAllStages); + } [TestMethod] public async Task DownloadFfMpeg_AllStages() @@ -350,7 +401,8 @@ public async Task DownloadFfMpeg_AllStages() $"FfMpegDownloadTest/ffmpeg-{CurrentArchitecture.GetCurrentRuntimeIdentifier()}/ffmpeg", true } - }), new FakeIFfmpegChmod(storage), appSettings, logger)); + }), new FakeIFfmpegChmod(storage), appSettings, logger), + new FakeIFfMpegPreflightRunCheck(storage, appSettings)); var resultAllStages = await ffmpegDownload.DownloadFfMpeg(); diff --git a/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegPreflightRunCheckTests.cs b/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegPreflightRunCheckTests.cs new file mode 100644 index 000000000..cd9d39d8d --- /dev/null +++ b/starsky/starskytest/starsky.foundation.video/GetDependencies/FfMpegPreflightRunCheckTests.cs @@ -0,0 +1,113 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.platform.Architecture; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.platform.Models; +using starsky.foundation.storage.ArchiveFormats; +using starsky.foundation.storage.Helpers; +using starsky.foundation.storage.Storage; +using starsky.foundation.video.GetDependencies; +using starskytest.FakeCreateAn; +using starskytest.FakeCreateAn.CreateAnZipfileFakeFFMpeg; +using starskytest.FakeMocks; + +namespace starskytest.starsky.foundation.video.GetDependencies; + +[TestClass] +public class FfMpegPreflightRunCheckTests +{ + private readonly AppSettings _appSettings; + private readonly string _currentArchitecture; + private readonly FfMpegChmod _ffMpegChmod; + private readonly FfmpegExePath _ffmpegExePath; + private readonly StorageHostFullPathFilesystem _hostFileSystemStorage; + private readonly bool _isWindows; + private readonly IWebLogger _logger; + + public FfMpegPreflightRunCheckTests() + { + _hostFileSystemStorage = new StorageHostFullPathFilesystem(new FakeIWebLogger()); + _logger = new FakeIWebLogger(); + _ffMpegChmod = new FfMpegChmod(new FakeSelectorStorage(_hostFileSystemStorage), _logger); + + var parentFolder = + Path.Combine(new CreateAnImage().BasePath, "FfMpegPreflightRunCheckTests"); + _appSettings = new AppSettings { DependenciesFolder = parentFolder }; + + _ffmpegExePath = new FfmpegExePath(_appSettings); + _currentArchitecture = CurrentArchitecture + .GetCurrentRuntimeIdentifier(); + _isWindows = new AppSettings().IsWindows; + } + + private async Task CreateFile(int exitCode, string echoName, bool enableChmod = true) + { + if ( _hostFileSystemStorage.ExistFolder( + _ffmpegExePath.GetExeParentFolder(_currentArchitecture)) ) + { + _hostFileSystemStorage.FolderDelete( + _ffmpegExePath.GetExeParentFolder(_currentArchitecture)); + } + + _hostFileSystemStorage.CreateDirectory( + _ffmpegExePath.GetExeParentFolder(_currentArchitecture)); + var stream = + StringToStreamHelper.StringToStream( + $"#!/bin/bash\necho Fake {echoName}\nexit {exitCode}"); + await _hostFileSystemStorage.WriteStreamAsync(stream, + _ffmpegExePath.GetExePath(_currentArchitecture)); + if ( enableChmod ) + { + await _ffMpegChmod.Chmod(_ffmpegExePath.GetExePath()); + } + + var result = Zipper.ExtractZip([.. new CreateAnZipfileFakeFfMpeg().Bytes]); + var (_, item) = result.FirstOrDefault(p => p.Key.Contains("ffmpeg.exe")); + + await _hostFileSystemStorage.WriteStreamAsync(new MemoryStream(item), + Path.Combine(_ffmpegExePath.GetExeParentFolder(_currentArchitecture), "ffmpeg.exe")); + } + + [TestMethod] + public async Task TryRun_StatusCodeHappyFlow() + { + await CreateFile(0, "ffmpeg"); + + // Arrange + var ffMpegPreflightRunCheck = new FfMpegPreflightRunCheck(_appSettings, _logger); + + // Act + var result = await ffMpegPreflightRunCheck.TryRun(); + + // Assert + Assert.IsTrue(result); + } + + [DataTestMethod] + [DataRow(0, "ffmpeg", true, true)] + [DataRow(1, "ffmpeg", true, false)] + [DataRow(0, "other_process", true, false)] + [DataRow(0, "ffmpeg", false, false)] + public async Task TryRun_StatusCode(int exitCode, string echoName, bool enableChmod, + bool expectedResult) + { + if ( _isWindows ) + { + Assert.Inconclusive("exit code 1 is not supported on windows"); + return; + } + + await CreateFile(exitCode, echoName, enableChmod); + + // Arrange + var ffMpegPreflightRunCheck = new FfMpegPreflightRunCheck(_appSettings, _logger); + + // Act + var result = await ffMpegPreflightRunCheck.TryRun(); + + // Assert + Assert.AreEqual(expectedResult, result); + } +}