This commit is contained in:
EH 2026-06-19 19:20:08 -06:00 committed by GitHub
commit 507ad20e19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69 additions and 24 deletions

View file

@ -103,6 +103,7 @@ type
FPublicKey, FPrivateKey: String;
function Connect: Boolean; override;
function DataSocket: Boolean; override;
procedure ApplyKeepAlive;
function ListMachine(Directory: String): Boolean;
procedure DoStatus(Response: Boolean; const Value: string); override;
procedure OnSocketStatus(Sender: TObject; Reason: THookSocketReason; const Value: String);
@ -135,7 +136,7 @@ type
public
property Encoding: String write SetEncoding;
property UseAllocate: Boolean write FUseAllocate;
property TcpKeepAlive: Boolean write FTcpKeepAlive;
property TcpKeepAlive: Boolean read FTcpKeepAlive write FTcpKeepAlive;
property PublicKey: String read FPublicKey write FPublicKey;
property PrivateKey: String read FPrivateKey write FPrivateKey;
property ShowHidden: Boolean read FShowHidden write FShowHidden;
@ -427,21 +428,51 @@ begin
end;
function TFTPSendEx.Connect: Boolean;
begin
Result:= inherited Connect;
if Result then
begin
LogProc(PluginNumber, MSGTYPE_CONNECT, nil);
ApplyKeepAlive;
end;
end;
{ Enable TCP keep-alive on the control socket so that connections survive long
idle periods: the probes keep NAT/firewall mappings open, and a peer that
silently went away is detected (the socket errors) instead of the link
appearing alive until the next operation blocks. Called for both the FTP
control connection and the SSH connection (see TScpSend.Connect). }
procedure TFTPSendEx.ApplyKeepAlive;
{$IFDEF UNIX}
const
// From netinet/tcp.h (Linux). Defined locally to avoid an extra dependency.
TCP_KEEPIDLE_ = 4; // begin probing after N seconds of idle
TCP_KEEPINTVL_ = 5; // seconds between probes
TCP_KEEPCNT_ = 6; // dropped after this many unanswered probes
{$ENDIF}
var
Option: Cardinal = 1;
Message: UnicodeString;
{$IFDEF UNIX}
KeepIdle: Integer = 30;
KeepIntvl: Integer = 10;
KeepCnt: Integer = 3;
{$ENDIF}
begin
Result:= inherited Connect;
if Result then LogProc(PluginNumber, MSGTYPE_CONNECT, nil);
// Apply TcpKeepAlive option
if FTcpKeepAlive and Result then
if not FTcpKeepAlive then Exit;
if SetSockOpt(FSock.Socket, SOL_SOCKET, SO_KEEPALIVE, @Option, SizeOf(Option)) <> 0 then
begin
if SetSockOpt(FSock.Socket, SOL_SOCKET, SO_KEEPALIVE, @Option, SizeOf(Option)) <> 0 then
begin
Message := UTF8ToUTF16(FSock.GetErrorDesc(synsock.WSAGetLastError));
LogProc(PluginNumber, msgtype_importanterror, PWideChar('CSOCK ERROR ' + Message));
end;
Message := UTF8ToUTF16(FSock.GetErrorDesc(synsock.WSAGetLastError));
LogProc(PluginNumber, msgtype_importanterror, PWideChar('CSOCK ERROR ' + Message));
Exit;
end;
{$IFDEF UNIX}
// Probe idle connections within ~1 minute instead of the OS default (~2 h),
// so a dropped link is noticed on the next refresh and NAT mappings survive.
SetSockOpt(FSock.Socket, IPPROTO_TCP, TCP_KEEPIDLE_, @KeepIdle, SizeOf(KeepIdle));
SetSockOpt(FSock.Socket, IPPROTO_TCP, TCP_KEEPINTVL_, @KeepIntvl, SizeOf(KeepIntvl));
SetSockOpt(FSock.Socket, IPPROTO_TCP, TCP_KEEPCNT_, @KeepCnt, SizeOf(KeepCnt));
{$ENDIF}
end;
function TFTPSendEx.DataSocket: Boolean;

View file

@ -320,24 +320,28 @@ begin
if I >= 0 then
begin
FtpSend:= TFTPSendEx(ActiveConnectionList.Objects[I]);
if FtpSend.NetworkError then
//Server closed the connection, or network error occurred, or whatever else.
//Attempt to reconnect and execute login sequence
// The link is dead: the server closed it, a network error occurred, or
// it was dropped while the tab sat idle. Discard the stale connection
// and open a fresh one through the normal path below. The cached
// password is reused when available, otherwise the user is prompted
// again. This way a simple refresh of the view fully reconnects and
// re-lists whatever folder is currently displayed in the tab.
begin
LogProc(PluginNumber, msgtype_details, PWideChar('Network error detected, attempting to reconnect...'));
I:= ConnectionList.IndexOf(ConnectionName);
if I >= 0 then
LogProc(PluginNumber, msgtype_details, PWideChar('Connection lost, reconnecting...'));
// Quick connections have no stored entry, so they cannot be rebuilt.
if ConnectionList.IndexOf(ConnectionName) < 0 then
begin
Connection := TConnection(ConnectionList.Objects[I]);
if not FtpLogin(Connection, FtpSend) then
begin
RequestProc(PluginNumber, RT_MsgOK, nil, 'Connection lost, unable to reconnect!', nil, MAX_PATH);
Exit;
end;
RequestProc(PluginNumber, RT_MsgOK, nil, 'Connection lost, unable to reconnect!', nil, MAX_PATH);
Exit;
end;
ActiveConnectionList.Delete(I);
FreeAndNil(FtpSend);
Result:= FtpConnect(ConnectionName, FtpSend);
Exit;
end;
Result:= True;
end
else

View file

@ -551,6 +551,11 @@ begin
end;
DoStatus(False, 'Authentication succeeded');
// Keep the SSH connection alive across long idle periods. TScpSend
// overrides Connect and never runs TFTPSendEx.Connect, so without this
// the SSH socket got no keep-alive at all (unlike plain FTP).
ApplyKeepAlive;
finally
if not Result then begin
libssh2_session_free(FSession);
@ -637,7 +642,12 @@ end;
function TScpSend.NetworkError: Boolean;
begin
Result:= FSock.CanRead(0) and (libssh2_session_last_errno(FSession) <> 0);
// The link is considered dead if libssh2 already recorded a fatal error, or
// if the idle socket has unexpected pending data (a peer FIN/RST, or a
// keep-alive probe that found the connection gone). The previous code
// required BOTH conditions, so a connection that the server closed while the
// tab was idle went unnoticed and never reconnected.
Result:= (libssh2_session_last_errno(FSession) <> 0) or FSock.CanRead(0);
end;
procedure TScpSend.CloneTo(AValue: TFTPSendEx);