Jump to content
aehimself

How to open a file in the already running IDE?

Recommended Posts

1 hour ago, aehimself said:

Perfection, works like a charm. It triggered my antivirus though, hope won't happen in the real application 🙂

make exhaustive testing, it stopped working on my system again. no clue why

Share this post


Link to post
55 minutes ago, Attila Kovacs said:

make exhaustive testing, it stopped working on my system again. no clue why

Happened to me too, however a simple Delphi restart solved it. Maybe DDE server died in Delphi, as DdeQueryNextServer found no matches.

Anyway, I added some information gathering and now I have the full process name of each process which was found. From here, I only need to enumerate the bds paths in Registry to find which Delphi version that executable is for.

 

Little bit more code than I expected, but at least it works!

 

Thank you!

Share this post


Link to post

here is the executable name

 

 

 

  Buffer: array [0 .. 4095] of Char;
  Modules: array [0 .. 255] of THandle;
  PID: DWORD;
  exePathLen: integer;
  hProcess: THandle;
  nResult: boolean;
  nTemp: Cardinal;
  aService, aTopic: WORD;

....    

if GetWindowThreadProcessId(ci.hwndPartner, PID) <> 0 then
    begin
      hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, False, PID);
      if hProcess <> 0 then
      begin
        nResult := EnumProcessModules(hProcess, @Modules[0], Length(Modules), nTemp);
        if nResult then
        begin
          nTemp := GetModuleFileNameEx(hProcess, 0, Buffer, SizeOf(Buffer));
          Memo1.Lines.Add(Buffer);
        end;
      end;
    end;

...

 

Edited by Attila Kovacs

Share this post


Link to post
3 minutes ago, Attila Kovacs said:

here is the executable name

 

 

 


  Buffer: array [0 .. 4095] of Char;
  Modules: array [0 .. 255] of THandle;
  PID: DWORD;
  exePathLen: integer;
  hProcess: THandle;
  nResult: boolean;
  nTemp: Cardinal;
  aService, aTopic: WORD;

....    

if GetWindowThreadProcessId(ci.hwndPartner, PID) <> 0 then
    begin
      hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, False, PID);
      if hProcess <> 0 then
      begin
        nResult := EnumProcessModules(hProcess, @Modules[0], Length(Modules), nTemp);
        if nResult then
        begin
          nTemp := GetModuleFileNameEx(hProcess, 0, Buffer, SizeOf(Buffer));
          Memo1.Lines.Add(Buffer);
        end;
      end;
    end;

...

 

Yep, similar to my approach. I moved this to a separate function though, but logic is the same.

Share this post


Link to post

Had that in mind but I'm afraid of using version numbers, as you have to consider patch levels in each version. I guess it might change bds.exe version number too.

Share this post


Link to post

Why not catch caption.  Don't tell anyone I'm working up a log program:classic_biggrin:

12/6/2022 6:26:47 AM= Windows.UI.Core.CoreWindow Windows Default Lock Screen
12/6/2022 6:26:54 AM= Shell_TrayWnd 
12/6/2022 6:27:09 AM= TMain Main
12/6/2022 6:28:29 AM= TAppBuilder ProgramLogger - Delphi 11 - AppMain [Running] [Built]
12/6/2022 6:29:38 AM= TMain Main
12/6/2022 6:29:39 AM= TAppBuilder ProgramLogger - Delphi 11 - AppMain [Running] [Built]
12/6/2022 6:30:28 AM= Chrome_WidgetWin_1 View program flow and calls to help understand code? - General Help - Delphi-PRAXiS [en] and 3 more pages - Personal - Microsoft? Edge

 

Share this post


Link to post
21 hours ago, Attila Kovacs said:

Here.

 

This queries all the running bde's and opens a file in every one.

This is a good start, you have to find out which is which version somehow, I have no more time for that.

DDE-test.7z

I took the liberty to include everything in a small, easy to use package: https://github.com/aehimself/AEFramework/blob/master/AE.DelphiVersions.pas

 

Usage:

Var
  dv: TDelphiVersions;
  v: TDelphiVersion;
begin
  dv := TDelphiVersions.Create;
  Try
    For v In dv.InstalledVersions Do
      If v.IsRunning Then
        v.OpenFile('C:\a.pas');
  Finally
    FreeAndNil(dv);
  End;
End;

Multiple DDE queries clearly lock something up, I suspect calling .IsRunning in an infinite loop will turn from True to False after a while.

Also, OpenFile will raise an AV if there are no instances. I'll add some error handling later.

Share this post


Link to post

there is a small change since then in at the beginning of the DDE part

 

  lHszApp := DdeCreateStringHandleW(FDdeInstId, PChar(DdeService), CP_WINUNICODE);
  DdeKeepStringHandle(FDdeInstId, lHszApp);
  lHszTopic := DdeCreateStringHandleW(FDdeInstId, PChar(DdeTopic), CP_WINUNICODE);
  DdeKeepStringHandle(FDdeInstId, lHszTopic);
  try
    ConvList := DdeConnectList(FDdeInstId, lHszApp, lHszTopic, 0, nil);
  finally
    DdeFreeStringHandle(FDdeInstId, lHszApp);
    DdeFreeStringHandle(FDdeInstId, lHszTopic);
  end;

 

also, I dropped vcl.ddeman and initialize DDE myself

 

.....

function DdeMgrCallBack(CallType, Fmt: UINT; Conv: HConv; hsz1, hsz2: HSZ; Data: HDDEData; Data1, Data2: ULONG_PTR): HDDEData; stdcall;
begin
  Result := 0;
end;

initialization

// DdeInitialize
FDdeInstId := 0;
InitRes := DdeInitializeW(FDdeInstId, DdeMgrCallBack, APPCLASS_STANDARD, 0);
if InitRes <> 0 then
  raise Exception.CreateFmt('DDE Error: %d', [DdeGetLastError(InitRes)]);

finalization

DdeUninitialize(FDdeInstId);

 

and headless dde windows messages (from console), optional:

 


    FDDEHWND := AllocateHWnd(TDDeHelper.DDEWndProc);
    try

      // fist initiate the communication, this results an ACK from the host
      aService := GlobalAddAtom(PChar(sService));
      aTopic := GlobalAddAtom(PChar(sTopic));
      try

        SendMessage(ci.hwndPartner, WM_DDE_INITIATE, FDDEHWND, Makelong(aService, aTopic));
      finally
        GlobalDeleteAtom(aService);
        GlobalDeleteAtom(aTopic);
      end;

      // send dde script
      ddeCommandH := GlobalLockString(ddeCommand, GMEM_DDESHARE);
      try
        PostMessage(ci.hwndPartner, WM_DDE_EXECUTE, FDDEHWND, ddeCommandH);
      finally
        GlobalUnlock(ddeCommandH);
        GlobalFree(ddeCommandH);
      end;

    finally
      DeAllocateHWND(FDDEHWND);
    end;

 

whereas

 

class procedure TDDeHelper.DDEWndProc(var AMsg: TMessage);
begin
  // ack debug
  if AMsg.Msg = WM_DDE_ACK then
  begin
    if FLog then
      WriteLn(Format('ACK from: %x %x', [AMsg.WParam, AMsg.LParam]));
  end
  else if FDDEHWND <> 0 then
    DefWindowProc(FDDEHWND, AMsg.Msg, AMsg.WParam, AMsg.LParam);
end;

 

as a whole:

 

 

 

uBdsLauncher2.pas

Share this post


Link to post

Btw, stability issue seem to be solved by calling DdeDisconnectList when finished. I'll push an update soon.

  • Like 2

Share this post


Link to post
1 hour ago, Attila Kovacs said:

something is still not ok, after 4 calls the IDE won't respond anymore

I'm using the code to start an IDE if nothing was detected and open a file in it. Once the first instance can not even be detected anymore I still can control the second just fine. This makes me believe the issue is with the DDE server of a specific instance.

 

There are two options.

Either we lock up Delphi's DDE server with our code, or Delphi's DDE server is buggy and crashes by itself.

 

Once an instance locks up I'll see if I can double-click on a file in Explorer to open it.

Share this post


Link to post
3 minutes ago, aehimself said:

Either we lock up Delphi's DDE server with our code, or Delphi's DDE server is buggy and crashes by itself.

 

but opening files from the shell still works which is also DDE from bdsLauncher

Share this post


Link to post
7 hours ago, Attila Kovacs said:

also, I dropped vcl.ddeman and initialize DDE myself

And with this, you made all calls work fine from a background thread!

  • Like 1

Share this post


Link to post

Here are two DDE log files,

This is what BDS is getting by calling "DdeConnectList()"..... (why??? 128 conversations are opened. wtaf?)

1(-3) working, 4th, last terminate is missing and the returned list by DdeConnectList is empty.

 

1.sxl

4.sxl

 

Ok, these are all the window handles on my system, not just bds however I was filtering for the one hwnd.... never mind.

But, 00C70C4C is a TPUtilWindow (AllocateHwd), which belongs to bds.exe

 

<000280> 00C70C4C S WM_DDE_INITIATE hwnd:003109E0 aApp:C24F ("bds") aTopic:C009 ("System")
<000281> 003109E0 S WM_DDE_ACK hwnd:00C70C4C aApp:C24F ("bds") aTopic:C009 ("System")
<000282> 003109E0 R WM_DDE_ACK
<000283> 00C70C4C R WM_DDE_INITIATE
<000400> 00C70C4C P WM_DDE_TERMINATE hwnd:003109E0  <---- this is missing from the 4th run
 

 

Code Meaning
P The message was posted to the queue with the PostMessage function. No information is available concerning the ultimate disposition of the message.
S The message was sent with the SendMessage function. This means that the sender doesn’t regain control until the receiver processes and returns the message. The receiver can, therefore, pass a return value back to the sender.
s The message was sent, but security prevents access to the return value.
R Each ‘S’ line has a corresponding ‘R’ (return) line that lists the message return value. Sometimes message calls are nested, which means that one message handler sends another message.
Edited by Attila Kovacs

Share this post


Link to post

ok, here a native approach, finding the dde window with winapi and sending dde messages, no dde@user32 involved

 

let's see, if it's more stable

 

unit uBdsLauncher2;

interface

procedure OpenFile(const AFileName: string; ALog: boolean = False);

implementation

uses
  Winapi.Windows,
  Winapi.Messages,
  Winapi.PsAPI,
  System.SysUtils,
  System.Classes;

const
  cDdeService = 'bds';
  cDdeTopic = 'system';

var
  FDDEHWND: THandle = 0;
  FLog: boolean;
  FWHandles: TArray<HWND>;
  aService, aTopic: WORD;

type
  TDdeHelper = class
  public
    class procedure DdeWndProc(var AMsg: TMessage);
  end;

function EnumWindowsProc(wHandle: HWND; lParam: THandle): BOOL; stdcall; export;
var
  ClassName: array [0 .. 255] of char;
begin
  Result := True;
  GetClassName(wHandle, ClassName, 255);
  if SameText(ClassName, 'TPUtilWindow') then
    SendMessage(wHandle, WM_DDE_INITIATE, FDDEHWND, Makelong(aService, aTopic));
end;


// make a string global, dont forget to unlock/free it
function GlobalLockString(AValue: string; AFlags: UINT): THandle;
var
  DataPtr: Pointer;
begin
  Result := GlobalAlloc(GMEM_ZEROINIT or AFlags, (Length(AValue) * SizeOf(char)) + 1);
  try
    DataPtr := GlobalLock(Result);
    Move(PChar(AValue)^, DataPtr^, Length(AValue) * SizeOf(char));
  except
    GlobalFree(Result);
    raise;
  end;
end;


function GetWindowExeName(wHandle: HWND): string;
var
  PID: DWORD;
  hProcess: THandle;
  nTemp: Cardinal;
  Modules: array [0 .. 255] of THandle;
  Buffer: array [0 .. 4095] of char;
begin
  Result := '';
  if GetWindowThreadProcessId(wHandle, PID) <> 0 then
  begin
    hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, False, PID);
    if hProcess <> 0 then
      if EnumProcessModules(hProcess, @Modules[0], Length(Modules), nTemp) then
        if GetModuleFileNameEx(hProcess, 0, Buffer, SizeOf(Buffer)) > 0 then
          Result := Buffer;
  end;
end;


procedure OpenFile(const AFileName: string; ALog: boolean = False);
var
  i: integer;
  ddeCommand: string;
  ddeCommandH: THandle;
begin
  FLog := ALog;

  FDDEHWND := AllocateHWnd(TDdeHelper.DdeWndProc);
  try

    SetLength(FWHandles, 0);
    aService := GlobalAddAtom(PChar(cDdeService));
    aTopic := GlobalAddAtom(PChar(cDdeTopic));
    try
      EnumWindows(@EnumWindowsProc, 0);
    finally
      GlobalDeleteAtom(aService);
      GlobalDeleteAtom(aTopic);
    end;

    // send the DDE command to every running BDE instances
    ddeCommand := Format('[open("%s")]', [AFileName]);
    ddeCommandH := GlobalLockString(ddeCommand, GMEM_DDESHARE);
    try
      for i := 0 to High(FWHandles) do
      begin
        if FLog then
          WriteLn(Format('Sending command "%s" to %s', [ddeCommand, IntToHex(FWHandles[i], 8)]));
        PostMessage(FWHandles[i], WM_DDE_EXECUTE, FDDEHWND, ddeCommandH);
        PostMessage(FWHandles[i], WM_DDE_TERMINATE, FDDEHWND, 0);
      end;
    finally
      GlobalUnlock(ddeCommandH);
      GlobalFree(ddeCommandH);
    end;

  finally
    DeAllocateHWND(FDDEHWND);
  end;
end;


class procedure TDdeHelper.DdeWndProc(var AMsg: TMessage);
var
  l: integer;
begin
  // ack debug
  if AMsg.Msg = WM_DDE_ACK then
  begin
    l := Length(FWHandles);
    SetLength(FWHandles, l + 1);
    FWHandles[l] := AMsg.WParam;

    if FLog then
      WriteLn(Format('ACK from: %x %x', [AMsg.WParam, AMsg.lParam]));
  end
  else if FDDEHWND <> 0 then
    DefWindowProc(FDDEHWND, AMsg.Msg, AMsg.WParam, AMsg.lParam);
end;

end.

 

Edited by Attila Kovacs

Share this post


Link to post
7 minutes ago, Attila Kovacs said:

ok, here a native approach, finding the dde window with winapi and sending dde messages, no dde@user32 involved

 

let's see, if it's more stable

This idea won't work as intended, as you are sending all the messages to all TPUtilWindows of all instances you'll find. At least for my purpose, where I am separating Delphi IDE instances by versions.

The theory is interesting though. You think one Delphi instance can have more DDE servers, which are constantly being destroyed / created?

Share this post


Link to post
3 minutes ago, aehimself said:

you are sending all the messages to all TPUtilWindows

Yes, but only the ones answering with ACK who's listening to DDE.

It works very fine. Refresh the post, I've updated several times.

Share this post


Link to post
6 minutes ago, aehimself said:

you are sending all the messages to all TPUtilWindows

by the way, if you are looking into my logs, DdeConnectList does the same.

The only thing I don't know, how "DdeConnectList" is enumerating the windows. It was sending it to 128 ones.

 

 

Edited by Attila Kovacs

Share this post


Link to post
7 minutes ago, aehimself said:

At least for my purpose, where I am separating Delphi IDE instances by versions.

There is an array with the window handles which answered to DDE_INITIATE, there is GetWindowExeName to get the executable, you have everything just like before, the only difference to your needs is, that I'm trying to open a file in every running IDE's in my demos.

  • Like 1

Share this post


Link to post

The window message version caused more havoc than it could solve. While collecting DDE targets worked more reliably, it interfered with sending commands.

The finding no instances symptom was caused by the list connect, not Delphi's DDE server; adding error handling revealed that:

      convlist := DdeConnectList(ddeid, svchandle, topichandle, 0, nil);
      If convlist = 0 Then
        Raise EDelphiVersionException.Create('Retrieving the list of Delphi DDE servers failed, DDE error ' + DdeGetLastError(ddeid).ToString);

threw DMLERR_NO_CONV_ESTABLISHED when something was "locked up".

 

I'm now playing around with

  res := DdeInitializeW(ddeid, nil, APPCMD_CLIENTONLY, 0);

Seems to be stable so far.

Share this post


Link to post

What do you mean it interfered with sending commands? Can you be more specific?

 

12 minutes ago, aehimself said:

adding error handling revealed that

I have this error handling and you will get error 16394 after the 4th call. (Again, look into the DDE logs)

 

Share this post


Link to post
17 minutes ago, aehimself said:

APPCMD_CLIENTONLY

No way, this was the first thing I tried, 2 days ago. Nothing changed..

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×