SHChangeNotify not updating URL= change in my .url shortcut file - windows

I have a simple Delphi application that creates a desktop shortcut for a URL. It makes a two-line text file with a .url filename extension in the user's Desktop folder:
[InternetShortcut]
URL=http://127.0.0.1/admin
That works fine. When I need to update the file with a new URL, I overwrite the old file. But Windows will not recognize the change until I restart Explorer or reboot. So I learned about SHChangeNotify() and called it after overwriting the file:
SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATH or SHCNF_FLUSH, PChar(Path), nil);
But it has no effect:
I tried with and without the SHCNF_FLUSH flag;
also the SHCNF_FLUSHNOWAIT flag makes no difference.
I also tried deleting the file first and then using the SHCNE_DELETE event and then re-creating the file. That doesn't work either, it just keeps using the old URL.
How do I force Explorer to reload the URL from the file without a restart?

While the file's content can be treated like any INI file I yet have not found a direct way to control manipulations to it:
When creating a file its content is read as expected: the system's default application for the URL='s protocol is started (i.e. for http it is most likely the internet browser).
Modifying the file per file systems has no effect - either MSIE itself maintains a cache or the COM's magic.
Indirectly manipulation is possible in the following way:
Empty the file's existing content. Why? Because the later step will just add the same INI section with an URL= value again, but the first section's URL= value remains the one that is taken into account.
Access the file per COM and change its properties. Sadly this writes more into the file - in my case the outcome/file's content was:
[{000214A0-0000-0000-C000-000000000046}]
Prop3=19,2
[InternetShortcut]
URL=http://127.0.0.1/index.php
IDList=
However, it "works" as in: the change (speak: a different URL) is recognized. Putting it all together my following code for Delphi 7 on Windows 7 should also work for you - just call the function:
uses
ShlObj, ActiveX, ComObj;
const
SID_IUniformResourceLocatorA= '{FBF23B80-E3F0-101B-8488-00AA003E56F8}';
SID_IUniformResourceLocatorW= '{CABB0DA0-DA57-11CF-9974-0020AFD79762}';
SID_InternetShortcut= '{FBF23B40-E3F0-101B-8488-00AA003E56F8}';
type
PUrlInvokeCommandInfoA= ^TUrlInvokeCommandInfoA;
TUrlInvokeCommandInfoA= record
dwcbSize,
dwFlags: DWORD; // Bit field of IURL_INVOKECOMMAND_FLAGS
hwndParent: HWND; // Parent window. Valid only if IURL_INVOKECOMMAND_FL_ALLOW_UI is set.
pcszVerb: LPCSTR; // Verb to invoke. Ignored if IURL_INVOKECOMMAND_FL_USE_DEFAULT_VERB is set.
end;
PUrlInvokeCommandInfoW= ^TUrlInvokeCommandInfoW;
TUrlInvokeCommandInfoW= record
dwcbSize,
dwFlags: DWORD;
hwndParent: HWND;
pcszVerb: LPCWSTR;
end;
IUniformResourceLocatorA= interface( IUnknown )
[SID_IUniformResourceLocatorA]
function SetURL( pcszURL: LPCSTR; dwInFlags: DWORD ): HRESULT; stdcall;
function GetURL( ppszURL: LPSTR ): HRESULT; stdcall;
function InvokeCommand( purlici: PUrlInvokeCommandInfoA ): HRESULT; stdcall;
end;
IUniformResourceLocatorW= interface( IUnknown )
[SID_IUniformResourceLocatorW]
function SetURL( pcszURL: LPCWSTR; dwInFlags: DWORD ): HRESULT; stdcall;
function GetURL( ppszURL: LPWSTR ): HRESULT; stdcall;
function InvokeCommand(purlici: PUrlInvokeCommandInfoW ): HRESULT; stdcall;
end;
function SetURL( sFile, sUrl: Widestring ): Integer;
const
CLSID_InternetShortCut: TGUID= SID_InternetShortcut;
var
oUrl: IUniformResourceLocatorW;
oFile: IPersistFile;
hFile: THandle;
begin
// First, the existing file's content should be emptied
hFile:= CreateFileW( PWideChar(sFile), GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0 );
if hFile= INVALID_HANDLE_VALUE then begin
result:= 1; // File might not exist, sharing violation, etc.
exit;
end;
// Initial file pointer is at position 0
if not SetEndOfFile( hFile ) then begin
result:= 2; // Missing permissions, etc.
CloseHandle( hFile );
exit;
end;
// Gracefully end accessing the file
if not CloseHandle( hFile ) then begin
result:= 3; // File system crashed, etc.
exit;
end;
// Using COM to access properties
result:= 0;
try
oUrl:= CreateComObject( CLSID_InternetShortCut ) as IUniformResourceLocatorW;
except
result:= 4; // CLSID unsupported, COM not available, etc.
end;
if result<> 0 then exit;
// Opening the file again
oFile:= oUrl as IPersistFile;
if oFile.Load( PWideChar(sFile), STGM_READWRITE )<> S_OK then begin
result:= 5; // Sharing violations, access permissions, etc.
exit;
end;
// Set the property as per interface - only saving the file is not enough
if oUrl.SetURL( PWideChar(sUrl), 0 )<> S_OK then begin
result:= 6;
exit;
end;
// Storing the file's new content - setting only the property is not enough
if oFile.Save( PWideChar(sFile), TRUE )<> S_OK then begin
result:= 7;
exit;
end;
// Success!
result:= 0;
end;
As per my desktop firewall the executing process modifies the memory of explorer.exe upon IPersistFile.Save() - after that executing the URL file should reflect its new content, while any attempt before that should still act upon the old file's content.

Related

How to debug startup of app run by SendTo menu

On Windows, I'd like for my FMX app to run from the SendTo context menu. If the app is already running I'd like for the second instance to pass its command line to the first and then exit. Code is below. The problem is that if I have first instance running in the debugger, and then double-click an appropriate file, I see no evidence that the first instance receives a message from a newly started instance. If the app is not already running then the double click starts a new instance as expected.
Is there a way to debug the startup of the instance launched by the SendTo menu?
This code adds the app to the SendTo menu:
class procedure TInstallationController.CreateSendTo;
var
lExePath: string;
lObject: IUnknown;
lSLink: IShellLink;
lPFile: IPersistFile;
lFolderPath: array[0..MAX_PATH] of char;
lLinkName: WideString;
begin
SHGetFolderPath(0, CSIDL_SENDTO, 0, 0, lFolderPath);
lLinkName := Format('%s\%s.lnk', [lFolderPath, 'AppName']);
{$IFNDEF DEBUG}
if String(lLinkName).Contains('debug') then
Tfile.Delete(lLinkName);
{$ENDIF DEBUG}
if not TFile.Exists(lLinkName) then
if CoInitializeEx(nil, COINIT_MULTITHREADED) = S_OK then
begin
lExePath := ParamStr(0);
lObject := CreateComObject(CLSID_ShellLink);
lSLink := lObject as IShellLink;
lPFile := lObject as IPersistFile;
with lSlink do
begin
SetPath(pChar(lExePath));
SetWorkingDirectory(PChar(TPath.GetDirectoryName(lExePath)));
end;
lPFile.Save(PWChar(WideString(lLinkName)), false);
end;
end;
This code is placed before Application.Initialize in the .dpr file:
var
lWindow: HWND;
lMutex: THandle;
lCopyDataStruct: TCopyDataStruct;
i: integer;
lArg: string;
lResult: DWORD;
begin
lMutex := CreateMutex(nil, False, PChar('43671EDF1E5A4B419F213336F2387B0D'));
if lMutex = 0 then
RaiseLastOSError;
if GetLastError = Error_Already_Exists then
begin
FillChar(lCopyDataStruct, Sizeof(lCopyDataStruct), 0);
for I := 1 to ParamCount do
begin
lArg := ParamStr(i);
lCopyDataStruct.cbData := (Length(lArg) + 1)*SizeOf(Char);
lCopyDataStruct.lpData := PChar(lArg);
lWindow := FindWindow('FMT' + STRMainWindowClassName, nil);
SendMessageTimeout(lWindow, WM_COPYDATA, 0, NativeInt(#lCopyDataStruct),
SMTO_BLOCK, 3000, #lResult);
end;
exit;
end;
...
end.
Assignments in FormCreate of the main form to support Windows message forwarding:
...
FHwnd := FmxHandleToHwnd(Handle);
FOldWndProc := GetWindowLongPtr(FHwnd, GWL_WNDPROC);
SetWindowLongPtr(FHwnd, GWL_WNDPROC, NativeInt(#WindowProc));
...
This forwards Windows messages to my main FMX form:
function WindowProc (HWND: HWND; Msg: UINT; wParam: wParam; lParam: lParam): LRESULT; stdcall;
begin
Result := MasterDetailView.WndProc (HWND, Msg, wParam, lParam);
end;
This main form method receives forwarded messages:
function TViewMasterDetail.WndProc(aHwnd: HWND; aMsg: UINT; aWParam: WPARAM;
aLParam: LPARAM): LResult;
begin
Result := 0;
if aMsg = WM_COPYDATA then
begin
TUtils.Log('External file: ' + PChar(PCopyDataStruct(aLParam)^.lpData));
Viewmodel.HandleExternalFile(PChar(PCopyDataStruct(aLParam)^.lpData));
Exit;
end;
result := CallWindowProc(Ptr(fOldWndProc), aHwnd, aMsg, aWParam, aLParam);
end;
TViewMasterDetail.WndProc is called many time, but as far as I can tell aMsg is never WM_COPYDATA. The 'External file:' message never appears in the log. Thanks
Programmer error. To approximate debugging the startup code I ran a copy of the app outside the debugger and then launched a second copy of the app in the debugger, passing the path to the target file on the command line. This told me FindWindow was failing. I wrote this startup code a long time ago and since then have changed the names of UI classes in the app, including the main window. But I neglected to change the constant I used for the class name of the main window and pass to FindWindow. Fixing the constant cleared the error. Just another win for the evils of using text!

How to get the shell image index of an object in the shell namespace?

I want to get the index in the system imagelist of an object in the shell namespace.
If this object was a file i could use SHGetFileInfo:
function GetFileImageIndex(const Filename: string): Integer;
var
sfi: TSHFileInfo;
begin
SHGetFileInfo(PChar(Filename), FILE_ATTRIBUTE_NORMAL, sfi, SizeOf(sfi),
SHGFI_USEFILEATTRIBUTES or SHGFI_SYSICONINDEX);
Result := sfi.iIcon;
end;
Except i don't have a file
The thing i have doesn't exist on the hard-drive as a folder or file, e.g.:
Control Panel
Homegroup
Network
But i still need to get the index in the system imagelist of the icon that corresponds to this thing. I started with SHGetFileInfo (as it supports pidls). But that fell apart. Then i tried using IExtractIcon, but that fell apart:
function GetObjectImageIndex(ParentFolder: IShellFolder; const ChildPidl: PItemIDList): Integer;
//var
// sfi: TSHFileInfo;
// extractIcon: IExtractIcon;
// iconFile: WideString;
// iconIndexInFile: Integer;
// flags: Cardinal;
begin
{
This function is the shell namespace equivalent of GetFileImageIndex helper function.
}
(*
Won't work (MSDN: "The PIDL must be a fully qualified PIDL. Relative PIDLs are not allowed.")
SHGetFileInfo(PWideChar(ChildPidl), FILE_ATTRIBUTE_NORMAL,
sfi, SizeOf(sfi),
SHGFI_PIDL or SHGFI_SYSICONINDEX);
*)
(*
Won't work. Doesn't return an index into the system imagelist
ParentFolder.GetUIObjectOf(0, 1, ChildPidl, IExtractIcon, nil, {out}extractIcon);
SetLength(iconFile, MAX_PATH);
extractIcon.GetIconLocation(0, PWideChar(iconFile), Length(iconFile), iconIndexInFile, {out}flags);
*)
Result := -1; //TODO: Figure out how to do it.
end;
Given an IShellFolder and a pidl in that folder, how do i get the icon in the system imagelist of that thing?
The simple answer is that you pass an absolute PIDL that identifies the object to SHGetFileInfo. You say you tried that without success, but this is the way to solve your problem.
You should go back to SHGetFileInfo and make it work. It looks like you got as far as having a relative PIDL and stopped. Make an absolute PIDL with ILCombine and you should be home.
If you don't have a PIDL for the containing IShellFolder then you'll want to read this topic: How to obtain the PIDL of an IShellFolder
function CreateGlobalChildIDList(AParentFolder: IShellFolder; const AChildIDList: PItemIDList): PItemIDList; forward;
function GetObjectImageIndex(AParentFolder: IShellFolder; const AChildIDList: PItemIDList): Integer;
var
ShellIcon: IShellIcon;
ChildIDList: PItemIDList;
FileInfo: TSHFileInfo;
begin
try
Result := -1;
try
OleCheck(AParentFolder.QueryInterface(IShellIcon, ShellIcon));
try
OleCheck(ShellIcon.GetIconOf(AChildIDList, GIL_FORSHELL, Result));
finally
ShellIcon := nil;
end;
except
Result := -1;
end;
if Result = -1 then
begin
ChildIDList := CreateGlobalChildIDList(AParentFolder, AChildIDList);
try
ZeroMemory(#FileInfo, SizeOf(FileInfo));
SHGetFileInfo(PWideChar(ChildIDList), FILE_ATTRIBUTE_NORMAL, FileInfo, SizeOf(FileInfo), SHGFI_PIDL or SHGFI_SYSICONINDEX);
Result := FileInfo.iIcon;
finally
CoTaskMemFree(ChildIDList);
end;
end;
except
Result := -1;
end;
end;
function CretaeGlobalChildIDList(AParentFolder: IShellFolder; const AChildIDList: PItemIDList): PItemIDList;
var
PersistFolder2: IPersistFolder2;
PersistIDList: IPersistIDList;
ParentIDList: PItemIDList;
begin
if Succeeded(AParentFolder.QueryInterface(IPersistFolder2, PersistFolder2)) then
try
OleCheck(PersistFolder2.GetCurFolder(ParentIDList));
try
Result := ILCombine(ParentIDList, AChildIDList);
finally
CoTaskMemFree(ParentIDList);
end;
finally
PersistFolder2 := nil;
end
else
if Succeeded(AParentFolder.QueryInterface(IPersistIDList, PersistIDList)) then
try
OleCheck(PersistIDList.GetIDList(ParentIDList));
try
Result := ILCombine(ParentIDList, AChildIDList);
finally
CoTaskMemFree(ParentIDList);
end;
finally
PersistIDList := nil;
end
else
raise Exception.Create('Cannot create PIDL');
end;

Set EXE VersionInfo

The information on the version Exe-file I receive by means of VerQueryValue. Is there an inverse function (WinApi or Delphi) which can register (establish or change) such information?
Here, for example, there is a program which is able to do so. How may it work (http://www.angusj.com/resourcehacker)?
The version information is stored via resources; to edit that you simply need to edit that resource. Here is a unit I found that can clone an existing file version information and attach it to another file. It's very easy to do what you want starting from this code (it's coded by a friend of mine and is available public):
unit cloneinfo;
interface
uses Windows, SysUtils;
type
LANGANDCODEPAGE = record
wLanguage: Word;
wCodePage: Word;
end;
procedure clone(sFile,output:string);
implementation
procedure clone(sFile,output:string);
var
dwHandle, cbTranslate: cardinal;
sizeVers: DWord;
lpData, langData: Pointer;
lpTranslate: ^LANGANDCODEPAGE;
hRes : THandle;
begin
sizeVers := GetFileVersionInfoSize(PChar(sFile), dwHandle);
If sizeVers = 0 then
exit;
GetMem(lpData, sizeVers);
try
ZeroMemory(lpData, sizeVers);
GetFileVersionInfo (PChar(sFile), 0, sizeVers, lpData);
If not VerQueryValue (lpData, '\VarFileInfo\Translation', langData, cbTranslate) then
exit;
hRes := BeginUpdateResource(pchar(output), FALSE);
//For i := 0 to (cbTranslate div sizeof(LANGANDCODEPAGE)) do
//begin
lpTranslate := Pointer(Integer(langData) + sizeof(LANGANDCODEPAGE));
UpdateResource(hRes, RT_VERSION, MAKEINTRESOURCE(VS_VERSION_INFO), lpTranslate^.wLanguage,lpData, sizeVers);
//end;
EndUpdateResource(hRes, FALSE);
finally
FreeMem(lpData);
end;
end;
end.

Why do I get an "Invalid handle" error using GetFileSizeEx() with files marked read-only?

When I use the Windows API call GetFileSizeEx() from my Delphi 6 app on a read-only file, I get an O/S error code 6 ("Invalid file handle"). If I remove the read-only attribute from the file, the error disappears. Why am I getting that error and is there a way to use that call or a similar one with read-only files?
Here's the relevant code:
function GetFileSizeEx(hFile: THandle; var FileSize: Int64): BOOL; stdcall; external 'kernel32.dll' name 'GetFileSizeEx';
function easyGetFileSize(theFileHandle: THandle): Int64;
begin
if not GetFileSizeEx(theFileHandle, Result) then
RaiseLastOSError;
end;
-- roschler
Did you check the result of opening the file to get the file handle? Obviously if the file failed to open, you're calling GetFileSizeEx with an invalid handle. You'll need to open the file in a read-only mode.
Maybe something like this?
function GetFileSize_(CONST sFilename: string): Int64; { NOT TESTED }
VAR aHandle: THandle;
begin
aHandle:= CreateFile(PChar(sFilename), GENERIC_READ, FILE_SHARE_READ, NIL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if aHandle = INVALID_HANDLE_VALUE
then Result:= -1
else
begin
GetFileSizeEx(aHandle, Result);
FileClose(aHandle);
end;
end;

Get Name / Description Startaddress Or From A Thread In A Process ( Delphi/Pascal )

Process Hacker has a process manager in C.
When you double-click in process manager on a process e.g. Explorer
You see a lot of info, including:
Topics related to the process. PDD, Cycles Delta Start, Address, priority.
Well I tried to do something similar in Delphi, but I get only the TID and priority ...
I can not put the info Start Address as follows: "msiltcfg.dll 0x258!" or can only return
00630EFA.
The (Original) Application Process Hacker show the information in the image below:
How do I solve this? based on the code example below.
procedure TForm1.Button7Click (Sender: TObject);
var
tbi: THREAD_BASIC_INFORMATION;
hThreadSnap, Process, hThread, ThreadInfo: THandle;
te32: tagTHREADENTRY32;
me32: MODULEENTRY32;
th32: THREADENTRY32;
dwPID: DWORD;
startaddr: Pointer;
Status: LongInt;
Error: DWORD;
modname: String;
hToken: DWORD;
TKP: TOKEN_PRIVILEGES;
otkp: TOKEN_PRIVILEGES;
dwLen: dword;
begin
hThreadSnap: = CreateToolhelp32Snapshot (TH32CS_SNAPTHREAD, 0);
if hThreadSnap = INVALID_HANDLE_VALUE then
Exit;
try
dwPID: = GetProcessID (Trim (Edit1.Text));
te32.dwSize: = SizeOf (THREADENTRY32);
me32.dwSize: = SizeOf (MODULEENTRY32);
ListBox1.Items.Clear;
ListBox2.Items.Clear;
if not Thread32First (hThreadSnap, te32) then
Exit;
repeat
if te32.th32OwnerProcessID = dwPID then
begin
hThread: = OpenThread (THREAD_ALL_ACCESS,
False, te32.th32ThreadID);
status: = ZwQueryInformationThread (hThread,
9,
ThreadQuerySetWin32StartAddress {}
#Startaddr,
SizeOf (startaddr)
# DwLen);
listbox1.Items.AddObject (Format ('StartAddress:% p'
[Startaddr]) + 'ID:' + IntToStr(te32.th32ThreadID), TObject (hThread));
if hThread <> 0 then
CloseHandle (hThread);
end;
Until not Thread32Next (hThreadSnap, te32);
finally
CloseHandle (hThreadSnap);
end;
end;
Take a look at our logging class in the Open Source SynCommons.pas unit: you can trace the stack of any method into the log file. If the .map (or its compressed .mab equivalence) is available, the line number will be displayed.
I'm working on a log viewer able to add source code lines during viewing, from a save .map/.mab file.
It's now used by the unit testing classes, so that any failure will create an entry in the log with the source line, and stack trace:
C:\Dev\lib\SQLite3\exe\TestSQL3.exe 0.0.0.0 (2011-04-13)
Host=Laptop User=MyName CPU=2*0-15-1027 OS=2.3=5.1.2600 Wow64=0 Freq=3579545
TSynLogTest 1.13 2011-04-13 05:40:25
20110413 05402559 fail TTestLowLevelCommon(00B31D70) Low level common: TDynArray "" stack trace 0002FE0B SynCommons.TDynArray.Init (15148) 00036736 SynCommons.Test64K (18206) 0003682F SynCommons.TTestLowLevelCommon._TDynArray (18214) 000E9C94 TestSQL3 (163)
The difference between a test suit without logging (TSynTests) and a test suit with logging (TSynTestsLogged) is only this:
procedure TSynTestsLogged.Failed(const msg: string; aTest: TSynTestCase);
begin
inherited;
with TestCase[fCurrentMethod] do begin
fLogFile.Log(sllFail,'%: % "%"',
[Ident,TestName[fCurrentMethodIndex],msg],aTest);
end; {with}
end;
The sllFail level if used here, but you can use any available level.

Resources