В этой статье мы рассмотрим решение проблем, связанных с асинхронным выполнением команд через SSH в приложениях Delphi, используя библиотеку ssh-pascal. Основное внимание будет уделено проблеме таймаутов и способам повышения производительности при работе с большими объемами данных.
Проблема синхронного выполнения команд
Как отмечал пользователь dummzeuch, стандартный метод ISshExec.Exec в библиотеке ssh-pascal работает синхронно, что создает две основные проблемы:
Приложение должно ждать завершения выполнения команды, что может занять значительное время (до 30 минут)
Возникают проблемы с таймаутами при работе с большими файлами
// Стандартный синхронный вызов
procedure TForm1.Button1Click(Sender: TObject);
var
Output, Error: string;
ExitCode: Integer;
begin
FSshExec.Exec('checksum_command', Output, Error, ExitCode);
// Код здесь не выполнится, пока команда не завершится
Memo1.Lines.Text := Output;
end;
Решение: асинхронный подход с коллбэками
Сообщество предложило несколько решений, включая модификацию исходного кода библиотеки для поддержки асинхронного получения данных. Вот один из вариантов реализации:
Вот как может выглядеть полная реализация асинхронного обработчика команд:
procedure TSshExec.Exec(const Command: string; var Output, ErrOutput: string;
var ExitCode: Integer);
var
Channel: PLIBSSH2_CHANNEL;
ReadBuffer, OutBuffer, ErrBuffer: TBytes;
StdStream, ErrStream: TBytesStream;
TimeVal: TTimeVal;
ReadFds: TFdSet;
BytesRead: ssize_t;
ReturnCode: Integer;
OldBlocking: Boolean;
begin
if FSession.SessionState <> session_Authorized then
raise ESshError.CreateRes(@Err_SessionAuth);
FCancelled := False;
Channel := libssh2_channel_open_session(FSession.Addr);
if Channel = nil then
CheckLibSsh2Result(libssh2_session_last_errno(FSession.Addr), FSession,
'libssh2_channel_open_session');
TimeVal.tv_sec := 1;
TimeVal.tv_usec := 0;
StdStream := TBytesStream.Create(OutBuffer);
ErrStream := TBytesStream.Create(ErrBuffer);
SetLength(ReadBuffer, FBufferSize);
OldBlocking := FSession.Blocking;
FSession.Blocking := False;
try
repeat
ReturnCode := libssh2_channel_exec(Channel,
M.AsAnsi(Command, FSession.CodePage).ToPointer);
CheckLibSsh2Result(ReturnCode, FSession, 'libssh2_channel_exec');
until ReturnCode <> LIBSSH2_ERROR_EAGAIN;
while not FCancelled do
begin
repeat
FD_ZERO(ReadFds);
_FD_SET(FSession.Socket, ReadFds);
ReturnCode := select(0, @ReadFds, nil, nil, @TimeVal);
if ReturnCode < 0 then CheckSocketResult(WSAGetLastError, 'select');
if libssh2_channel_eof(Channel) = 1 then Break;
until (ReturnCode > 0) or FCancelled;
try
BytesRead := libssh2_channel_read(Channel, PAnsiChar(ReadBuffer), FBufferSize);
CheckLibSsh2Result(BytesRead, FSession, 'libssh2_channel_read_ex');
if BytesRead > 0 then
begin
StdStream.WriteBuffer(ReadBuffer, BytesRead);
DoOnStdOutReceive(ReadBuffer, BytesRead);
end;
BytesRead := libssh2_channel_read_stderr(Channel, PAnsiChar(ReadBuffer), FBufferSize);
CheckLibSsh2Result(BytesRead, FSession, 'libssh2_channel_read_ex');
if BytesRead > 0 then
begin
ErrStream.WriteBuffer(ReadBuffer, BytesRead);
DoOnStdOutReceive(ReadBuffer, BytesRead);
end;
except
on E: Exception do
begin
OutputDebugString(PChar(E.Message));
Break;
end;
end;
if BytesRead = 0 then Break;
end;
Output := AnsiToUnicode(PAnsiChar(StdStream.Memory), StdStream.Size, FSession.CodePage);
ErrOutput := AnsiToUnicode(PAnsiChar(ErrStream.Memory), ErrStream.Size, FSession.CodePage);
libssh2_channel_close(Channel);
if FCancelled then
ExitCode := 130
else
ExitCode := libssh2_channel_get_exit_status(Channel);
finally
StdStream.Free;
ErrStream.Free;
libssh2_channel_free(Channel);
FSession.Blocking := OldBlocking;
end;
end;
Альтернативное решение с использованием событий
pyscripter предложил альтернативный подход с использованием событий для обработки выходных данных:
type
TExecOutput = (eoStdout, eoStdErr);
TOnSshExecOutput = procedure(OutputType: TExecOutput; const Data: TBytes) of object;
ISshExec = interface
procedure SetOnOutput(ACallback: TOnSshExecOutput);
procedure Exec(const Command: string; var Output, ErrOutput: string;
var ExitCode: Integer);
end;
Реализация обработчика событий:
procedure TForm1.SshExecOutput(OutputType: TExecOutput; const Data: TBytes);
var
Text: string;
begin
Text := TEncoding.UTF8.GetString(Data);
case OutputType of
eoStdout: Memo1.Lines.Add(Text);
eoStdErr: Memo2.Lines.Add(Text);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Output, Error: string;
ExitCode: Integer;
begin
FSshExec.SetOnOutput(SshExecOutput);
FSshExec.Exec('checksum_command', Output, Error, ExitCode);
// Данные будут появляться в Memo1 и Memo2 по мере поступления
end;
Рекомендации по настройке производительности
Размер буфера: Увеличьте размер буфера для уменьшения количества операций ввода-вывода
FSshExec.BufferSize := 65536; // 64KB
Таймауты: Настройте таймауты для предотвращения преждевременного разрыва соединения
TimeVal.tv_sec := 5; // 5 секунд между проверками TimeVal.tv_usec := 0;
Потоковая обработка: Обрабатывайте данные по мере поступления, не накапливая их в памяти
Заключение
Представленные решения позволяют эффективно решить проблемы с асинхронным выполнением команд через SSH в Delphi-приложениях. Выбор между коллбэками и событиями зависит от конкретных требований вашего проекта. Оба подхода обеспечивают:
Отсутствие блокировки основного потока приложения
Возможность обработки данных по мере их поступления
Устойчивость к таймаутам при работе с большими файлами
Гибкость в настройке производительности
Для максимальной эффективности рекомендуется использовать последнюю версию библиотеки ssh-pascal и учитывать предложенные рекомендации по настройке производительности.
Использование библиотеки ssh-pascal в Delphi для асинхронного выполнения SSH-команд с обработкой данных в реальном времени и настройкой производительности.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.