diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24703f3..fb1484b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,7 +41,7 @@ jobs: --configuration Release ` --runtime win-x64 ` --self-contained true ` - --output SFTPSync/bin/Release/net8.0-windows/publish/win-x64/ ` + --output SFTPSync/bin/Release/net8.0-windows ` /p:PublishSingleFile=true ` /p:PublishReadyToRun=true @@ -51,7 +51,7 @@ jobs: --configuration Release ` --runtime win-x64 ` --self-contained true ` - --output SFTPSyncStop/bin/Release/net8.0-windows/publish/win-x64/ ` + --output SFTPSyncStop/bin/Release/net8.0-windows ` /p:PublishSingleFile=true ` /p:PublishReadyToRun=true @@ -61,7 +61,7 @@ jobs: --configuration Release ` --runtime win-x64 ` --self-contained true ` - --output SFTPSyncUI/bin/Release/net8.0-windows/publish/win-x64/ ` + --output SFTPSyncUI/bin/Release/net8.0-windows ` /p:PublishSingleFile=true ` /p:PublishReadyToRun=true @@ -75,7 +75,7 @@ jobs: AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }} shell: pwsh run: | - $publishDirs = @("SFTPSync/bin/Release/net8.0-windows/publish/win-x64", "SFTPSyncStop/bin/Release/net8.0-windows/publish/win-x64", "SFTPSyncUI/bin/Release/net8.0-windows/publish/win-x64") + $publishDirs = @("SFTPSync/bin/Release/net8.0-windows", "SFTPSyncStop/bin/Release/net8.0-windows", "SFTPSyncUI/bin/Release/net8.0-windows") foreach ($dir in $publishDirs) { if (Test-Path $dir) { $files = Get-ChildItem -Path $dir -Include *.exe, *.dll -Recurse diff --git a/SFTPSync.chm-keep b/SFTPSync.chm-keep new file mode 100644 index 0000000..80cc187 Binary files /dev/null and b/SFTPSync.chm-keep differ diff --git a/SFTPSyncLib/RemoteSync.cs b/SFTPSyncLib/RemoteSync.cs index 715b56f..fc457d8 100644 --- a/SFTPSyncLib/RemoteSync.cs +++ b/SFTPSyncLib/RemoteSync.cs @@ -16,6 +16,8 @@ public class RemoteSync : IDisposable SftpClient _sftp; SyncDirector _director; HashSet _activeDirSync = new HashSet(); + readonly SemaphoreSlim _sftpLock = new SemaphoreSlim(1, 1); + bool _disposed; public Task DoneMakingFolders { get; } @@ -85,7 +87,7 @@ public static void SyncFile(SftpClient sftp, string sourcePath, string destinati return; } - catch (Exception ex) when (ex is IOException || ex is FileNotFoundException) + catch (Exception ex) { retryCount++; if (retryCount >= maxRetries) @@ -150,6 +152,9 @@ public async Task CreateDirectories(string localPath, string remotePath) try { + if (!EnsureConnectedSafe()) + return; + //Got local directories to sync var localDirectories = FilteredDirectories(localPath); @@ -194,6 +199,9 @@ public async Task InitialSync(string localPath, string remotePath) //Wait for the folders to be created before starting the initial sync await DoneMakingFolders; + if (!EnsureConnectedSafe()) + return; + //Get the local directories to sync var localDirectories = FilteredDirectories(localPath); @@ -256,65 +264,151 @@ public static bool IsFileReady(String sFilename) } } + private void EnsureConnected() + { + if (_disposed) + throw new ObjectDisposedException(nameof(RemoteSync)); - private async void Fsw_Changed(object? sender, FileSystemEventArgs arg) + if (_sftp.IsConnected) + return; + + _sftp.Connect(); + } + + private bool EnsureConnectedSafe() { - if (arg.ChangeType == WatcherChangeTypes.Changed || arg.ChangeType == WatcherChangeTypes.Created - || arg.ChangeType == WatcherChangeTypes.Renamed) + try { - var changedPath = Path.GetDirectoryName(arg.FullPath); - var relativePath = _localRootDirectory == changedPath ? "" : changedPath?.Substring(_localRootDirectory.Length).Replace('\\', '/'); - var fullRemotePath = _remoteRootDirectory + relativePath; - await Task.Yield(); - bool makeDirectory = true; - lock (_activeDirSync) - { - if (changedPath == null) - return; - if (_activeDirSync.Contains(changedPath)) - makeDirectory = false; - else - _activeDirSync.Add(changedPath); - } + EnsureConnected(); + return true; + } + catch (Exception ex) + { + Logger.LogError($"SFTP connection error: {ex.Message}"); + return false; + } + } - //check if we're a new directory - if (makeDirectory && Directory.Exists(arg.FullPath) && !_sftp.Exists(arg.FullPath)) - { - _sftp.CreateDirectory(fullRemotePath); - } - if (makeDirectory) + private async void Fsw_Changed(object? sender, FileSystemEventArgs arg) + { + try + { + if (arg.ChangeType == WatcherChangeTypes.Changed || arg.ChangeType == WatcherChangeTypes.Created + || arg.ChangeType == WatcherChangeTypes.Renamed) { + var changedPath = Path.GetDirectoryName(arg.FullPath); + var relativePath = _localRootDirectory == changedPath ? "" : changedPath?.Substring(_localRootDirectory.Length).Replace('\\', '/'); + var fullRemotePath = _remoteRootDirectory + relativePath; + await Task.Yield(); + bool makeDirectory = true; lock (_activeDirSync) { - _activeDirSync.Remove(changedPath); + if (changedPath == null) + return; + if (_activeDirSync.Contains(changedPath)) + makeDirectory = false; + else + _activeDirSync.Add(changedPath); } - } - while (!IsFileReady(arg.FullPath)) - await Task.Delay(25); + bool connectionOk; + await _sftpLock.WaitAsync(); + try + { + connectionOk = EnsureConnectedSafe(); + if (connectionOk) + { + //check if we're a new directory + if (makeDirectory && Directory.Exists(arg.FullPath) && !_sftp.Exists(fullRemotePath)) + { + _sftp.CreateDirectory(fullRemotePath); + } + } + } + finally + { + _sftpLock.Release(); + } + if (!connectionOk) + { + if (makeDirectory) + { + lock (_activeDirSync) + { + _activeDirSync.Remove(changedPath); + } + } + return; + } - lock (_activeDirSync) - { - if (_activeDirSync.Contains(arg.FullPath)) + if (makeDirectory) + { + lock (_activeDirSync) + { + _activeDirSync.Remove(changedPath); + } + } + + if (Directory.Exists(arg.FullPath)) return; - else - _activeDirSync.Add(arg.FullPath); - } - SyncFile(_sftp, arg.FullPath, fullRemotePath + "/" + Path.GetFileName(arg.FullPath) ); - lock (_activeDirSync) - { - _activeDirSync.Remove(arg.FullPath); + var waitStart = DateTime.UtcNow; + while (!IsFileReady(arg.FullPath)) + { + if (!File.Exists(arg.FullPath)) + return; + if (DateTime.UtcNow - waitStart > TimeSpan.FromSeconds(30)) + { + Logger.LogWarnig($"Timed out waiting for file to be ready: {arg.FullPath}"); + return; + } + await Task.Delay(25); + } + + lock (_activeDirSync) + { + if (_activeDirSync.Contains(arg.FullPath)) + return; + else + _activeDirSync.Add(arg.FullPath); + } + + bool fileConnectionOk; + await _sftpLock.WaitAsync(); + try + { + fileConnectionOk = EnsureConnectedSafe(); + if (fileConnectionOk) + { + SyncFile(_sftp, arg.FullPath, fullRemotePath + "/" + Path.GetFileName(arg.FullPath)); + } + } + finally + { + _sftpLock.Release(); + lock (_activeDirSync) + { + _activeDirSync.Remove(arg.FullPath); + } + } + + if (!fileConnectionOk) + return; } } + catch (Exception ex) + { + Logger.LogError($"Unhandled exception in file sync handler: {ex.Message}"); + } } public void Dispose() { if (_sftp != null) { + _disposed = true; _sftp.Dispose(); } } diff --git a/SFTPSyncLib/SyncDirector.cs b/SFTPSyncLib/SyncDirector.cs index 4ccdff0..4d3a25f 100644 --- a/SFTPSyncLib/SyncDirector.cs +++ b/SFTPSyncLib/SyncDirector.cs @@ -13,12 +13,15 @@ public SyncDirector(string rootFolder) _fsw = new FileSystemWatcher(rootFolder, "*.*") { IncludeSubdirectories = true, - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, + // Max allowed size; helps reduce missed events during bursts. + InternalBufferSize = 64 * 1024 }; _fsw.Changed += Fsw_Changed; _fsw.Created += Fsw_Created; _fsw.Renamed += Fsw_Renamed; + _fsw.Error += Fsw_Error; _fsw.EnableRaisingEvents = true; } @@ -61,5 +64,10 @@ private void Fsw_Changed(object sender, FileSystemEventArgs e) } } } + + private void Fsw_Error(object sender, ErrorEventArgs e) + { + Logger.LogError($"FileSystemWatcher error: {e.GetException()?.Message}"); + } } } diff --git a/SFTPSyncSetup/Product.wxs b/SFTPSyncSetup/Product.wxs index abf3c0f..927e67c 100644 --- a/SFTPSyncSetup/Product.wxs +++ b/SFTPSyncSetup/Product.wxs @@ -5,7 +5,7 @@ Id="*" Name="SFTPSync" Language="1033" - Version="1.4" + Version="1.5" Manufacturer="Synergex International Corporation" UpgradeCode="6000f870-b811-4e22-b80b-5b8956317d09"> @@ -53,71 +53,71 @@ - + - + - - - + - - - + - - - + - + - + - + - + - + - + - - - + - - - + - + - + - + @@ -175,17 +175,17 @@ - + - - + diff --git a/SFTPSyncSetup/SFTPSyncSetup.wixproj b/SFTPSyncSetup/SFTPSyncSetup.wixproj index 05c1909..33fabd6 100644 --- a/SFTPSyncSetup/SFTPSyncSetup.wixproj +++ b/SFTPSyncSetup/SFTPSyncSetup.wixproj @@ -6,7 +6,7 @@ 3.10 5074cbcb-641b-4a9c-b3bc-8dd0b78810a6 2.0 - SFTPSync-1.4 + SFTPSync-1.5 Package SFTPSyncSetup diff --git a/SFTPSyncUI/SFTPSyncUI.cs b/SFTPSyncUI/SFTPSyncUI.cs index f4d7577..ec49f9b 100644 --- a/SFTPSyncUI/SFTPSyncUI.cs +++ b/SFTPSyncUI/SFTPSyncUI.cs @@ -27,6 +27,23 @@ static void Main() if (processPath != null) ExecutableFile = Path.ChangeExtension(processPath, ".exe"); + Application.ThreadException += (sender, args) => + { + Logger.LogError($"Unhandled UI exception: {args.Exception.Message}"); + }; + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + if (args.ExceptionObject is Exception ex) + Logger.LogError($"Unhandled exception: {ex.Message}"); + else + Logger.LogError("Unhandled exception: unknown error"); + }; + TaskScheduler.UnobservedTaskException += (sender, args) => + { + Logger.LogError($"Unobserved task exception: {args.Exception.Message}"); + args.SetObserved(); + }; + // Check if another instance is already running and if so, tell it to show its window, then exit bool createdNew; Mutex mutex = new Mutex(true, $"Global\\SFTPSyncUI", out createdNew); @@ -257,4 +274,4 @@ public static void StopSync(Action loggerAction) mainForm?.SetStatusBarText("Sync stopped"); } } -} \ No newline at end of file +} diff --git a/SFTPSyncUI/SFTPSyncUI.csproj b/SFTPSyncUI/SFTPSyncUI.csproj index 9e93ba4..a864dd6 100644 --- a/SFTPSyncUI/SFTPSyncUI.csproj +++ b/SFTPSyncUI/SFTPSyncUI.csproj @@ -20,10 +20,10 @@ LICENSE.md True Synergex International, Inc. - 1.4 - 1.4 + 1.5 + 1.5 preview - 1.4 + 1.5 diff --git a/SFTPSyncUI/docs/HTML Help (CHM).chm b/SFTPSyncUI/docs/HTML Help (CHM).chm new file mode 100644 index 0000000..851f70c Binary files /dev/null and b/SFTPSyncUI/docs/HTML Help (CHM).chm differ