How to share an application and associated datafile - delphi

I have written a program (firemonkey) using delphi community edition. I would like to share the program, but the .exe file that my friends will be downloading has to access a text file from time to time to retrieve strings. When writing the program I used an event handler, to load the text file at startup:
tform3.formCreate (Sender:Tobject);
...
assignfile(myfile,('C:**********.txt));
...
Worked just fine during the
design stage.
As a hobbyist, I now find myself stuck. If I use INNO setup compiler to create an installation program, which I plan to do, I can't have this same hardwired reference ('C:*****) to the data file's location. What I need is to change the above code such that the .exe file can locate the supporting datafile irrespective of where that .exe file (and datafile) ends up on someone else's PC.
How can I do this? i.e. What code do I need (in place of the above) to ensure that the installation program I hand out will install an .exe file that can locate the data file it references?
Any help, much appreciated. Still learning.

Read-only access
If the data file should always be opened in read-only mode, the simplest solution is to place it next to the *.exe file. Then, at runtime, you dynamically find the path to the *.exe file and modify it to find the path to the data file. For instance,
uses
IOUtils;
procedure TForm1.FormCreate(Sender: TObject);
var
FileName: string;
begin
FileName := TPath.Combine(ExtractFilePath(ParamStr(0)), 'data.txt');
ShowMessage(TFile.ReadAllText(FileName, TEncoding.UTF8));
end;
ParamStr(0) contains the path to the *.exe file, such as
'C:\Users\Andreas Rejbrand\Documents\Embarcadero\Studio\Projects\test\Win32\Debug\Project1.exe'
Then ExtractFilePath(ParamStr(0)) is
'C:\Users\Andreas Rejbrand\Documents\Embarcadero\Studio\Projects\test\Win32\Debug\'
and, finally, TPath.Combine(ExtractFilePath(ParamStr(0)), 'data.txt') is
'C:\Users\Andreas Rejbrand\Documents\Embarcadero\Studio\Projects\test\Win32\Debug\data.txt'
Make sure the installer puts the data file next to the *.exe file.
Read and write access
If we are talking about a settings file or some other file that each user needs to change (via the software), you cannot place it next to the *.exe file, because the *.exe file typically resides in the Program Files folder, which is read only. Also, there is only one Program Files folder, but possibly many users on the PC, and each user should have his or her own copy.
The solution is to save the file in the user's own folders, specifically, the AppData folder:
FileName := TPath.GetHomePath + '\Mariner\My Word Processor App\Settings\settings.ini';
(using a different approach to path building).
On my system, this becomes
'C:\Users\Andreas Rejbrand\AppData\Roaming\Mariner\My Word Processor App\Settings\settings.ini'
Your installer (Inno Setup) has built-in support for placing files in this location.

If it is only accessed read only, you could also consider adding it as a resource to the executable. Which would then allow you to simply distribute this executable without the need for an installer.
Delphi Dabbler has an example, but I found it a bit confusing. I'll link to it (PDF) anyway.

You can let the user select the place where the file has to be saved. Proposing AppData folder if the file is for each individual user or CommonAppData if the file has to be shared between different users.
When the use selected the data file destination, you can save it to an INI file. The INI file can be stored, without asking the user, either to the registry or to an INI file saved in the AppData folder or ProgramData folder.
Here is a snipped of source code to get hand on those special folders:
const
SectionWindow = 'Window';
SectionData = 'Data';
CompanyFolder = 'YourCompanyName';
constructor TForm1.Create(AOwner: TComponent);
var
CommonPath : array [0..MAX_PATH] of Char;
LocalPath : array [0..MAX_PATH] of Char;
LangFileName : String;
begin
SHGetFolderPath(0, CSIDL_COMMON_APPDATA, 0, SHGFP_TYPE_CURRENT, #CommonPath[0]);
SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, SHGFP_TYPE_CURRENT, #LocalPath[0]);
FIniSection := SectionWindow;
FIniSectionData := SectionData;
FAppName := ChangeFileExt(ExtractFileName(Application.ExeName), '');
FCommonAppData := IncludeTrailingPathDelimiter(CommonPath) +
CompanyFolder + '\' + FAppName + '\';
FLocalAppData := IncludeTrailingPathDelimiter(LocalPath) +
CompanyFolder + '\' + FAppName + '\';
FIniFileName := FLocalAppData + FAppName + '.ini';
ForceDirectories(FCommonAppData);
ForceDirectories(FLocalAppData);
inherited Create(AOwner);
end;

Related

File Not Found in Delphi (FMX)

I am trying to load a bunch of lines from a text file located in same directory as the .dproj and Delphi can't find the file I'm trying to read from.
the code is as follows:
procedure TFoPrincipale.Button2Click(Sender: TObject);
var monFich : TextFile;
Sligne : string;
begin
try
AssignFile(monFich, 'docText.txt');
Reset(monFich);
except
showmessage('Le fichier est introuvable');
exit;
end;
while not Eof (monFich) do
begin
Readln(monFich, Sligne);
Memo1.Lines.Add(Sligne);
end;
CloseFile(monFich);
end;
If you are using default project options then your application executable isn't compiled into the project directory but instead into the Win32\Debug or Win32\Release subfolder of your project folder.
So you should take into account the relative path to your file. In your case, the desired file is in second parent folder of the folder in which your executable resides.
I recommend you first get the path location of your executable file using ExtractFilePath(Application.ExeName).
Then you can make use of TDirectory.GetParent() in order to move up the directory chain until you reach the desired directory.

Dynamically Set Database Parameters For RAD Server

I have developed an application that uses RAD Server over IIS. So far I have successfully created my Server and client applications. The app provides some information that needs to be verified against a MSSQL database on the Server Side. Everything works fine in a test environment as my Database Connection parameters are set in my FDConnection component.
However, I would like to change the Connection parameters by reading an ini file when the Server is accessed.
On my development system I can place the ini file in the directory where my bpl output is located. (ie. C:\Users\Username\Projects\Application\Server\Win32\Debug). The server then reads the ini file correctly and updates the component parameters.
I have created the directories on the server in accordance with the RAD Server documentation and have placed the required EMS files in the directory. (ie: C:\inetpub\RADServer\EMSServer) Since this is where the emsserver.ini file resides, I thought this would be the correct place to put my ini file. If I launch the EMSDevServer.exe from this directory, the ini file gets read properly and FDConnection parameters get updated.
However, when I launch the RAD Server through IIS using the ISAPI dlls, it appears the ini file is not found as my database connections fail.
I have tried putting the ini file in the C:\Users\Public\Documents\Embarcadero\EMS directory and that did not work either.
Following is my code to access the ini file which is called on DataModuleCreate.
procedure TdmSecurity.DataModuleCreate(Sender: TObject);
begin
SetConnectionStr(FDConnectionSTIKS);
end;
procedure SetConnectionStr(var FDConnectionSTIKS: TFDConnection);
var ConfigIni: TInifile;
DBServerName, DBName, Path: string;
begin
Path := GetCurrentDir;
ConfigIni := TIniFile.Create(System.IOUtils.TPath.Combine(Path, 'Config.ini'));
// ConfigIni := TIniFile.Create(System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetDocumentsPath, 'Config.ini'));
// ConfigIni := TIniFile.Create('C:\Users\leonard\Projects\LumberNowEMS\Server\Win32\Debug\config.ini');
DBServerName := ConfigIni.ReadString(AppNode, 'ServerName', 'ZEUS');
DBName := ConfigIni.ReadString(AppNode, 'DataBase', 'NOTHING');
// showmessage(configini.FileName);
with FDConnectionSTIKS.Params as TFDPhysMSSQLConnectionDefParams do
begin
DriverID := 'MSSQL';
Server := DBServerName;
Database := DBName;
UserName := DBUserID;
Password := DBPassword;
end;
ConfigIni.Free;
// showmessage(Path + '; DBName:' +DBName);
// Result := Format('DriverID=MSSQL;Server=%s;Database=%s;User_name=%s;Password=%s', [DBServerName, DBName, DBUserID, DBPassword]);
end;
I expected that the IIS would read the ini file from the same place but it does not appear to be so. Can someone tell me where I should put the ini file so IIS can access it correctly or perhaps a better way of setting the database connection in Rad Server? Perhaps I could put my parameters in the emsserver.ini if there is a variable I can access.
#nolaspeaker - I would give you credit for the answer but you only posted a comment. Your response prompted me to find a way to determine the path that was being used. So I wrote the value of the path variable to a known text file.
Path := GetCurrentDir;
AssignFile(F, 'C:\Temp\Data.txt');
Rewrite(F);
WriteLn(F, Path);
CloseFile(F);
The path that is being used is 'C:\Windows\SysWOW64\inetsrv';
I moved my Config.ini file to this directory and it was found and read correctly.
Thanks for the responses.
Quite late, still posting an answer.
Functions in System.SysUtils will be helpful here.
following function call would give you the path to the directory where the .bpl file resides.
ExtractFilePath(GetModuleName(HInstance));
place the ini along with .bpl file, combining the above function's result with the Ini filename will help you to target an ini.

Application cant access file on startup

I'm doing an application(XE6 , Firemonkey) to synchronize files between a shared folder and a computer/s. This application checks every x hours if there are new files to be synchronized, and it starts on windows start-up.
I can do everything, my application starts on start-up, and it does the synchronization, as long as i'm the one starting it. Whem the application auto starts on start up it gives me an exception "EINOUTERROR" - File Access Denied.
On starting the application reads a small .txt file to set up it self (shared folder location, rate of synchronization etc), my guess is that since its the windows starting the app runs it without privileges to read the .txt, but even after changing the .txt permissions to full control on everyone it gives the same error.
File open code:
AssignFile(myFile,'Dados.txt');
if FileExists('Dados.txt') then
Append(myFile)
else
Rewrite(myFile);
FileMode := fmOpenRead;
Reset(myFile);
Code of placing the app on startup programs :
procedure TSyncM.RunOnStartup(const sCmdLine: string; bRunOnce: boolean; Remove: Boolean) ;
var sKey: string;
Section: string;
const ApplicationTitle = 'GEN4Sync';
begin
if (bRunOnce) then
sKey := 'Once'
else
sKey := '';
Section := 'Software\Microsoft\Windows\CurrentVersion\Run' + sKey + #0;
with TRegIniFile.Create('') do
try
RootKey := HKEY_CURRENT_USER;
if Remove then
DeleteKey(Section, ApplicationTitle)
else
WriteString(Section, ApplicationTitle, sCmdLine) ;
finally
Free;
end;
end;
If i comment the piece of code that calls the reading of that .txt my app starts and executes well, but i don't want to set it up everytime.
Thanks in advance
I think that the issue is related to your use of relative paths. You have written the code under the assumption that the working directory is the same directory as contains the executable. That is not necessarily so.
When you start the application by double clicking on the executable file, for instance, the shell ensures that the initial working directory is the directory containing the executable file. However, when Windows starts your program at startup I suspect that the working directory is the system directory. And of course your file is not found there, and you don't have rights to write there.
Instead of using relative paths, use the full path to the file.
FileName := ExtractFilePath(ParamStr(0)) + 'Dados.txt';
Or perhaps
FileName := TPath.Combine(ExtractFilePath(ParamStr(0)), 'Dados.txt');
Note that this does also assume that your executable file is located in a folder which you can write to. That is often not the case so you may need to find a different location.
I do have to comment that I find it somewhat incongruous that you are mixing the very modern (FireMonkey) with the ancient (Pascal I/O). Perhaps it is time to move to a more modern I/O technique.

How to fix madExcept creating temporal files in User\LocalSettings\Temp

I sterated using "Standard User Analyzer" from Application Compatibility toolkit and it reported that my app is not UAC compatible because:
"DeleteFileA: File (\Device\HarddiskVolume1\Documents and Settings\Administrator\Local Settings\Temp\mtgstudio.madExcept) is denied 'DELETE' access with error 0x5."
"DeleteFileA: File (\Device\HarddiskVolume1\Documents and Settings\Administrator\Local Settings\Temp) is denied 'DELETE' access with error 0x5."
Checking the madExcept.pas file I found:
function GetTempPath : AnsiString;
var arrCh : array [0..MAX_PATH] of AnsiChar;
begin
if windows.GetTempPathA(MAX_PATH, arrCh) > 0 then begin
result := arrCh;
if result <> '' then begin
CreateDirectoryA(PAnsiChar(result), nil);
if result[Length(result)] <> '\' then
result := result + '\';
result := result + KillExt(ExtractFileName(ModuleName(0))) + '.madExcept';
CreateDirectoryA(PAnsiChar(result), nil);
result := result + '\';
end;
end else
result := '';
end;
Is there a good way to overwrite the madExcept behaviour and store the temp files in a UAC allowed location?
It doesn't look like there's anything to fix. The GetTempPath API function is exactly the function to use to get a location where a program is allowed to create temporary files. That the compatibility tester was unable to delete the directories doesn't mean that the directories should have been someplace else. It only means they couldn't be deleted at the time the program tried. It could be that another program (such as the one being tested) had a file open in one of those directories; Windows doesn't allow folders to be deleted when there are open files in them.
One possible source of problems is the way MadExcept creates the directories. It creates them such that they inherit the permissions of their parent directories. If deletion is forbidden for the parent directory, then it will also be forbidden for the newly created temp directories. That partly points to a configuration problem on your system: GetTempPath might be returning a path for a directory that doesn't exist. It just returns the first value it finds in any of the TMP, TEMP, and USERPROFILE environment variables. It's the user's responsibility (not your program's) to make sure those are accurate.
Knowing that MadExcept uses GetTempPath to discover the temp directory gives you an opportunity. You can call SetEnvironmentVariable to change the TMP value for your process, and MadExcept will create its directory there instead. (But if the system-designated location for temporary files already doesn't work, good luck finding some alternative to use.)

Quickest way to find the oldest file in a directory using Delphi

HI
We have a large number of remote computers that capture video onto disk drives. Each camera has it's own unique directory and there can be up to 16 directories on any one disk.
I'm trying to locate the oldest video file on the disk but using FindFirst/FindNext to compare the File Creation DateTime takes forever.
Does anybody know of a more efficient way of finding the oldest file in a directory? We remotely connect to the pc's from a central HO location.
Regards, Pieter
-- Update
Thank you all for the answers. In the end I used the following.
Map a drive ('w:') to the remote computer using windows.WNetAddConnection2
//Execute dir on the remote computer using cmd.exe /c dir
//NOTE: Drive letters are relative to the remote computer. (psexec -w parameter)
psexec \\<IPAddress> -i /accepteula -w "c:\windows\system32" cmd.exe "/c dir q:\video /OD /TC /B > q:\dir.txt"
//Read the first line of "w:\dir.txt" to get the oldest file in that directory.
//Disconnect from the remote computer using windows.WNetCancelConnection2
You could also try FindFirstFileEx with FindExInfoBasic parameter, and on Windows 7 or Server 2008 R2 or later, FIND_FIRST_EX_LARGE_FETCH which should improve performance.
First, grab the RunDosAppPipedToTStrings routine from this page on how to run a DOS program and pipe its output to a TStrings. The example uses a TMemo's Lines property, but you can pass any TStrings in, such as TStringList. Note that this will fail silently if CreateProcess returns false. You might want to add an else case to the "if CreateProcess" block that raises an exception.
Then create a simple batch file in the same folder as your EXE. Call it getdir.bat. All it should say is:
dir %1
This produces a directory listing of whatever folder you pass to it. Unfortunately, "dir" is a DOS keyword command, not a program, so you can't invoke it directly. Wrapping it in a batch file gets around that. This is a bit of a hack, but it works. If you can find a better way to run DIR, so much the better.
You'll want to invoke RunDosAppPipedToTStrings with code that looks something like this:
procedure GetDirListing(dirname: string; list: TStringList);
const
CMDNAME = '%s\getdir.bat "%s"';
var
path: string;
begin
list.Clear;
path := ExcludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
RunDosAppPipedToTStrings(format(CMDNAME, [path, dirname]), list, false);
end;
Then all that's left to do is parse the output, extract date and time and filenames, sort by date and time, and grab the filename of the file with the lowest date. I'll leave that much to you.
If you can run something on the remote computer that can iterate over the directories, that will be the fastest approach. If you wanted to use Mason's example, try launching it with PsExec from SysInternals.
If you can only run an application locally then no, there's no faster way than FindFirst/FindNext, and anything else you do will boil down to that eventually. If your local computer is running Windows 7 you can use FindFirstFileEx instead, which has flags to indicate it should use larger buffers for the transfers and that it shouldn't read the 8.3 alias, which can help the speed a bit.
I had almost the same problem on the fax server software I developed. I had to send the faxes in the order they were received from thousands (all stored in a directory). The solution I adopted (which is slow to start but fast to run) is to make a sorted list of all the files using the
SearchRec.Time
as the key. After the file is in the list, I'm setting the attributes of the file as a faSysFile:
NewAttributes := Attributes or faSysFile;
Now when I do a new search with
FileAttrs := (faAnyFile and not faDirectory);
only the files that are not faSysFile are shown, so I can add to the list the files that are coming in new.
Now you have a list with all the files sorted by time.
Don't forget, when you start your application, first step is to remove the faSysFile attribute from the files in the folder so they can be processed again.
procedure FileSetSysAttr(AFileName: string);
var
Attributes, NewAttributes: Word;
begin
Attributes := FileGetAttr(AFileName);
NewAttributes := Attributes or faSysFile;
FileSetAttr(AFileName, NewAttributes);
end;
procedure FileUnSetSysAttr(AFileName: string);
var
Attributes, NewAttributes: Word;
begin
Attributes := FileGetAttr(AFileName);
NewAttributes := Attributes and not faSysFile;
FileSetAttr(AFileName, NewAttributes);
end;
procedure PathUnSetSysAttr(APathName: string);
var
sr: TSearchRec;
FileAttrs: Integer;
begin
FileAttrs := (faAnyFile and not faDirectory) and (faAnyFile or faSysFile);
APathName := IncludeTrailingBackslash(APathName);
if SysUtils.FindFirst(APathName + '*.*', FileAttrs, sr) = 0 then
try
repeat
if (sr.Attr and faDirectory) = 0 then
FileUnSetSysAttr(APathName + sr.Name);
until SysUtils.FindNext(sr) <> 0;
finally
SysUtils.FindClose(sr);
end;
end;
I know this is not the best solution, but works for me.

Resources