Преглед изворни кода

Issue 2382 – Prevent hang when new device is attached or removed while some mapped network drive is not available

https://winscp.net/tracker/2382

Source commit: 70ab5c4162ee0e1f845cc78c1a703c62edf80c48
Martin Prikryl пре 4 месеци
родитељ
комит
89849132f6

+ 4 - 19
source/forms/ScpCommander.cpp

@@ -79,6 +79,7 @@ __fastcall TScpCommanderForm::TScpCommanderForm(TComponent* Owner)
   FInternalDDDownloadList = new TStringList();
   FLocalPathComboBoxPaths = new TStringList();
 
+  DriveInfo->AddHandler(DriveNotification);
   LocalPathComboUpdateDrives();
 
   LocalBackButton->LinkSubitems = HistoryMenu(osLocal, true)->Items;
@@ -142,6 +143,7 @@ __fastcall TScpCommanderForm::TScpCommanderForm(TComponent* Owner)
 //---------------------------------------------------------------------------
 __fastcall TScpCommanderForm::~TScpCommanderForm()
 {
+  DriveInfo->RemoveHandler(DriveNotification);
   delete FInternalDDDownloadList;
   SAFE_DESTROY(FLocalPathComboBoxPaths);
 }
@@ -2449,10 +2451,9 @@ void __fastcall TScpCommanderForm::CommandLineComboEditWndProc(TMessage & Messag
   }
 }
 //---------------------------------------------------------------------------
-void __fastcall TScpCommanderForm::LocalDriveViewRefreshDrives(TObject * Sender, bool Global)
+void __fastcall TScpCommanderForm::DriveNotification(TDriveNotification Notification, UnicodeString Drive)
 {
-  // Process global drive notifications from only one drive view
-  if (!Global || (Sender == LocalDriveView))
+  if (Notification == dnRefresh)
   {
     LocalPathComboUpdateDrives();
     LocalPathComboUpdate(LocalDirView, LocalPathComboBox);
@@ -2461,22 +2462,6 @@ void __fastcall TScpCommanderForm::LocalDriveViewRefreshDrives(TObject * Sender,
       LocalPathComboUpdate(OtherLocalDirView, RemotePathComboBox);
     }
   }
-
-  if (!Global)
-  {
-    TDriveView * TheOtherLocalDriveView;
-    if (Sender == LocalDriveView)
-    {
-      TheOtherLocalDriveView = OtherLocalDriveView;
-    }
-    else
-    {
-      DebugAssert(Sender == OtherLocalDriveView);
-      TheOtherLocalDriveView = LocalDriveView;
-    }
-    // Or rather an immediate refresh?
-    TheOtherLocalDriveView->ScheduleDriveRefresh();
-  }
 }
 //---------------------------------------------------------------------------
 void __fastcall TScpCommanderForm::HomeDirectory(TOperationSide Side)

+ 0 - 2
source/forms/ScpCommander.dfm

@@ -1278,7 +1278,6 @@ inherited ScpCommanderForm: TScpCommanderForm
         Height = 45
         WatchDirectory = True
         DirView = OtherLocalDirView
-        OnRefreshDrives = LocalDriveViewRefreshDrives
         OnBusy = DirViewBusy
         OnDDDragEnter = LocalFileControlDDDragEnter
         OnDDDragLeave = FileControlDDDragLeave
@@ -1898,7 +1897,6 @@ inherited ScpCommanderForm: TScpCommanderForm
       Height = 45
       WatchDirectory = True
       DirView = LocalDirView
-      OnRefreshDrives = LocalDriveViewRefreshDrives
       OnBusy = DirViewBusy
       OnDDDragEnter = LocalFileControlDDDragEnter
       OnDDDragLeave = FileControlDDDragLeave

+ 1 - 1
source/forms/ScpCommander.h

@@ -533,7 +533,6 @@ __published:
           bool FromLink);
   void __fastcall CommandLineComboBeginEdit(TTBEditItem *Sender,
           TTBEditItemViewer *Viewer, TEdit *EditControl);
-  void __fastcall LocalDriveViewRefreshDrives(TObject *Sender, bool Global);
   void __fastcall QueueSubmenuItemPopup(TTBCustomItem *Sender,
           bool FromLink);
   void __fastcall DirViewHistoryGo(TCustomDirView *Sender, int Index,
@@ -551,6 +550,7 @@ __published:
   void __fastcall OtherLocalDirViewUpdateStatusBar(TObject *Sender, const TStatusFileInfo &FileInfo);
   void __fastcall OtherLocalDirViewPathChange(TCustomDirView *Sender);
   void __fastcall LocalDriveViewNeedHiddenDirectories(TObject *Sender);
+  void __fastcall DriveNotification(TDriveNotification Notification, UnicodeString Drive);
 
 private:
   bool FConstructed;

+ 42 - 274
source/packages/filemng/DriveView.pas

@@ -56,7 +56,6 @@ const
   dvdsFloppy          = 8;  {Include floppy drives}
   dvdsRereadAllways   = 16; {Refresh drivestatus in any case}
 
-  WM_USER_SHCHANGENOTIFY = WM_USER + $2000 + 13;
   WM_USER_SUBDIRREADER = WM_USER_SHCHANGENOTIFY + 1;
 
 type
@@ -71,8 +70,6 @@ type
     DiscMonitor: TDiscMonitor; {Monitor thread}
     ChangeTimer: TTimer;       {Change timer for the monitor thread}
     DefaultDir: string;        {Current directory}
-    DriveHandle: THandle;
-    NotificationHandle: HDEVNOTIFY;
   end;
 
   TDriveStatusPair = TPair<string, TDriveStatus>;
@@ -155,8 +152,6 @@ type
     procedure WndProc(var Msg: TMessage);
   end;
 
-  TDriveViewRefreshDrives = procedure(Sender: TObject; Global: Boolean) of object;
-
   TDriveView = class(TCustomDriveView)
   private
     FDriveStatus: TObjectDictionary<string, TDriveStatus>;
@@ -175,8 +170,6 @@ type
     FForceRename: Boolean;
     FRenameNode: TTreeNode;
     FLastRenameName: string;
-    FInternalWindowHandle: HWND;
-    FChangeNotify: ULONG;
     FPrevSelected: TTreeNode;
     FPrevSelectedIndex: Integer;
     FChangeTimerSuspended: Integer;
@@ -186,7 +179,6 @@ type
 
     {Additional events:}
     FOnDisplayContextMenu: TNotifyEvent;
-    FOnRefreshDrives: TDriveViewRefreshDrives;
     FOnNeedHiddenDirectories: TNotifyEvent;
 
     {used components:}
@@ -235,13 +227,11 @@ type
     procedure TerminateWatchThread(Drive: string);
     function WatchThreadActive: Boolean; overload;
     function WatchThreadActive(Drive: string): Boolean; overload;
-    procedure InternalWndProc(var Msg: TMessage);
-    procedure UpdateDriveNotifications(Drive: string);
+    procedure SubscribeDriveNotifications(Drive: string);
     procedure DriveRemoved(Drive: string);
     procedure DriveRemoving(Drive: string);
-    procedure CancelDriveRefresh;
-    procedure DoRefreshDrives(Global: Boolean);
-    procedure RefreshRootNodes(dsFlags: Integer);
+    procedure RefreshRootNodes(Floppy: Boolean = False);
+    procedure DriveNotification(Notification: TDriveNotification; Drive: string);
 
     function DirAttrMask: Integer;
     function CreateDriveStatus: TDriveStatus;
@@ -303,7 +293,6 @@ type
     function GetDriveText(Drive: string): string;
     procedure ScanDrive(Drive: string);
     function GetDrives: TStrings;
-    procedure ScheduleDriveRefresh;
 
     {Node handling:}
     procedure SetImageIndex(Node: TTreeNode); virtual;
@@ -360,7 +349,6 @@ type
     {Additional events:}
     property OnDisplayContextMenu: TNotifyEvent read FOnDisplayContextMenu
       write FOnDisplayContextMenu;
-    property OnRefreshDrives: TDriveViewRefreshDrives read FOnRefreshDrives write FOnRefreshDrives;
     property OnBusy;
 
     property DDLinkOnExeDrag;
@@ -835,8 +823,6 @@ end;
 constructor TDriveView.Create(AOwner: TComponent);
 var
   Drive: TRealDrive;
-  ChangeNotifyEntry: TSHChangeNotifyEntry;
-  Dummy: string;
 begin
   inherited;
 
@@ -878,20 +864,7 @@ begin
   FStartPos.Y := -1;
   FDragPos := FStartPos;
 
-  FInternalWindowHandle := Classes.AllocateHWnd(InternalWndProc);
-
-  // Source: petr.solin 2022-02-25
-  FChangeNotify := 0;
-  if SpecialFolderLocation(CSIDL_DESKTOP, Dummy, ChangeNotifyEntry.pidl) then
-  begin
-    ChangeNotifyEntry.fRecursive := False;
-
-    FChangeNotify :=
-      SHChangeNotifyRegister(
-        FInternalWindowHandle, SHCNRF_ShellLevel or SHCNRF_NewDelivery,
-        SHCNE_RENAMEFOLDER or SHCNE_MEDIAINSERTED or SHCNE_MEDIAREMOVED,
-        WM_USER_SHCHANGENOTIFY, 1, ChangeNotifyEntry);
-  end;
+  DriveInfo.AddHandler(DriveNotification);
 
   with FDragDropFilesEx do
   begin
@@ -903,8 +876,8 @@ destructor TDriveView.Destroy;
 var
   DriveStatusPair: TDriveStatusPair;
 begin
-  if FChangeNotify <> 0 then SHChangeNotifyDeregister(FChangeNotify);
-  Classes.DeallocateHWnd(FInternalWindowHandle);
+
+  DriveInfo.RemoveHandler(DriveNotification);
 
   for DriveStatusPair in FDriveStatus do
   begin
@@ -915,7 +888,6 @@ begin
       if Assigned(ChangeTimer) then
         FreeAndNil(ChangeTimer);
     end;
-    UpdateDriveNotifications(DriveStatusPair.Key);
   end;
   FDriveStatus.Free;
 
@@ -945,8 +917,6 @@ begin
     ChangeTimer.Interval := 0;
     ChangeTimer.Enabled := False;
     ChangeTimer.OnTimer := ChangeTimerOnTimer;
-    DriveHandle := INVALID_HANDLE_VALUE;
-    NotificationHandle := nil;
   end;
 end;
 
@@ -956,161 +926,6 @@ begin
   TerminateWatchThread(Drive);
 end;
 
-type
-  PDevBroadcastHdr = ^TDevBroadcastHdr;
-  TDevBroadcastHdr = record
-    dbch_size: DWORD;
-    dbch_devicetype: DWORD;
-    dbch_reserved: DWORD;
-  end;
-
-  PDevBroadcastVolume = ^TDevBroadcastVolume;
-  TDevBroadcastVolume = record
-    dbcv_size: DWORD;
-    dbcv_devicetype: DWORD;
-    dbcv_reserved: DWORD;
-    dbcv_unitmask: DWORD;
-    dbcv_flags: WORD;
-  end;
-
-  PDEV_BROADCAST_HANDLE = ^DEV_BROADCAST_HANDLE;
-  DEV_BROADCAST_HANDLE = record
-    dbch_size       : DWORD;
-    dbch_devicetype : DWORD;
-    dbch_reserved   : DWORD;
-    dbch_handle     : THandle;
-    dbch_hdevnotify : HDEVNOTIFY  ;
-    dbch_eventguid  : TGUID;
-    dbch_nameoffset : LongInt;
-    dbch_data       : Byte;
-  end;
-
-  PPItemIDList = ^PItemIDList;
-
-const
-  DBT_DEVTYP_HANDLE = $00000006;
-  DBT_CONFIGCHANGED = $0018;
-  DBT_DEVICEARRIVAL = $8000;
-  DBT_DEVICEQUERYREMOVE = $8001;
-  DBT_DEVICEREMOVEPENDING = $8003;
-  DBT_DEVICEREMOVECOMPLETE = $8004;
-  DBT_DEVTYP_VOLUME = $00000002;
-
-// WORKAROUND Declaration in Winapi.ShlObj.pas is wrong
-function SHChangeNotification_Lock(hChange: THandle; dwProcId: DWORD;
-  var PPidls: PPItemIDList; var plEvent: Longint): THANDLE; stdcall;
-external 'shell32.dll' name 'SHChangeNotification_Lock';
-
-procedure TDriveView.InternalWndProc(var Msg: TMessage);
-var
-  DeviceType: DWORD;
-  UnitMask: DWORD;
-  DeviceHandle: THandle;
-  Drive: Char;
-  DriveStatusPair: TDriveStatusPair;
-  PPIDL: PPItemIDList;
-  Event: LONG;
-  Lock: THandle;
-begin
-  with Msg do
-  begin
-    if Msg = WM_USER_SHCHANGENOTIFY then
-    begin
-      Lock := SHChangeNotification_Lock(wParam, lParam, PPIDL, Event);
-      try
-        if (Event = SHCNE_RENAMEFOLDER) or // = drive rename
-           (Event = SHCNE_MEDIAINSERTED) or // also bitlocker drive unlock (also sends SHCNE_UPDATEDIR)
-           (Event = SHCNE_MEDIAREMOVED) then
-        begin
-          ScheduleDriveRefresh;
-        end;
-      finally
-        SHChangeNotification_Unlock(Lock);
-      end;
-    end
-      else
-    if Msg = WM_DEVICECHANGE then
-    begin
-      if (wParam = DBT_CONFIGCHANGED) or
-         (wParam = DBT_DEVICEARRIVAL) or
-         (wParam = DBT_DEVICEREMOVECOMPLETE) then
-      begin
-        ScheduleDriveRefresh;
-      end
-        else
-      if (wParam = DBT_DEVICEQUERYREMOVE) or
-         (wParam = DBT_DEVICEREMOVEPENDING) then
-      begin
-        DeviceType := PDevBroadcastHdr(lParam)^.dbch_devicetype;
-        // This is specifically for VeraCrypt.
-        // For normal drives, see DBT_DEVTYP_HANDLE below
-        // (and maybe now that we have generic implementation, this specific code for VeraCrypt might not be needed anymore)
-        if DeviceType = DBT_DEVTYP_VOLUME then
-        begin
-          UnitMask := PDevBroadcastVolume(lParam)^.dbcv_unitmask;
-          Drive := FirstDrive;
-          while UnitMask > 0 do
-          begin
-            if UnitMask and $01 <> 0 then
-            begin
-              DriveRemoving(Drive);
-            end;
-            UnitMask := UnitMask shr 1;
-            Drive := Chr(Ord(Drive) + 1);
-          end;
-        end
-          else
-        if DeviceType = DBT_DEVTYP_HANDLE then
-        begin
-          DeviceHandle := PDEV_BROADCAST_HANDLE(lParam)^.dbch_handle;
-          for DriveStatusPair in FDriveStatus do
-            if DriveStatusPair.Value.DriveHandle = DeviceHandle then
-            begin
-              DriveRemoving(DriveStatusPair.Key);
-            end;
-        end;
-      end;
-    end
-      else
-    if Msg = WM_TIMER then
-    begin
-      CancelDriveRefresh;
-      try
-        //DriveInfo.Load;
-        RefreshRootNodes(dsAll or dvdsRereadAllways);
-        DoRefreshDrives(True);
-      except
-        Application.HandleException(Self);
-      end;
-    end;
-
-    Result := DefWindowProc(FInternalWindowHandle, Msg, wParam, lParam);
-  end;
-end;
-
-procedure TDriveView.DoRefreshDrives(Global: Boolean);
-begin
-  if Assigned(OnRefreshDrives) then
-    OnRefreshDrives(Self, Global);
-end;
-
-procedure TDriveView.CancelDriveRefresh;
-begin
-  KillTimer(FInternalWindowHandle, 1);
-end;
-
-procedure TDriveView.ScheduleDriveRefresh;
-begin
-  CancelDriveRefresh;
-  // Delay refreshing drives for a sec.
-  // Particularly with CD/DVD drives, if we query display name
-  // immediately after receiving DBT_DEVICEARRIVAL, we do not get media label.
-  // Actually one sec does not help usually, but we do not want to wait any longer,
-  // because we want to add USB drives asap.
-  // And this problem might be solved now by SHChangeNotifyRegister/SHCNE_RENAMEFOLDER.
-  SetTimer(FInternalWindowHandle, 1, MSecsPerSec, nil);
-end;
-
 procedure TDriveView.CreateWnd;
 var
   DriveStatus: TDriveStatus;
@@ -1396,7 +1211,7 @@ procedure TDriveView.Loaded;
 begin
   inherited;
   {Create the drive nodes:}
-  RefreshRootNodes(dsDisplayName or dvdsFloppy);
+  RefreshRootNodes(True);
   {Set the initial directory:}
   if (Length(FDirectory) > 0) and DirectoryExists(ApiPath(FDirectory)) then
     Directory := FDirectory;
@@ -1729,7 +1544,21 @@ begin
   end;
 end;
 
-procedure TDriveView.RefreshRootNodes(dsFlags: Integer);
+procedure TDriveView.DriveNotification(Notification: TDriveNotification; Drive: string);
+begin
+  case Notification of
+    dnRefresh:
+      RefreshRootNodes;
+
+    dnRemoving:
+      // Lame way to reduce rick, we modify the treee while it's being updated already.
+      // It might be better to post an update message to the message loop.
+      if WatchThreadActive(Drive) then
+        DriveRemoving(Drive);
+  end;
+end;
+
+procedure TDriveView.RefreshRootNodes(Floppy: Boolean);
 var
   Drives: TStrings;
   NewText: string;
@@ -1751,18 +1580,12 @@ begin
     begin
       Drive := Drives[Index];
       DriveStatus := GetDriveStatus(Drive);
-      if ((dsFlags and dvdsFloppy) <> 0) or DriveInfo.IsFixedDrive(Drive) then
+      if Floppy or DriveInfo.IsFixedDrive(Drive) then
       begin
         with DriveInfo.Get(Drive) do
         begin
           WasValid := Assigned(DriveStatus.RootNode);
         end;
-        if ((dsFlags and dvdsReReadAllways) = 0) and
-           (Length(DriveInfo.Get(Drive).DisplayName) > 0) then
-              dsFlags := dsFlags and (not dsDisplayName);
-
-        DriveInfo.ReadDriveStatus(Drive, dsFlags);
-
         with DriveInfo.Get(Drive), DriveStatus do
         begin
           if Valid then
@@ -1853,8 +1676,6 @@ begin
   begin
     Result := CreateDriveStatus;
     FDriveStatus.Add(Drive, Result);
-    RefreshRootNodes(dsAll or dvdsRereadAllways);
-    DoRefreshDrives(False);
   end;
 end; {GetDriveStatus}
 
@@ -2008,21 +1829,21 @@ begin {FindNodeToPath}
   HandleNeeded;
 
   Drive := DriveInfo.GetDriveKey(Path);
+  // Likely a network drive
   if (not Assigned(GetDriveStatus(Drive).RootNode)) and
-     // hidden or possibly recently un-hidden by other drive view (refresh is pending)
-     (DriveInfo.Get(Drive).Valid or DriveInfo.Get(Drive).ValidButHiddenByDrivePolicy) then
+     // Side effect of this is drive refresh that adds the network drive to the trees
+     DriveInfo.Get(Drive).ValidButHiddenByDrivePolicy then
   begin
-    if DriveInfo.Get(Drive).ValidButHiddenByDrivePolicy then
-      DriveInfo.OverrideDrivePolicy(Drive);
-
-    if DriveInfo.Get(Drive).Valid then
-    begin
-      CancelDriveRefresh; // cancel a possible pending refresh (see the previous comment)
-      RefreshRootNodes(dsAll or dvdsRereadAllways); // overkill and is likely already called by GetDriveStatus
-      DoRefreshDrives(False);
-    end;
+    // This refreshes the drives again
+    DriveInfo.OverrideDrivePolicy(Drive);
   end;
 
+  // if not assigned now, it must be that the drive already existed in DriveInfo, but it didn't make it to this view
+  // (possible a network drive opened in another panel before)
+  if not Assigned(GetDriveStatus(Drive).RootNode) then
+    // overkill, as we know exactly what drive to add (so not need to check all drives)
+    RefreshRootNodes;
+
   if Assigned(GetDriveStatus(Drive).RootNode) then
   begin
     if DriveInfo.IsRealDrive(Drive) then
@@ -2501,7 +2322,7 @@ begin
       DiscMonitor.SetDirectory(DriveInfo.GetDriveRoot(Drive));
       DiscMonitor.Open;
     end;
-    UpdateDriveNotifications(Drive);
+    SubscribeDriveNotifications(Drive);
   end;
 end; {CreateWatchThread}
 
@@ -2546,7 +2367,6 @@ begin
   begin
     DiscMonitor.Close;
   end;
-  UpdateDriveNotifications(Drive);
 end; {DirWatchChangeInvalid}
 
 procedure TDriveView.ChangeDetected(Sender: TObject; const Directory: string;
@@ -2603,58 +2423,11 @@ begin
   end;
 end; {ChangeTimerOnTimer}
 
-procedure TDriveView.UpdateDriveNotifications(Drive: string);
-var
-  NeedNotifications: Boolean;
-  Path: string;
-  DevBroadcastHandle: DEV_BROADCAST_HANDLE;
-  Size: Integer;
+procedure TDriveView.SubscribeDriveNotifications(Drive: string);
 begin
-  if DriveInfo.IsFixedDrive(Drive) then
-  begin
-    with GetDriveStatus(Drive) do
-    begin
-      NeedNotifications :=
-        WatchThreadActive(Drive) and
-        (DriveInfo.Get(Drive).DriveType <> DRIVE_REMOTE) and
-        DriveInfo.Get(Drive).DriveReady;
-
-      if NeedNotifications <> (DriveHandle <> INVALID_HANDLE_VALUE) then
-      begin
-        if NeedNotifications then
-        begin
-          Path := DriveInfo.GetDriveRoot(Drive);
-          DriveHandle :=
-            CreateFile(PChar(Path), GENERIC_READ, FILE_SHARE_READ or FILE_SHARE_WRITE, nil,
-            OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS or FILE_ATTRIBUTE_NORMAL, 0);
-          if DriveHandle <> INVALID_HANDLE_VALUE then
-          begin
-            Size := SizeOf(DevBroadcastHandle);
-            ZeroMemory(@DevBroadcastHandle, Size);
-            DevBroadcastHandle.dbch_size := Size;
-            DevBroadcastHandle.dbch_devicetype := DBT_DEVTYP_HANDLE;
-            DevBroadcastHandle.dbch_handle := DriveHandle;
-
-            NotificationHandle :=
-              RegisterDeviceNotification(FInternalWindowHandle, @DevBroadcastHandle, DEVICE_NOTIFY_WINDOW_HANDLE);
-            if NotificationHandle = nil then
-            begin
-              CloseHandle(DriveHandle);
-              DriveHandle := INVALID_HANDLE_VALUE;
-            end;
-          end;
-        end
-          else
-        begin
-          UnregisterDeviceNotification(NotificationHandle);
-          NotificationHandle := nil;
-
-          CloseHandle(DriveHandle);
-          DriveHandle := INVALID_HANDLE_VALUE;
-        end;
-      end;
-    end;
-  end;
+  // As previously, subscribe drive notification only once the drive is at least once visited.
+  // Shouldn't do much harm to subscribe always, but just in case.
+  DriveInfo.SubscribeDriveNotifications(Drive);
 end;
 
 procedure TDriveView.StartWatchThread;
@@ -2674,7 +2447,7 @@ begin
     if Assigned(DiscMonitor) and not DiscMonitor.Enabled then
       DiscMonitor.Enabled := True;
   end;
-  UpdateDriveNotifications(Drive);
+  SubscribeDriveNotifications(Drive);
 end; {StartWatchThread}
 
 procedure TDriveView.StopWatchThread;
@@ -2687,8 +2460,6 @@ begin
     with GetDriveStatus(Drive) do
       if Assigned(DiscMonitor) then
         DiscMonitor.Enabled := False;
-
-    UpdateDriveNotifications(Drive);
   end;
 end; {StopWatchThread}
 
@@ -2711,8 +2482,6 @@ begin
       DiscMonitor.Free;
       DiscMonitor := nil;
     end;
-
-  UpdateDriveNotifications(Drive);
 end; {StopWatchThread}
 
 procedure TDriveView.StartAllWatchThreads;
@@ -2732,7 +2501,7 @@ begin
         if Assigned(DiscMonitor) and (not DiscMonitor.Active) then
         begin
           DiscMonitor.Open;
-          UpdateDriveNotifications(DriveStatusPair.Key);
+          SubscribeDriveNotifications(DriveStatusPair.Key);
         end;
       end;
 
@@ -2759,7 +2528,6 @@ begin
       if Assigned(DiscMonitor) then
       begin
         DiscMonitor.Close;
-        UpdateDriveNotifications(DriveStatusPair.Key);
       end;
     end;
 end; {StopAllWatchThreads}
@@ -2885,7 +2653,7 @@ begin
   if ShowIt = FShowVolLabel then
     Exit;
   FShowVolLabel := ShowIt;
-  RefreshRootNodes(dvdsFloppy);
+  RefreshRootNodes(True);
 end; {SetShowVolLabel}
 
 procedure TDriveView.DisplayContextMenu(Node: TTreeNode; Point: TPoint);

+ 392 - 22
source/packages/filemng/IEDriveInfo.pas

@@ -31,7 +31,7 @@ interface
 
 uses
   Windows, Registry, SysUtils, Classes, ComCtrls, ShellApi, ShlObj, CommCtrl, Forms,
-  BaseUtils, System.Generics.Collections, Vcl.Graphics;
+  BaseUtils, System.Generics.Collections, Vcl.Graphics, Winapi.Messages;
 
 const
   {Flags used by TDriveInfo.ReadDriveStatus and TDriveView.RefreshRootNodes:}
@@ -39,12 +39,15 @@ const
   dsImageIndex = 1;   {Fetch imageindex, if not allready fetched}
   dsSize = 2;         {Fetch disk size and serialnumber}
   dsDisplayName = 4;  {Fetch drives displayname}
-  dsAll = dsImageIndex or dsSize or dsDisplayName;
+  dsSynchronous = dsImageIndex or dsDisplayName;
+  dsAll = dsSynchronous or dsSize;
   FirstDrive          = 'A';
   SystemDrive         = 'C';
   LastDrive           = 'Z';
   FirstSpecialFolder  = CSIDL_DESKTOP;
   LastSpecialFolder   = CSIDL_PRINTHOOD;
+  WM_USER_SHCHANGENOTIFY = WM_USER + $2000 + 13;
+  WM_DRIVEINFO_PROCESS = WM_USER + $2000 + 18;
 
 type
   TDriveInfoRec = class
@@ -59,6 +62,9 @@ type
     DriveSerial : DWORD;       {Serial number of the drive}
     Size        : Int64;       {Drivesize}
     ImageIndex  : Integer;     {Drive imageIndex}
+    DriveHandle: THandle;
+    NotificationHandle: HDEVNOTIFY;
+    SubscribeDriveNotifications: Boolean;
   end;
 
   TRealDrive = char;
@@ -72,6 +78,9 @@ type
     PIDL: PItemIDList;
   end;
 
+  TDriveNotification = (dnRefresh, dnRemoving);
+  TDriveNotificationEvent = procedure(Notification: TDriveNotification; Drive: string) of object;
+
   TDriveInfo = class(TObject)
   private
     FData: TObjectDictionary<string, TDriveInfoRec>;
@@ -82,6 +91,8 @@ type
     FHonorDrivePolicy: Integer;
     FUseABDrives: Boolean;
     FLoaded: Boolean;
+    FHandlers: TList<TDriveNotificationEvent>;
+    FChangeNotify: ULONG;
     function GetFolder(Folder: TSpecialFolder): PSpecialFolderRec;
     procedure ReadDriveBasicStatus(Drive: string);
     procedure ResetDrive(Drive: string);
@@ -92,6 +103,17 @@ type
     function GetDriveBitMask(Drive: string): Integer;
     function DoAnyValidPath(DriveType: Integer; CanBeHidden: Boolean; var Path: string): Boolean;
     function ReadDriveMask(Reg: TRegistry; ValueName: string): DWORD;
+    procedure ScheduleDriveRefresh;
+    procedure CancelDriveRefresh;
+    procedure InternalWndProc(var Msg: TMessage);
+    procedure InvokeHandlers(DriveNotification: TDriveNotification; Drive: string);
+    procedure UpdateDriveNotifications(Drive: string);
+    procedure UpdateDrivesNotifications;
+    procedure ProcessThreadResults;
+    constructor Create;
+    procedure ReadAsynchronous;
+    procedure DoReadDriveStatus(Drive: string; Flags: Integer);
+    procedure DriveRemoving(Drive: string);
 
   public
     function Get(Drive: string): TDriveInfoRec;
@@ -113,8 +135,12 @@ type
     property FirstFixedDrive: Char read GetFirstFixedDrive;
     property UseABDrives: Boolean read FUseABDrives write FUseABDrives;
 
-    constructor Create;
     destructor Destroy; override;
+
+    procedure AddHandler(Handler: TDriveNotificationEvent);
+    procedure RemoveHandler(Handler: TDriveNotificationEvent);
+    procedure DriveRefresh;
+    procedure SubscribeDriveNotifications(Drive: string);
   end;
 
 function GetShellFileName(const Name: string): string; overload;
@@ -140,7 +166,9 @@ type
   PRGBQuadArray = ^TRGBQuadArray;    // From graphics.pas
   TRGBQuadArray = array[Byte] of TRGBQuad;  // From graphics.pas
 
+// Globals so that we do not have to fear that thread run after DriveInfo is released
 var
+  InternalWindowHandle: HWND;
   ThreadLock: TRTLCriticalSection;
   ReadyDrives: string;
 
@@ -198,18 +226,19 @@ begin
   FUseABDrives := True;
   FLoaded := False;
   FData := TObjectDictionary<string, TDriveInfoRec>.Create([doOwnsValues]);
+  FHandlers := TList<TDriveNotificationEvent>.Create;
+  FChangeNotify := 0;
 end; {TDriveInfo.Create}
 
 destructor TDriveInfo.Destroy;
 begin
+  Assert(FHandlers.Count = 0);
+  FHandlers.Free;
   FData.Free;
   inherited;
 end; {TDriveInfo.Destroy}
 
 procedure TDriveInfo.NeedData;
-var
-  I: Integer;
-  Drive: Char;
 begin
   if not FLoaded then
   begin
@@ -217,6 +246,14 @@ begin
     FLoaded := True;
   end;
 
+  ProcessThreadResults;
+end;
+
+procedure TDriveInfo.ProcessThreadResults;
+var
+  I: Integer;
+  Drive: Char;
+begin
   EnterCriticalSection(ThreadLock);
   try
     for I := 1 to Length(ReadyDrives) do
@@ -224,6 +261,8 @@ begin
       Drive := ReadyDrives[I];
       Assert(FData.ContainsKey(Drive));
       FData[Drive].DriveReady := True;
+      UpdateDriveNotifications(Drive);
+      AppLog(Format('Drive "%s" is ready', [Drive]))
     end;
     ReadyDrives := '';
   finally
@@ -381,11 +420,12 @@ begin
   with FData[Drive] do
   begin
     DriveType := Windows.GetDriveType(PChar(GetDriveRoot(Drive)));
-    DriveBitMask := GetDriveBitMask(Drive);
+    if IsRealDrive(Drive) then DriveBitMask := GetDriveBitMask(Drive)
+      else DriveBitMask := 0;
     InaccessibleByDrivePolicy :=
-      IsRealDrive(Drive) and ((HonorDrivePolicy and 2) <> 0) and ((DriveBitMask and FNoViewOnDrive) <> 0);
+      ((HonorDrivePolicy and 2) <> 0) and ((DriveBitMask and FNoViewOnDrive) <> 0);
     HiddenByDrivePolicy :=
-      IsRealDrive(Drive) and ((HonorDrivePolicy and 1) <> 0) and ((DriveBitMask and FNoDrives) <> 0);
+      ((HonorDrivePolicy and 1) <> 0) and ((DriveBitMask and FNoDrives) <> 0);
     ValidDriveType :=
       (DriveType in [DRIVE_REMOVABLE, DRIVE_FIXED, DRIVE_CDROM, DRIVE_RAMDISK, DRIVE_REMOTE]) and
       (not InaccessibleByDrivePolicy);
@@ -404,6 +444,9 @@ begin
     DriveSerial := 0;
     Size := -1;
     ImageIndex := 0;
+    DriveHandle := INVALID_HANDLE_VALUE;
+    NotificationHandle := nil;
+    SubscribeDriveNotifications := False;
   end;
 end;
 
@@ -431,7 +474,6 @@ var
   Drive: TRealDrive;
   Reg: TRegistry;
   Folder: TSpecialFolder;
-  Drives: string;
 begin
   AppLog('Loading drives');
   Reg := TRegistry.Create;
@@ -451,21 +493,35 @@ begin
 
   FDesktop := nil;
 
-  Drives := EmptyStr;
   for Drive := FirstDrive to LastDrive do
   begin
-    if AddDrive(Drive).Valid then
+    AddDrive(Drive);
+  end;
+
+  ReadAsynchronous;
+
+  for Folder := Low(FFolders) to High(FFolders) do
+    FFolders[Folder].Valid := False;
+end;
+
+procedure TDriveInfo.ReadAsynchronous;
+var
+  Drive: TRealDrive;
+  Drives: string;
+begin
+  for Drive := FirstDrive to LastDrive do
+  begin
+    // Not using Get as that would recurse into Load
+    if FData[Drive].Valid then
       Drives := Drives + Drive;
   end;
+    TDriveInfoThread.Create(Drives);
 
   if Length(Drives) > 0 then
   begin
-    AppLog(Format('Drives found: %s', [Drives]));
+    AppLog(Format('Drives to check in the background: %s', [Drives]));
     TDriveInfoThread.Create(Drives);
   end;
-
-  for Folder := Low(FFolders) to High(FFolders) do
-    FFolders[Folder].Valid := False;
 end;
 
 function TDriveInfo.AddDrive(Drive: string): TDriveInfoRec;
@@ -473,7 +529,10 @@ begin
   Result := TDriveInfoRec.Create;
   FData.Add(Drive, Result);
   ResetDrive(Drive);
-  ReadDriveBasicStatus(Drive);
+  if IsFixedDrive(Drive) or (not IsRealDrive(Drive)) then // not floppy
+    DoReadDriveStatus(Drive, dsSynchronous)
+  else
+    ReadDriveBasicStatus(Drive);
 end;
 
 function TDriveInfo.GetImageIndex(Drive: string): Integer;
@@ -528,17 +587,26 @@ function TDriveInfo.Get(Drive: string): TDriveInfoRec;
 begin
   NeedData;
 
-  // We might want to wait for ReadyDrives to beempty before returning
+  // We might want to wait for FReadyDrives to be empty before returning
   // (or even better do that only in DriveReady getter)
 
   if not FData.TryGetValue(Drive, Result) then
   begin
     Assert(IsUncPath(Drive));
     Result := AddDrive(Drive);
+    DriveRefresh;
   end;
 end; {TDriveInfo.GetData}
 
 procedure TDriveInfo.ReadDriveStatus(Drive: string; Flags: Integer);
+begin
+  // Among other, this makes sure the pending drive-ready status from the background thread are collected,
+  // before we overwrite it with fresh status here.
+  NeedData;
+  DoReadDriveStatus(Drive, Flags);
+end;
+
+procedure TDriveInfo.DoReadDriveStatus(Drive: string; Flags: Integer);
 var
   ErrorMode: Word;
   FileInfo: TShFileInfo;
@@ -554,9 +622,6 @@ var
   DriveInfoRec: TDriveInfoRec;
   S: string;
 begin
-  // Among other, this makes sure the pending drive-ready status from the background thread are collected,
-  // before we overwrite it with fresh status here.
-  NeedData;
   if not Assigned(FDesktop) then
     SHGetDesktopFolder(FDesktop);
 
@@ -610,6 +675,10 @@ begin
           begin
             DriveSerial := 0;
           end;
+          // Particularly when removing drive fails (as other app has it locked), we end up with not monitoring the
+          // drive. When the drive is visited again in panel, it calls into here, and we take the opportunity
+          // to resume monitoring
+          UpdateDriveNotifications(Drive);
         finally
           { restore old error mode }
           SetErrorMode(ErrorMode);
@@ -684,10 +753,304 @@ begin
   Assert(FData[Drive].ValidButHiddenByDrivePolicy);
   Mask := (not GetDriveBitMask(Drive));
   FNoDrives := FNoDrives and Mask;
-  ReadDriveBasicStatus(Drive);
+  ReadDriveStatus(Drive, dsAll);
   Assert(FData[Drive].Valid);
+  DriveRefresh;
 end;
 
+procedure TDriveInfo.AddHandler(Handler: TDriveNotificationEvent);
+var
+  ChangeNotifyEntry: TSHChangeNotifyEntry;
+  Dummy: string;
+begin
+  if not FHandlers.Contains(Handler) then
+  begin
+    FHandlers.Add(Handler);
+
+    if FHandlers.Count = 1 then
+    begin
+      // Source: petr.solin 2022-02-25
+      if SpecialFolderLocation(CSIDL_DESKTOP, Dummy, ChangeNotifyEntry.pidl) then
+      begin
+        ChangeNotifyEntry.fRecursive := False;
+
+        FChangeNotify :=
+          SHChangeNotifyRegister(
+            InternalWindowHandle, SHCNRF_ShellLevel or SHCNRF_NewDelivery,
+            SHCNE_RENAMEFOLDER or SHCNE_MEDIAINSERTED or SHCNE_MEDIAREMOVED,
+            WM_USER_SHCHANGENOTIFY, 1, ChangeNotifyEntry);
+      end;
+      UpdateDrivesNotifications;
+    end;
+  end;
+end;
+
+procedure TDriveInfo.RemoveHandler(Handler: TDriveNotificationEvent);
+begin
+  if (FHandlers.Remove(Handler) >= 0) and (FHandlers.Count = 0) then
+  begin
+    if FChangeNotify <> 0 then
+    begin
+      SHChangeNotifyDeregister(FChangeNotify);
+      FChangeNotify := 0;
+    end;
+    UpdateDrivesNotifications;
+  end;
+end;
+
+procedure TDriveInfo.InvokeHandlers(DriveNotification: TDriveNotification; Drive: string);
+var
+  Handler: TDriveNotificationEvent;
+begin
+  for Handler in FHandlers do
+    Handler(DriveNotification, Drive);
+end;
+
+type
+  PDevBroadcastHdr = ^TDevBroadcastHdr;
+  TDevBroadcastHdr = record
+    dbch_size: DWORD;
+    dbch_devicetype: DWORD;
+    dbch_reserved: DWORD;
+  end;
+
+  PDevBroadcastVolume = ^TDevBroadcastVolume;
+  TDevBroadcastVolume = record
+    dbcv_size: DWORD;
+    dbcv_devicetype: DWORD;
+    dbcv_reserved: DWORD;
+    dbcv_unitmask: DWORD;
+    dbcv_flags: WORD;
+  end;
+
+  PDEV_BROADCAST_HANDLE = ^DEV_BROADCAST_HANDLE;
+  DEV_BROADCAST_HANDLE = record
+    dbch_size       : DWORD;
+    dbch_devicetype : DWORD;
+    dbch_reserved   : DWORD;
+    dbch_handle     : THandle;
+    dbch_hdevnotify : HDEVNOTIFY  ;
+    dbch_eventguid  : TGUID;
+    dbch_nameoffset : LongInt;
+    dbch_data       : Byte;
+  end;
+
+  PPItemIDList = ^PItemIDList;
+
+const
+  DBT_DEVTYP_HANDLE = $00000006;
+  DBT_CONFIGCHANGED = $0018;
+  DBT_DEVICEARRIVAL = $8000;
+  DBT_DEVICEQUERYREMOVE = $8001;
+  DBT_DEVICEREMOVEPENDING = $8003;
+  DBT_DEVICEREMOVECOMPLETE = $8004;
+  DBT_DEVTYP_VOLUME = $00000002;
+
+// WORKAROUND Declaration in Winapi.ShlObj.pas is wrong
+function SHChangeNotification_Lock(hChange: THandle; dwProcId: DWORD;
+  var PPidls: PPItemIDList; var plEvent: Longint): THANDLE; stdcall;
+external 'shell32.dll' name 'SHChangeNotification_Lock';
+
+procedure TDriveInfo.InternalWndProc(var Msg: TMessage);
+var
+  DeviceType: DWORD;
+  UnitMask: DWORD;
+  DeviceHandle: THandle;
+  Drive: Char;
+  PPIDL: PPItemIDList;
+  Event: LONG;
+  Lock: THandle;
+  DrivePair: TPair<string, TDriveInfoRec>;
+begin
+  with Msg do
+  begin
+    if Msg = WM_USER_SHCHANGENOTIFY then
+    begin
+      Lock := SHChangeNotification_Lock(wParam, lParam, PPIDL, Event);
+      try
+        if (Event = SHCNE_RENAMEFOLDER) or // = drive rename
+           (Event = SHCNE_MEDIAINSERTED) or // also bitlocker drive unlock (also sends SHCNE_UPDATEDIR)
+           (Event = SHCNE_MEDIAREMOVED) then
+        begin
+          ScheduleDriveRefresh;
+        end;
+      finally
+        SHChangeNotification_Unlock(Lock);
+      end;
+    end
+      else
+    // from RegisterDeviceNotification
+    if Msg = WM_DEVICECHANGE then
+    begin
+      if (wParam = DBT_CONFIGCHANGED) or
+         (wParam = DBT_DEVICEARRIVAL) or
+         (wParam = DBT_DEVICEREMOVECOMPLETE) then
+      begin
+        ScheduleDriveRefresh;
+      end
+        else
+      if (wParam = DBT_DEVICEQUERYREMOVE) or
+         (wParam = DBT_DEVICEREMOVEPENDING) then
+      begin
+        DeviceType := PDevBroadcastHdr(lParam)^.dbch_devicetype;
+        // This is specifically for VeraCrypt.
+        // For normal drives, see DBT_DEVTYP_HANDLE below
+        // (and maybe now that we have generic implementation, this specific code for VeraCrypt might not be needed anymore)
+        if DeviceType = DBT_DEVTYP_VOLUME then
+        begin
+          UnitMask := PDevBroadcastVolume(lParam)^.dbcv_unitmask;
+          Drive := FirstDrive;
+          while UnitMask > 0 do
+          begin
+            if UnitMask and $01 <> 0 then
+            begin
+              DriveRemoving(Drive);
+            end;
+            UnitMask := UnitMask shr 1;
+            Drive := Chr(Ord(Drive) + 1);
+          end;
+        end
+          else
+        if DeviceType = DBT_DEVTYP_HANDLE then
+        begin
+          DeviceHandle := PDEV_BROADCAST_HANDLE(lParam)^.dbch_handle;
+          for DrivePair in FData do
+            if DrivePair.Value.DriveHandle = DeviceHandle then
+            begin
+              DriveRemoving(DrivePair.Key);
+            end;
+        end;
+      end;
+    end
+      else
+    if Msg = WM_TIMER then
+    begin
+      CancelDriveRefresh;
+      try
+        for Drive := FirstFixedDrive to LastDrive do
+          ReadDriveStatus(Drive, dsSynchronous);
+        ReadAsynchronous;
+        DriveRefresh;
+      except
+        Application.HandleException(Self);
+      end;
+    end
+      else
+    if Msg = WM_DRIVEINFO_PROCESS then
+    begin
+      ProcessThreadResults;
+    end;
+
+    Result := DefWindowProc(InternalWindowHandle, Msg, wParam, lParam);
+  end;
+end;
+
+procedure TDriveInfo.DriveRemoving(Drive: string);
+begin
+  FData[Drive].DriveReady := False;
+  UpdateDriveNotifications(Drive);
+  AppLog(Format('Removing drive "%s"', [Drive]));
+  InvokeHandlers(dnRemoving, Drive);
+end;
+
+procedure TDriveInfo.CancelDriveRefresh;
+begin
+  KillTimer(InternalWindowHandle, 1);
+end;
+
+procedure TDriveInfo.ScheduleDriveRefresh;
+begin
+  CancelDriveRefresh;
+  // Delay refreshing drives for a sec.
+  // Particularly with CD/DVD drives, if we query display name
+  // immediately after receiving DBT_DEVICEARRIVAL, we do not get media label.
+  // Actually one sec does not help usually, but we do not want to wait any longer,
+  // because we want to add USB drives asap.
+  // And this problem might be solved now by SHChangeNotifyRegister/SHCNE_RENAMEFOLDER.
+  SetTimer(InternalWindowHandle, 1, MSecsPerSec, nil);
+end;
+
+procedure TDriveInfo.UpdateDriveNotifications(Drive: string);
+var
+  NeedNotifications: Boolean;
+  Path: string;
+  DevBroadcastHandle: DEV_BROADCAST_HANDLE;
+  Size: Integer;
+  DriveInfoRec: TDriveInfoRec;
+begin
+  if IsFixedDrive(Drive) then
+  begin
+    // Not using Get to avoid recursion
+    DriveInfoRec := FData[Drive];
+    NeedNotifications :=
+      (FHandlers.Count > 0) and
+      (DriveInfoRec.DriveType <> DRIVE_REMOTE) and
+      DriveInfoRec.DriveReady and
+      DriveInfoRec.SubscribeDriveNotifications;
+
+    if NeedNotifications <> (DriveInfoRec.DriveHandle <> INVALID_HANDLE_VALUE) then
+    begin
+      Path := GetDriveRoot(Drive);
+      if NeedNotifications then
+      begin
+        DriveInfoRec.DriveHandle :=
+          CreateFile(PChar(Path), GENERIC_READ, FILE_SHARE_READ or FILE_SHARE_WRITE, nil,
+          OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS or FILE_ATTRIBUTE_NORMAL, 0);
+        if DriveInfoRec.DriveHandle <> INVALID_HANDLE_VALUE then
+        begin
+          Size := SizeOf(DevBroadcastHandle);
+          ZeroMemory(@DevBroadcastHandle, Size);
+          DevBroadcastHandle.dbch_size := Size;
+          DevBroadcastHandle.dbch_devicetype := DBT_DEVTYP_HANDLE;
+          DevBroadcastHandle.dbch_handle := DriveInfoRec.DriveHandle;
+
+          DriveInfoRec.NotificationHandle :=
+            RegisterDeviceNotification(InternalWindowHandle, @DevBroadcastHandle, DEVICE_NOTIFY_WINDOW_HANDLE);
+          if DriveInfoRec.NotificationHandle <> nil then
+          begin
+            AppLog(Format('Registered drive notification for "%s"', [Path]));
+          end
+            else
+          begin
+            CloseHandle(DriveInfoRec.DriveHandle);
+            DriveInfoRec.DriveHandle := INVALID_HANDLE_VALUE;
+          end;
+        end;
+      end
+        else
+      begin
+        AppLog(Format('Unregistered drive notification for "%s"', [Path]));
+        UnregisterDeviceNotification(DriveInfoRec.NotificationHandle);
+        DriveInfoRec.NotificationHandle := nil;
+
+        CloseHandle(DriveInfoRec.DriveHandle);
+        DriveInfoRec.DriveHandle := INVALID_HANDLE_VALUE;
+      end;
+    end;
+  end;
+end;
+
+procedure TDriveInfo.UpdateDrivesNotifications;
+var
+  Drive: string;
+begin
+  for Drive in FData.Keys do
+    UpdateDriveNotifications(Drive);
+end;
+
+procedure TDriveInfo.DriveRefresh;
+begin
+  InvokeHandlers(dnRefresh, '');
+end;
+
+procedure TDriveInfo.SubscribeDriveNotifications(Drive: string);
+begin
+  Get(Drive).SubscribeDriveNotifications := True;
+  UpdateDriveNotifications(Drive);
+end;
+
+// ===================
+
 function GetShellFileName(const Name: string): string;
 var
   SFI: TSHFileInfo;
@@ -866,13 +1229,20 @@ end;
 initialization
   InitializeCriticalSection(ThreadLock);
   if not Assigned(DriveInfo) then
+  begin
     DriveInfo := TDriveInfo.Create;
+    InternalWindowHandle := Classes.AllocateHWnd(DriveInfo.InternalWndProc);
+  end;
 
 finalization
   if Assigned(DriveInfo) then
   begin
+    EnterCriticalSection(ThreadLock);
+    Classes.DeallocateHWnd(InternalWindowHandle);
+    InternalWindowHandle := 0;
     DriveInfo.Free;
     DriveInfo := nil;
+    LeaveCriticalSection(ThreadLock);
   end;
   DeleteCriticalSection(ThreadLock);
 end.

+ 2 - 1
source/windows/CustomWinConfiguration.h

@@ -15,11 +15,12 @@
 // CM_DPICHANGED + 10 (packages/my/PasTools.pas)
 #define WM_WANTS_MOUSEWHEEL_INACTIVE (WM_WINSCP_USER + 11)
 #define WM_WANTS_SCREEN_TIPS (WM_WINSCP_USER + 12)
-// WM_USER_SHCHANGENOTIFY + 13 (packages/filemng/DriveView.pas)
+// WM_USER_SHCHANGENOTIFY + 13 (packages/filemng/IEDriveInfo.pas)
 // WM_PASTE_FILES + 14 (forms/CustomScpExplorer.cpp)
 #define WM_IS_HIDDEN (WM_WINSCP_USER + 15)
 // WM_USER_INVALIDATEITEM + 16 (packages/filemng/DirView.pas)
 #define WM_QUEUE_CALLBACK (WM_WINSCP_USER + 17)
+// WM_DRIVEINFO_PROCESS + 18 (packages/filemng/IEDriveInfo.pas)
 //---------------------------------------------------------------------------
 #define C(Property) (Property != rhc.Property) ||
 struct TSynchronizeChecklistConfiguration

+ 3 - 0
source/windows/WinMain.cpp

@@ -1315,6 +1315,9 @@ int __fastcall Execute()
                 // from now on, we do not support runtime interface change
                 CustomWinConfiguration->CanApplyInterfaceImmediately = false;
                 AddStartupSequence(L"A");
+                // Start loading drives on the background asap,
+                // to prevent unnecessary refreshes after the explorer opens
+                DriveInfo->NeedData();
                 TCustomScpExplorerForm * ScpExplorer = CreateScpExplorer();
                 AddStartupSequence(L"E");
                 CustomWinConfiguration->AppliedInterface = CustomWinConfiguration->Interface;