Files
HeidiSQL/source/communication.pas

507 lines
14 KiB
ObjectPascal

unit communication;
(*
Functions for Inter-Process Communication.
Note about WM_COPYDATA:
This message must be sent synchronously with SendMessage,
cannot be sent asynchronously using PostMessage. I think
the reason is related to the idea behind WM_COPYDATA causing
Windows to do a context switch to the receiving application..
*)
interface
uses
Db,
Windows,
Threading,
Classes,
Messages;
const
// Our custom message types.
WM_COMPLETED = WM_APP + 1;
// Our message subtypes for WM_COPYDATA messages.
CMD_EXECUTEQUERY_NORESULTS = 1; { Slightly faster - Fire-and-forget, no results }
CMD_EXECUTEQUERY_RESULTS = 2; { Normal - Wait for completion, fetch results }
RES_QUERYDATA = 257;
RES_EXCEPTION = 258;
RES_NONQUERY = 259;
// Our custom return codes.
ERR_NOERROR = 0;
ERR_UNSPECIFIED = 1;
// Sent by TMysqlQueryThread to notify status
WM_MYSQL_THREAD_NOTIFY = WM_USER+100;
type
TNonQueryRunner = procedure(sendingApp: THandle; query: string) of object;
TQueryRunner = function(sendingApp: THandle; query: string): TDataSet of object;
(*
Run this procedure at application startup.
*)
procedure InitializeComm(myWindow: THandle; nonQueryRunner: TNonQueryRunner; queryRunner: TQueryRunner);
(*
Execute a query on another window, request results but don't wait for them.
*)
function RemoteExecSqlAsync(handler: TCompletionHandler; timeout: Cardinal; window: THandle; query: String; method: DWORD; waitControl: TObject = nil): Cardinal;
(*
Execute a query on another window, showing a wait dialog while processing
and calling a completion handler via messaging when done.
*)
function RemoteExecSql(handler: TCompletionHandler; timeout: Cardinal; window: THandle; query: String; info: String; method: DWORD): Cardinal;
(*
Execute a query on another window, showing a wait dialog while processing
and returning results or raising an exception when done.
*)
function RemoteExecQuery(window: THandle; query: String; info: String): TDataSet;
(*
Execute a query on a window.
*)
procedure RemoteExecNonQuery(window: THandle; query: string; info: string = '');
(*
Execute a USE query on a window,
given the version of the mysql server that window is connected to.
*)
procedure RemoteExecUseNonQuery(window: THandle; mysqlVersion: integer; dbName: string; info: string = '');
(*
Fill in resulting data and return waiting thread to caller.
Call from message handler when a query has finished executing and results are ready.
*)
procedure FinishRemoteExecution(msg: TWMCopyData);
(*
Slightly lame wrapper: Call to release a RemoteXXX SendMessage call on the "other side".
*)
procedure ReleaseRemoteCaller(errCode: integer);
(*
Extract a SQL query from a WM_COPYDATA message.
*)
function GetQueryFromMsg(msg: TWMCopyData): string;
(*
Extract a request id from a WM_COPYDATA message.
*)
function GetRequestIdFromMsg(msg: TWMCopyData): Cardinal;
(*
Helper which will handle WM_COMPLETED messages received.
*)
procedure HandleWMCompleteMessage(var msg: TMessage);
(*
Helper which will handle WM_COPYDATA messages received.
*)
procedure HandleWMCopyDataMessage(var msg: TWMCopyData);
implementation
uses
Forms,
Dialogs,
AdoDb,
AdoInt,
ActiveX,
Helpers,
SysUtils;
type
TCopyDataStruct = packed record
dwData: LongWord; // up to 32 bits of data to be passed to the receiving application.
cbData: LongWord; // the size, in bytes, of the data pointed to by the lpData member.
lpData: Pointer; // points to data to be passed to the receiving application. This member can be nil.
end;
var
sender: THandle;
nqRunner: TNonQueryRunner;
qRunner: TQueryRunner;
function CopyDataSetToAdoDataSet(src: TDataSet): TAdoDataSet;
var
dst: TAdoDataSet;
i: Integer;
begin
dst := TAdoDataSet.Create(nil);
dst.FieldDefs.Assign(src.FieldDefs);
dst.CreateDataSet;
src.First;
while not src.Eof do begin
dst.Append;
for i := 0 to dst.FieldCount - 1 do begin
dst.Fields[i].Assign(src.Fields[i]);
end;
dst.Post;
src.Next;
end;
result := dst;
end;
procedure SendDatasetToRemote(window: THandle; request: Cardinal; resType: integer; ds: TDataSet);
var
adods: TAdoDataSet;
data: TCopyDataStruct;
ms: TMemoryStream;
sa: TStreamAdapter;
olevar: OleVariant;
begin
ms := TMemoryStream.Create;
try
ms.Write(request, sizeof(Cardinal));
if resType <> RES_NONQUERY then begin
adods := CopyDataSetToAdoDataSet(ds);
sa := TStreamAdapter.Create(ms);
olevar := adods.Recordset;
olevar.Save(sa as IStream, 0);
end;
debug(Format('ipc: Sending data set to window %d, request id %d, size %d', [window, request, ms.Size]));
data.dwData := resType;
data.cbData := ms.Size;
data.lpData := ms.Memory;
SendMessage(window, WM_COPYDATA, sender, integer(@data));
finally
ms.free;
end;
end;
procedure SendErrorStringToRemote(window: THandle; request: Cardinal; resType: integer; error: string);
var
data: TCopyDataStruct;
ms: TMemoryStream;
pcerr: PChar;
begin
ms := TMemoryStream.Create;
try
ms.Write(request, sizeof(Cardinal));
pcerr := PChar(error);
ms.Write(pcerr^, StrLen(pcerr) + 1);
debug(Format('ipc: Sending error message to window %d, request id %d, size %d', [window, request, ms.Size]));
data.dwData := resType;
data.cbData := ms.Size;
data.lpData := ms.Memory;
SendMessage(window, WM_COPYDATA, sender, integer(@data));
finally
ms.free;
end;
end;
function GetStringFromMsgInternal(msg: TWMCopyData): string;
var
pcsql: PChar;
ms: TMemoryStream;
begin
ms := TMemoryStream.Create;
try
with msg.CopyDataStruct^ do begin
ms.Write(lpData^, cbData);
ms.Position := sizeof(Cardinal);
pcsql := StrAlloc(ms.Size - sizeof(Cardinal));
ms.Read(pcsql^, ms.Size - sizeof(Cardinal));
result := pcsql;
end;
finally
ms.free;
end;
end;
function GetQueryFromMsg(msg: TWMCopyData): string;
begin
result := GetStringFromMsgInternal(msg);
end;
function GetExceptionTextFromMsg(msg: TWMCopyData): string;
begin
result := GetStringFromMsgInternal(msg);
end;
function GetRequestIdFromMsg(msg: TWMCopyData): Cardinal;
var
req: Cardinal;
ms: TMemoryStream;
begin
ms := TMemoryStream.Create;
try
with msg.CopyDataStruct^ do begin
ms.Write(lpData^, cbData);
ms.Position := 0;
ms.Read(req, sizeof(Cardinal));
result := req;
end;
finally
ms.free;
end;
end;
function GetDataSetFromMsg(msg: TWMCopyData): TDataSet;
var
adods: TAdoDataSet;
ms: TMemoryStream;
sa: TStreamAdapter;
olevar: OleVariant;
begin
ms := TMemoryStream.Create;
try
with msg.CopyDataStruct^ do begin
ms.Write(lpData^, cbData);
ms.Position := sizeof(Cardinal);
sa := TStreamAdapter.Create(ms);
olevar := CoRecordset.Create;
olevar.Open(sa as IStream);
adods := TAdoDataSet.Create(nil);
adods.Recordset := IUnknown(olevar) as _Recordset;
result := adods;
end;
finally
ms.free;
end;
end;
procedure RemoteExecSqlInternal(method: DWORD; req: Cardinal; window: THandle; query: String);
var
ms: TMemoryStream;
pcsql: PChar;
data: TCopyDataStruct;
err: integer;
begin
ms := TMemoryStream.Create;
try
debug(Format('ipc: Remote query being requested, id %d.', [req]));
ms.Write(req, sizeof(Cardinal));
pcsql := PChar(query);
ms.Write(pcsql^, StrLen(pcsql) + 1);
data.dwData := method;
data.cbData := ms.Size;
data.lpData := ms.Memory;
err := SendMessage(window, WM_COPYDATA, sender, integer(@data));
if err <> 0 then Exception.CreateFmt('Remote returned error %d when asked to execute query', [err]);
finally
ms.free;
end;
end;
function RemoteExecSqlAsync(handler: TCompletionHandler; timeout: Cardinal; window: THandle; query: String; method: DWORD; waitControl: TObject = nil): Cardinal;
var
req: Cardinal;
begin
req := SetCompletionHandler(handler, timeout, waitControl);
RemoteExecSqlInternal(method, req, window, query);
result := req;
end;
function RemoteExecSql(handler: TCompletionHandler; timeout: Cardinal; window: THandle; query: String; info: String; method: DWORD): Cardinal;
var
cancelDialog: TForm;
requestId: Cardinal;
begin
if Length(info) = 0 then info := 'Waiting for remote session to execute query...';
cancelDialog := CreateMessageDialog(info, mtCustom, [mbCancel]);
requestId := RemoteExecSqlAsync(handler, timeout, window, query, method, cancelDialog);
// The callback method shouldn't be activated before messages has been processed,
// so we can safely touch the wait control (a cancel dialog) here.
cancelDialog.ShowModal;
// We just cancel in any case.
// If the query was completed before the cancel dialog closed,
// the notification code won't accept the cancel, so it's OK.
NotifyInterrupted(requestId, Exception.Create('User cancelled.'));
result := RequestId;
end;
function RemoteExecQuery(window: THandle; query: String; info: String): TDataSet;
var
requestId: Cardinal;
begin
// Call with no handler (= no completion message) and no timeout.
requestId := RemoteExecSql(nil, INFINITE_TIMEOUT, window, query, info, CMD_EXECUTEQUERY_RESULTS);
// Take care of results since there's no handler.
result := TDataSet(ExtractResultObject(requestId));
end;
procedure RemoteExecNonQuery(window: THandle; query: string; info: string);
var
requestId: Cardinal;
begin
// Call with no handler (= no completion message) and no timeout.
requestId := RemoteExecSql(nil, INFINITE_TIMEOUT, window, query, info, CMD_EXECUTEQUERY_NORESULTS);
// Take care of results since there's no handler.
ExtractResultObject(requestId);
end;
procedure RemoteExecUseNonQuery(window: THandle; mysqlVersion: integer; dbName: string; info: string);
begin
RemoteExecNonQuery(window, 'USE ' + maskSql(mysqlVersion, dbName), info);
end;
procedure SwitchWaitControlInternal(waitControl: TObject);
var
cancelDialog: TForm;
begin
// Hide the cancel dialog if it's still showing.
cancelDialog := TForm(waitControl);
if (cancelDialog <> nil) and cancelDialog.Visible then cancelDialog.Close;
end;
procedure FinishRemoteExecution(msg: TWMCopyData);
var
res: TDataSet;
req: Cardinal;
s: string;
begin
req := GetRequestIdFromMsg(msg);
debug(Format('ipc: Remote execute query call finished for request id %d.', [req]));
case msg.CopyDataStruct^.dwData of
RES_QUERYDATA: begin
res := GetDataSetFromMsg(msg);
NotifyComplete(req, res);
end;
RES_EXCEPTION: begin
s := GetExceptionTextFromMsg(msg);
NotifyFailed(req, Exception.Create('Error from remote: ' + s));
end;
RES_NONQUERY: begin
// Uses a blank object to indicate completed queries with no result data..
NotifyComplete(req, TObject.Create());
end;
end;
end;
procedure ReleaseRemoteCaller(errCode: integer);
begin
// reply to the message so the clients thread is unblocked
ReplyMessage(errCode);
end;
procedure ReportFinishedQuery(method: DWORD; window: THandle; request: Cardinal; ds: TDataSet);
var
resType: DWORD;
begin
if method = CMD_EXECUTEQUERY_NORESULTS then resType := RES_NONQUERY
else resType := RES_QUERYDATA;
SendDataSetToRemote(window, request, resType, ds);
end;
procedure ReportFailedQuery(method: DWORD; window: THandle; request: Cardinal; error: string); overload;
begin
SendErrorStringToRemote(window, request, RES_EXCEPTION, error);
end;
procedure HandleWMCompleteMessage(var msg: TMessage);
var
req: Cardinal;
res: TNotifyStructure;
callback: TCompletionHandler;
begin
debug('ipc: Handling WM_COMPLETED.');
try
// Extract results.
req := msg.LParam;
res := ExtractResults(req, true);
// Switch wait control to non-waiting state.
SwitchWaitControlInternal(res.GetWaitControl);
// Perform rest of completion via callback, if any.
callback := res.GetHandler;
if @callback <> nil then begin
// Clear results.
ExtractResults(req);
// Perform callback.
callback(res);
end;
// Otherwise just assume that completion will be handled
// by some thread which were waiting for the wait control.
//
// In the future, we could explicitly sound an event for
// this purpose, in case it's not possible to wait on the
// wait control..
finally
ReleaseRemoteCaller(ERR_NOERROR);
end;
end;
procedure HandleWMCopyDataMessage(var msg: TWMCopyData);
var
method: DWORD;
query: string;
remoteReqId: integer;
data: TDataSet;
//tab: THandle;
begin
debug('ipc: Handling WM_COPYDATA.');
method := msg.CopyDataStruct^.dwData;
if
(method = CMD_EXECUTEQUERY_NORESULTS) or
(method = CMD_EXECUTEQUERY_RESULTS)
then begin
try
remoteReqId := GetRequestIdFromMsg(msg);
query := GetQueryFromMsg(msg);
finally
ReleaseRemoteCaller(ERR_NOERROR);
end;
try
if method = CMD_EXECUTEQUERY_NORESULTS then begin
nqRunner(msg.From, query);
ReportFinishedQuery(method, msg.From, remoteReqId, nil);
end else begin
data := qRunner(msg.From, query);
ReportFinishedQuery(method, msg.From, remoteReqId, data);
end;
except
on e: Exception do begin
ReportFailedQuery(method, msg.From, remoteReqId, e.Message);
end;
end;
end;
if
(method = RES_QUERYDATA) or
(method = RES_EXCEPTION) or
(method = RES_NONQUERY)
then begin
ReleaseRemoteCaller(ERR_NOERROR);
FinishRemoteExecution(msg);
end;
end;
procedure InitializeComm(myWindow: THandle; nonQueryRunner: TNonQueryRunner; queryRunner: TQueryRunner);
begin
sender := myWindow;
nqRunner := nonQueryRunner;
qRunner := queryRunner;
end;
end.