How to set the working directory using JCL's TJclCommandLineTool? - delphi

I'm using the JCL Delphi library in my project. Below is my code.
// uses JclSysUtils;
var cmd := TJclCommandLineTool.Create(parts[0]);
if cmd.Execute(params) then
begin
...
end;
As you can see, I use TJclCommandLineTool to invoke an external command line program. The problem is that the external program is always running in the current directory that I started my application. I want to know how can I pass a custom working directory to the external program. JCL's document is pretty lame and I can't find related information.

TJclCommandLineTool does not have options like this. So easiest way to do it:
//uses System.IOUtil;
var oldDir := TDirectory.GetCurrentDirectory;
try
TDirectory.SetCurrentDirectory(ANewProgramDir);
var cmd := TJclCommandLineTool.Create(parts[0]);
if cmd.Execute(params) then
begin
// ...
end;
finally
TDirectory.SetCurrentDirectory(oldDir);
end;

Related

How to solve [FireDAC][Phys][SQLite] ERROR: unable to open database file, When app is installed in programFiles?

I developed a tool using Firedac with the database as SQLite.
after finishing the project and making an installer (InnoSetup) I get an error
[FireDAC][Phys][SQLite] ERROR: unable to open database file
when I launch the app (double click).
This is the connection parameters I use
constructor TDbInteract.Create(const aDatabasePath: string; const aOnNeedCredentials: TOnNeedCredentials);
var
aParams: array of string;
begin
if not TFile.Exists(aDatabasePath) then
raise Exception.Create('Database file not found');
aParams := ['DriverID=SQLite',
'Database=' + aDatabasePath,
'OpenMode=CreateUTF16',
'LockingMode=Normal',
'JournalMode=WAL',
'StringFormat=Unicode',
'Synchronous=Full',
'UpdateOptions.LockWait=True',
'BusyTimeout=30000',
'SQLiteAdvanced=temp_store=MEMORY;page_size=4096;auto_vacuum=FULL'];
InitiateResource(aParams, aOnNeedCredentials);
end;
procedure TDbInteract.InitiateResource(const aParams: array of string; const aOnNeedCredentials: TOnNeedCredentials);
var
I: Integer;
Credentials: TStringDynArray;
begin
FRowsAffected := 0;
FIsForeignKeyHonored := True;
FOwnsResultDataSets := True;
FDataSetContainer := TDataSetContainer.Create(nil);
FConnection := TFDConnection.Create(nil);
try
for I := Low(aParams) to High(aParams) do
begin
FConnection.Params.Add(aParams[I]);
end;
if Assigned(aOnNeedCredentials) then
begin
aOnNeedCredentials(Self, Credentials);
for I := Low(Credentials) to High(Credentials) do
begin
FConnection.Params.Add(Credentials[I]);
end;
end;
FConnection.Open;
except
raise;
end;
end;
**Identified problems:
I read somewhere (do not remember the page I was in) that SQLite engine requires full lock on the directory that it wants to write to. and this is the problem. How ever I run the tool as invoker and my account is an admin so that is not a problem. Also I have the same tool written in c# and this problem never occurs.
Solutions I found:
Run the tool as administrator
Do not Install the tool in ProgramFiles directory
I really don't like these solutions. and would like to run my tool from program Files directory as it is part of a bigger project.
Note: The database file is in programdata directory. It is created by the tool (this works).
Edit: I just tried putting the DB file in C:\Users\Nacereddine\AppData\Roaming\MyTool And I still have the same problem when the tool is installed in C:\Program Files (x86)\MyTool
This how I create the DB file
class procedure TDbInteract.CreateSQLiteDb(const aDatabasePath: string; const aTables: TStringDynArray);
var
I: Integer;
aParams: array of string;
aConnection: TFDConnection;
begin
aParams := ['DriverID=SQLite',
'Database=' + aDatabasePath,
'OpenMode=CreateUTF16',
'LockingMode=Normal',
'JournalMode=WAL',
'StringFormat=Unicode',
'Synchronous=Full',
'UpdateOptions.LockWait=True',
'BusyTimeout=30000',
'SQLiteAdvanced=temp_store=MEMORY;page_size=4096;auto_vacuum=FULL'];
aConnection := TFDConnection.Create(nil);
try
for I := Low(aParams) to High(aParams) do
begin
aConnection.Params.Add(aParams[I]);
end;
aConnection.Open();
for I := Low(aTables) to High(aTables) do
begin
aConnection.ExecSQL(aTables[I]);
end;
finally
aConnection.Close;
aConnection.Free;
end;
end;
Note: I do not know if this makes any difference but the Db file is encrypted.
Sorry for the trouble folks.
The problem was that we had a Localization db file installed with the tool in ProgramFiles.
What made me exclude that from my investigation is that, when opening this file I set the OpenMode to ReadOnly
FConnection.Params.Add('OpenMode=ReadOnly');
but as I said before in my question SQLite engine requires full access to the folder containing the db file so it preforms a lock on it (still did not find the page I read this on).
I checked this by playing around with open modes and debugging the tool each time.
once I changed the permissions of both the file and the directory the error was gone.
at the end I decided to move the localization file to the programData directory with the main db file and all is well.
I realized (Thank you for this #Ken and #David) that the programData directory also requires admin permissions to write to, and therefore I will move the db files to a more appropriate dir (i.e Users).
What is useful from this problem is that even if you connect to the Sqlite db file with OpenMode=ReadOnly, you still need write access for the path to that file.

run program with administrator access to write to registery

i tried to use this tutorial to work:
https://stackoverflow.com/a/14710803/1149172
first create a file with name uac.manifest with provided content
then create uac.rc width content of 1 24 "uac.manifest"
then copied the files to program folder (in delphi program sources folder) and changed the project file like this:
program Project4;
{.$R 'uac.res' 'uac.rc'} // UAC only
uses
Vcl.Forms,
Unit6 in 'Unit6.pas' {Form6};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm6, Form6);
Application.Run;
end.
at last i put my registery code at the form
procedure AddEntryToRegistry;
var key: string;
Reg: TRegIniFile;
begin
key := 'Software\Microsoft\Windows\CurrentVersion\Run';
Reg := TRegIniFile.Create;
try
Reg.RootKey:=HKEY_LOCAL_MACHINE;
Reg.CreateKey(Key);
if Reg.OpenKey(Key,False) then Reg.WriteString(key, 'MyApp', 'c:MyApp.exe');
finally
Reg.Free;
end;
end;
every thing seems ok and i dont got any runtime rror
but after clicking the button nothing happened (dont shoe any error and dont add the key to registery)!!!
where is wrong with my works!?
my delphi is xe5 and working on win 8ul
Firstly, I'm going to take it as read that your program is running elevated. If that's not happening then there's no need to look at any code. You did not say otherwise, so let us proceed under the assumption that you are succeeding to elevate.
You are suffering from the registry redirector. Your 32 bit process is running on a 64 bit machine. And so HKLM\Software is redirected to the 32 bit view, stored at HKLM\Software\Wow6432Node.
You can, if you need, use the KEY_WOW64_64KEY flag to access the 64 bit view. Combine this with the flags in the registry object's Access property.
However, the system reads keys from both 32 and 64 bit views of the registry when enumerating the startup programs so you do not need to do this. For the sake of simplicity and predictability I would leave your 32 bit program writing to the 32 bit view.
Your call to CreateKey should be removed. The system creates that key and you can safely assume it exists. And you should not use TRegIniFile. Use TRegistry instead.
Your code should look like this:
procedure AddEntryToRegistry;
var
Reg: TRegistry;
begin
Reg := TRegistry.Create(KEY_ALL_ACCESS);
try
Reg.RootKey := HKEY_LOCAL_MACHINE;
if Reg.OpenKey('Software\Microsoft\Windows\CurrentVersion\Run', False) then
Reg.WriteString('MyApp', 'C:\MyApp.exe');
finally
Reg.Free;
end;
end;
Should you feel that you need to write to the 64 bit view then it is done like this:
procedure AddEntryToRegistry;
var
Reg: TRegistry;
begin
Reg := TRegistry.Create;
try
Reg.RootKey := HKEY_LOCAL_MACHINE;
Reg.Access := KEY_ALL_ACCESS or KEY_WOW64_64KEY;
if Reg.OpenKey('Software\Microsoft\Windows\CurrentVersion\Run', False) then
Reg.WriteString('MyApp', 'C:\MyApp.exe');
finally
Reg.Free;
end;
end;
Judging by your comments, it looks like you might be failing to elevate. The lack of a UAC dialog when your program starts is the tell-tale sign that this is happening. Once your program starts without a UAC dialog, there's no point continuing. You will not write to HKLM without elevation.
Regarding your manifest, you can link only one. So if you want to specify a manifest other than the Enable runtime themes manifest that the IDE can provide, you need to do it all yourself.
In the project options specify that you want to use a custom manifest. That's under Project | Options | Application | Runtime themes. Set the drop down to Use custom manifest. And then supply the file name of your manifest. You'll want to add in the comctl32 v6 part to make sure that you get runtime themes. But don't worry about that now. Just concentrate on getting elevation sorted, and the registry code working.
You are also silently ignoring any errors which does make things a little harder to debug. If it so happens that you are not elevating, then running the code is rather pointless. You know it must fail. But you could at least make it easier to diagnose the problem by throwing an error if OpenKey fails.
procedure AddEntryToRegistry;
var
Reg: TRegistry;
begin
Reg := TRegistry.Create(KEY_ALL_ACCESS);
try
Reg.RootKey := HKEY_LOCAL_MACHINE;
if not Reg.OpenKey('Software\Microsoft\Windows\CurrentVersion\Run', False) then
raise EMyExceptionClass.Create('Could not open registry key');
Reg.WriteString('MyApp', 'C:\MyApp.exe');
finally
Reg.Free;
end;
end;
One final point to make is that writing to this registry key is an admin task. You should require elevation once only, not every time your application starts. If you are planning to require elevation for your application just for this purpose, then you must re-design. This admin task should be performed outside your main application. The most natural place is inside your install program which users will accept requiring elevation.
You are saving your app path to the Run key of the HKEY_LOCAL_MACHINE hive. You should be using the HKEY_CURRENT_USER hive instead, then you will not need to use UAC elevation anymore (unless your app is doing other things that require admin rights).

Create and/or Write to a file

I feel like this should be easy, but google is totally failing me at the moment. I want to open a file, or create it if it doesn't exist, and write to it.
The following
AssignFile(logFile, 'Test.txt');
Append(logFile);
throws an error on the second line when the file doesn't exist yet, which I assume is expected. But I'm really failing at finding out how to a) test if the file exists and b) create it when needed.
FYI, working in Delphi XE.
You can use the FileExists function and then use Append if exist or Rewrite if not.
AssignFile(logFile, 'Test.txt');
if FileExists('test.txt') then
Append(logFile)
else
Rewrite(logFile);
//do your stuff
CloseFile(logFile);
Any solution that uses FileExists to choose how to open the file has a race condition. If the file's existence changes between the time you test it and the time you attempt to open the file, your program will fail. Delphi doesn't provide any way to solve that problem with its native file I/O routines.
If your Delphi version is new enough to offer it, you can use the TFile.Open with the fmOpenOrCreate open mode, which does exactly what you want; it returns a TFileStream.
Otherwise, you can use the Windows API function CreateFile to open your file instead. Set the dwCreationDisposition parameter to OPEN_ALWAYS, which tells it to create the file if it doesn't already exist.
You should be using TFileStream instead. Here's a sample that will create a file if it doesn't exist, or write to it if it does:
var
FS: TFileStream;
sOut: string;
i: Integer;
Flags: Word;
begin
Flags := fmOpenReadWrite;
if not FileExists('D:\Temp\Junkfile.txt') then
Flags := Flags or fmCreate;
FS := TFileStream.Create('D:\Temp\Junkfile.txt', Flags);
try
FS.Position := FS.Size; // Will be 0 if file created, end of text if not
sOut := 'This is test line %d'#13#10;
for i := 1 to 10 do
begin
sOut := Format(sOut, [i]);
FS.Write(sOut[1], Length(sOut) * SizeOf(Char));
end;
finally
FS.Free;
end;
end;
If you are just doing something simple, the IOUtils Unit is a lot easier. It has a lot of utilities for writing to files.
e.g.
procedure WriteAllText(const Path: string; const Contents: string);
overload; static;
Creates a new file, writes the specified string to the file, and then
closes the file. If the target file already exists, it is overwritten.
You can also use the load/save feature in a TStringList to solve your problem.
This might be a bad solution, because the whole file will be loaded into memory, modified in memory and then saved to back to disk. (As opposed to your solution where you just write directly to the file). It's obviously a bad solution for multiuser situations.
But this approach is OK for smaller files, and it is easy to work with and easy understand.
const
FileName = 'test.txt';
var
strList: TStringList;
begin
strList := TStringList.Create;
try
if FileExists(FileName) then
strList.LoadFromFile(FileName);
strList.Add('My new line');
strList.SaveToFile(FileName);
finally
strList.Free;
end;
end;

PascalScript including other script per uses or include command

I have included the PascalScript engine into my software. My user now wants to write a whole set of scripts for this engine, and would like to know if it is possible to include other Scripts by an include or uses commmand.
What he wants to do is write one script that holds all kinds of constants/variables and another that does the logic. In the end he wants to include the constants into his logic script.
I hope this was clear enough to understand.
I found out, heres how how do to it:
The UsePreprocessor Property of the PascalScript compiler needs to be set to true. If so you can now use the following preprocessor command:
{$I filename.txt}
Also you need to implement the OnNeedFile Event of the compiler with something like the following example that I found on the net:
function TForm1.ceNeedFile(Sender: TObject; const OrginFileName: String;
var FileName, Output: String): Boolean;
var
path: string;
f: TFileStream;
begin
Path := ExtractFilePath(ParamStr(0)) + FileName;
try
F := TFileStream.Create(Path, fmOpenRead or fmShareDenyWrite);
except
Result := false;
exit;
end;
try
SetLength(Output, f.Size);
f.Read(Output[1], Length(Output));
finally
f.Free;
end;
Result := True;
end;
(Please fix your question title)
I'm not entirely sure but afaik Pascalscript has no "file" concept. You could simply concat both parts before passing them to the interpreter, or have a small preprocessor look for the {$I } include statements and look the code to insert up.
If you set the conditional define to PS_USESSUPPORT
and in the OnFindUnknownFile event you have to load the pas-file content
into the output string.
Then you can use "Uses XYZ;".

fmShareDenyWrite mode doesn't appear to work

I'm using a TFileSteam to open a log file. I would like to be able to read through this log file from other processes. I thought the fmShareDenyWrite mode would allow this.
However if I try to open the file from other processes, I get an error. For example, if I try and type the file from the command line, I get "the process can not access the file because it is being used by another process".
Here is the file initialization code:
if FileExists(AutoLogFileName) then
_ActivityLogStream := TFileStream.Create(AutoLogFileName,
fmOpenReadWrite or fmShareDenyWrite)
else
_ActivityLogStream := TFileStream.Create(AutoLogFileName,
fmCreate or fmShareDenyWrite);
NOTE:
I am using Delphi version 6.
Don't know whether this was already a bug in D6, but that is a distinct possibility. There is a QC report on this reported against D2007: QC 65767: http://qc.embarcadero.com/wc/qcmain.aspx?d=65767. This report is now closed, as it was resolved in D2010 (14.0.3467.22472 to be exact).
Update (prompted by Gabr's comment):
You can create your own TFileStream descendant that does honor the mode. Just override the Create(const AFileName: string; Mode: Word; Rights: Cardinal) constructor (there are two overloaded constructors) and handle the mode parameter yourself. Copy the code from the original constructor and change the
if Mode = fmCreate then
begin
inherited Create(FileCreate(AFileName, Rights));
to
if (Mode and fmCreate = fmCreate) then
begin
myMode := Mode and $FF;
if myMode = $FF then
myMode := fmShareExclusive;
inherited Create(FileCreate(AFileName, myMode, Rights));
where myMode is a local var of type Word.
mfCreate mode does not behave/work correctly with any share attribute. To work around, you must create file handle yourself and pass it to the constructor
Cheer

Resources