@@ -22,6 +22,8 @@ namespace SecureFolderFS.Core.WebDav
2222 /// <inheritdoc cref="IFileSystemInfo"/>
2323 public abstract class WebDavFileSystem : IFileSystemInfo
2424 {
25+ private const int MAX_LISTENER_START_ATTEMPTS = 10 ;
26+
2527 /// <inheritdoc/>
2628 public string Id { get ; } = Constants . FileSystem . FS_ID ;
2729
@@ -49,13 +51,12 @@ public virtual async Task<IVfsRoot> MountAsync(IFolder folder, IDisposable unloc
4951 if ( ! PortHelpers . IsPortAvailable ( webDavOptions . Port ) )
5052 webDavOptions . SetPortInternal ( PortHelpers . GetNextAvailablePort ( webDavOptions . Port ) ) ;
5153
52- var prefix = $ " { webDavOptions . Protocol } :// { webDavOptions . Domain } : { webDavOptions . Port } /" ;
53- var httpListener = new HttpListener ( ) ;
54-
55- httpListener . Prefixes . Add ( prefix ) ;
56- httpListener . AuthenticationSchemes = AuthenticationSchemes . Anonymous ;
54+ // Start the listener up-front (self-healing onto another port if the bind fails) so a genuine
55+ // failure surfaces to the caller and fails the unlocking operation, instead of leaving a silently-dead server.
56+ // Starting here also fixes webDavOptions.Port to the actually bound port before the platform
57+ // implementation derives the mount URL from it.
58+ var httpListener = StartListener ( webDavOptions ) ;
5759
58- //var store = new EncryptingDiskStore(specifics.ContentFolder.Id, specifics, !specifics.Options.IsReadOnly);
5960 var rootFolder = new CryptoFolder ( specifics . ContentFolder . Id , specifics . ContentFolder , specifics ) ;
6061 var store = new BackedDavStore ( rootFolder , ! specifics . Options . IsReadOnly ) ;
6162 var dispatcher = new WebDavDispatcher ( new RootDiskStore ( specifics . Options . VolumeName , store ) , new RequestHandlerProvider ( ) , null ) ;
@@ -68,6 +69,38 @@ public virtual async Task<IVfsRoot> MountAsync(IFolder folder, IDisposable unloc
6869 cancellationToken ) ;
6970 }
7071
72+ /// <summary>
73+ /// Creates and starts an <see cref="HttpListener"/> for the configured WebDAV endpoint, with a fallback to
74+ /// the next available port if the bind fails (e.g., the port was taken between the availability check and the bind).
75+ /// </summary>
76+ /// <param name="options">The WebDAV options. The <see cref="WebDavOptions.Port"/> is updated to the actually bound port.</param>
77+ /// <returns>A started <see cref="HttpListener"/> bound to the resolved port.</returns>
78+ private static HttpListener StartListener ( WebDavOptions options )
79+ {
80+ HttpListenerException ? lastException = null ;
81+ for ( var attempt = 0 ; attempt < MAX_LISTENER_START_ATTEMPTS ; attempt ++ )
82+ {
83+ var listener = new HttpListener ( ) ;
84+ listener . Prefixes . Add ( $ "{ options . Protocol } ://{ options . Domain } :{ options . Port } /") ;
85+ listener . AuthenticationSchemes = AuthenticationSchemes . Anonymous ;
86+
87+ try
88+ {
89+ listener . Start ( ) ;
90+ return listener ;
91+ }
92+ catch ( HttpListenerException ex )
93+ {
94+ // The port became unavailable between the check and the bind, proceed with self-heal onto the next free port
95+ lastException = ex ;
96+ listener . Close ( ) ;
97+ options . SetPortInternal ( PortHelpers . GetNextAvailablePort ( options . Port + 1 ) ) ;
98+ }
99+ }
100+
101+ throw lastException ?? new HttpListenerException ( ) ;
102+ }
103+
71104 /// <inheritdoc/>
72105 public abstract Task < string > GetVolumeNameAsync ( string candidateName , CancellationToken cancellationToken = default ) ;
73106
0 commit comments