I have an exception logger that logs all exceptions to a log file:
class function TLogger.LogException (ACaller: String; E: Exception): Boolean;
var
LogFilename, tmp: string;
LogFile: TextFile;
appsettings: TApplicationSettings;
begin
// prepare log file
appsettings:=TApplicationSettings.Create;
try
tmp:=appsettings.ErrorLogsLocation;
finally
FreeAndNil(appsettings);
end;
if NOT (DirectoryExists(tmp)) then
CreateDir(tmp);
//We create a new log file for every day to help with file size issues
LogFilename:=IncludeTrailingPathDelimiter (tmp) + 'LJErrors_' + FormatDateTime('yyyy-mm-dd', Now) +'.log';
try
AssignFile (LogFile, LogFilename);
if FileExists (LogFilename) then
Append (LogFile) // open existing file
else
Rewrite (LogFile); // create a new one
// write to the file and show error
Writeln(LogFile, CRLF+CRLF);
Writeln (LogFile, 'Application Path: ' + ExtractFilePath(ParamStr (0)));
Writeln (LogFile, 'Application Version: ' + TUtility.GetAppVersionString);
Writeln (LogFile, 'Operating System: ' + TUtility.GetOSInfo);
Writeln (LogFile, 'Error occurred at: ' + FormatDateTime ('dd-mmm-yyyy hh:nn:ss AM/PM', Now));
Writeln (LogFile, 'Logged By: ' + ACaller);
Writeln (LogFile, 'Unit Name: ' + E.UnitName);
Writeln (LogFile, 'Error Message: ' + E.Message);
Writeln (LogFile, 'Error Class: ' + E.ClassName);
Writeln (LogFile, 'Base Exception Error: ' + E.BaseException.Message);
Writeln (LogFile, 'Base Exception Class: ' + E.BaseException.ClassName);
Writeln (LogFile, 'Stack Trace: ' + E.StackTrace);
Result:=True;
finally
// close the file
CloseFile (LogFile);
end;
end;
To enable the Exception.StackTrace, I am using JCLDebug as outlined in: https://blog.gurock.com/working-with-delphis-new-exception-stacktrace.
unit StackTrace;
interface
uses
SysUtils, Classes, JclDebug;
implementation
function GetExceptionStackInfoProc(P: PExceptionRecord): Pointer;
var
LLines: TStringList;
LText: String;
LResult: PChar;
begin
LLines := TStringList.Create;
try
JclLastExceptStackListToStrings(LLines, True, True, True, True);
LText := LLines.Text;
LResult := StrAlloc(Length(LText));
StrCopy(LResult, PChar(LText));
Result := LResult;
finally
LLines.Free;
end;
end;
function GetStackInfoStringProc(Info: Pointer): string;
begin
Result := string(PChar(Info));
end;
procedure CleanUpStackInfoProc(Info: Pointer);
begin
StrDispose(PChar(Info));
end;
initialization
// Start the Jcl exception tracking and register our Exception
// stack trace provider.
if JclStartExceptionTracking then
begin
Exception.GetExceptionStackInfoProc := GetExceptionStackInfoProc;
Exception.GetStackInfoStringProc := GetStackInfoStringProc;
Exception.CleanUpStackInfoProc := CleanUpStackInfoProc;
end;
finalization
// Stop Jcl exception tracking and unregister our provider.
if JclExceptionTrackingActive then
begin
Exception.GetExceptionStackInfoProc := nil;
Exception.GetStackInfoStringProc := nil;
Exception.CleanUpStackInfoProc := nil;
JclStopExceptionTracking;
end;
end.
I have enabled the following options in Project Options:
Compiling: Debug Information, Local Symbols, Symbol Reference Info, Use debug .dcus, Use imported data references
Linking: Debug Information
However, when I trigger an exception, even though the GetExceptionStackInfoProc gets triggered, the Exception.StackInfo is always an empty string. Any ideas as to what I may be missing?
UPDATE 20170504: Thanks to Stefan Glienke for the solution. For completeness, I am including the code here for the changed GetExceptionStackInfoProc procedure that incorporates his solution:
function GetExceptionStackInfoProc(P: PExceptionRecord): Pointer;
var
LLines: TStringList;
LText: String;
LResult: PChar;
jcl_sil: TJclStackInfoList;
begin
LLines := TStringList.Create;
try
jcl_sil:=TJclStackInfoList.Create(True, 7, p.ExceptAddr, False, nil, nil);
try
jcl_sil.AddToStrings(LLines, true, true, true, true);
finally
FreeAndNil(jcl_sil);
end;
LText := LLines.Text;
LResult := StrAlloc(Length(LText));
StrCopy(LResult, PChar(LText));
Result := LResult;
finally
LLines.Free;
end;
end;
You need to create it yourself (and free it):
TJclStackInfoList.Create(True, 7, p.ExceptAddr, False, nil, nil);
On that instance you can call AddToStrings.
For more info take a look at JclDebug.GetExceptionStackInfo. The value for AIgnoreLevels is taken from there but in my tests I always had one entry too much so I increased it by one.
The following it a snippet from what I get from a Button1 application calling RaiseLastOSError;
[0042BFE5] System.SysUtils.Sysutils.RaiseLastOSError$qqrix20System.UnicodeString (Line 24937, "System.SysUtils.pas")
[0042BF5B] System.SysUtils.Sysutils.RaiseLastOSError$qqrv (Line 24919, "System.SysUtils.pas")
[005CD004] Unit85.TForm85.Button1Click$qqrp14System.TObject (Line 28, "Unit85.pas")
[0051D567] Vcl.Controls.TControl.Click$qqrv (Line 7429, "Vcl.Controls.pas")
[00534CDA] Vcl.StdCtrls.Stdctrls.TCustomButton.Click$qqrv (Line 5434, "Vcl.StdCtrls.pas")
If you actually read the JclDebug unit, you will see that initialization and finalization sections have registration and deregistration code, so simply placing the unit into the uses section is enough. (See SetupExceptionProcs)
Related
I wonder, why this code does not catch a 'Disk Full' error like it should?
This is important because the user may lose their data if they do not notice that the saving failed.
I don't get this...
procedure TForm2.Button1Click(Sender: TObject);
var
Writer: TStreamWriter;
n : integer;
begin
Writer := TStreamWriter.Create('MyUTF8Text.txt', false, TEncoding.UTF8);
Try //Finally
Try //Except
for n := 1 to 1000 do
begin
Writer.WriteLine('Testing text writing to the UTF-8 file.');
end;
Except
on E: Exception do
begin
ShowMessage('Exception Class name: ' + E.ClassName);
ShowMessage('Exception Message: ' + E.Message);
end;
end; // except
Finally
Writer.Free();
End; //finally
end;
"BTW. 'Writer might not be initialized' warning, is it serious really?"
Edit: There was this warning because TStreamWriter.Create was after TRY.
Thanks for your advice I corrected that line of code to the correct location before(!) the TRY.
Try This:
procedure WriteStream;
var
Writer: TStreamWriter;
fs: TFileStream;
s: String;
n, bytesWritten : integer;
begin
bytesWritten := 0;
fs := TFileStream.Create('MyUTF8Text.txt', fmCreate);
try
//avoid warning by initiaizing before try
Writer := TStreamWriter.Create(fs);
try //Finally
try //Except
for n := 1 to 10 do
begin
s := 'Testing text writing to the UTF-8 file.' + '#13#10';
//keep count of bytes written
bytesWritten := bytesWritten + TEncoding.UTF8.GetByteCount(s);
Writer.Write(s);
end;
Writer.Flush;
Writer.Close;
//Check stream size to make sure all bytes written
if bytesWritten <> Writer.BaseStream.Size then
raise Exception.Create(String.Format('Expected %d bytes, wrote %d', [Writer.BaseStream.Size, bytesWritten]));
except
on E: Exception do
begin
Showmessage('Exception Class name: ' + E.ClassName);
Showmessage('Exception Message: ' + E.Message);
end;
end; // except
finally
Writer.Free; // Will only free if it has been constructed
end; //finally
finally
fs.Free;
end;
end;
I'm reading this article that explain how to set a TLS callback in Delphi. The article author says the example works on "Delphi: 2007, 2010, XE4, XE10". But I have tested on Delphi 10 Seattle, Berlin, and Rio, and it does not work (the TLS callback is not executed), but when i test it on Delphi XE5, it works fine.
I also noted that the size of the .map file when compiling the test_app project in Delphi XE5 and Delphi 10 are different. The .map file in Delphi 10 is 5x bigger than the .map file in Delphi XE5 (something around 25KB and 125KB, respectively).
What detail am I missing here?
Following is the code with a reasonable translation to English of the add_tls project and the test_app project.
PS: The test_app project needs to be set to generate a .map file. Project > Options > Linking > Map file => Detailed.
add_tls:
program add_tls;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
Classes,
SysUtils,
Generics.Collections;
procedure ShowHelp;
begin
Writeln('Usage: AddTls.exe "executable path"');
Writeln('Return Codes:');
Writeln(' - 0: TLS Callback successfully added');
Writeln(' - 1: the path to the executable file is not specified');
Writeln(' - 2: executable not found');
Writeln(' - 3: MAP file not found matching the specified file');
Writeln(' - 4: MAP file parsing error');
Writeln(' - 5: error accessing executable file');
Writeln(' - 6: there is no initialized TLS section in the executable file');
end;
type
TSectionData = record
Index: Integer;
StartAddr: DWORD;
SectionName: ShortString;
end;
TSectionDataList = TList<TSectionData>;
const
HardcodeTLS32Offset = 12;
//
// This is an easy way to search for TLS BUT tables - only in projects,
// collected in XE and above
// If the executable is built by another compiler, it will not work naturally
// but the article is not about that :)
// so:
// =============================================================================
function GetTlsTableAddr(const FilePath: string): DWORD;
var
F: TFileStream;
DOS: TImageDosHeader;
NT: TImageNtHeaders;
I: Integer;
Section: TImageSectionHeader;
begin
Result := 0;
// open the file for reading
F := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
try
// read DOS header to go to NT
F.ReadBuffer(DOS, SizeOf(TImageDosHeader));
F.Position := DOS._lfanew;
// We read the NT header to get the number of sections
F.ReadBuffer(NT, SizeOf(TImageNtHeaders));
// read sections and look for TLS
for I := 0 to NT.FileHeader.NumberOfSections - 1 do
begin
F.ReadBuffer(Section, SizeOf(TImageSectionHeader));
if PAnsiChar(#Section.Name[0]) = '.tls' then
begin
// found IMAGE_TLS_DIRECTORY, we immediately correct the AddressOfCallback field
Result := Section.PointerToRawData + HardcodeTLS32Offset;
Break;
end;
end;
finally
F.Free;
end;
end;
// just parse the map file and look for the addresses of the sections
function GetSectionDataList(const FilePath: string; var Index: Integer): TSectionDataList;
var
S: TStringList;
Line: string;
Section: TSectionData;
begin
Result := TSectionDataList.Create;
try
S := TStringList.Create;
try
S.LoadFromFile(FilePath);
Index := 0;
Writeln('I am looking for a table of sections...');
while Copy(Trim(S[Index]), 1, 5) <> 'Start' do
Inc(Index);
Inc(Index);
while Trim(S[Index]) <> '' do
begin
Line := Trim(S[Index]);
Section.Index := StrToInt(Copy(Line, 1, 4));
Delete(Line, 1, 5);
Section.StartAddr := StrToInt('$' + Copy(Line, 1, 8));
Delete(Line, 1, 19);
Section.SectionName := ShortString(Trim(Copy(Line, 1, 8)));
Result.Add(Section);
Inc(Index);
end;
Writeln('Total sections found: ', Result.Count);
finally
S.Free;
end;
except
// we suppress all exceptions. there are error codes
on E: Exception do
Writeln('GetSectionDataList: ' + E.ClassName + ': ' + E.Message);
end;
end;
// again, parse the mapfile and look for the address of the function called tls_callback
// which (if found) we summarize with the address of the section in which it is located
function GetTlsCallbackAddr(const FilePath: string;
SectionDataList: TSectionDataList; Index: Integer): DWORD;
var
S: TStringList;
Line: string;
SectionIndex, TlsAddr: Integer;
begin
Result := 0;
try
S := TStringList.Create;
try
S.LoadFromFile(FilePath);
Writeln('Looking for tls_callback...');
repeat
Line := Trim(S[Index]);
Inc(Index);
if Index = S.Count then Break;
until Pos('.tls_callback', Line) <> 0;
if Pos('.tls_callback', Line) = 0 then
begin
Writeln('No tls_callback entry found in MAP file');
Exit;
end;
SectionIndex := StrToInt(Copy(Line, 1, 4));
Delete(Line, 1, 5);
TlsAddr := StrToInt('$' + Copy(Line, 1, 8));
Writeln('tls_callback found, offset: ', IntToHex(TlsAddr, 8), ', section: ', SectionIndex);
Writeln('Looking for a record about the section...');
for Index := 0 to SectionDataList.Count - 1 do
if SectionDataList[Index].Index = SectionIndex then
begin
Result := SectionDataList[Index].StartAddr + DWORD(TlsAddr);
Writeln('TLS Callback, found in section "', SectionDataList[Index].SectionName,
'", offset sections: ', IntToHex(SectionDataList[Index].StartAddr, 8),
', calculated addressc: ', IntToHex(Result, 8));
Break;
end;
if Result = 0 then
Writeln('Section containing tls_callback not found')
finally
S.Free;
end;
except
// we suppress all exceptions. there are error codes
on E: Exception do
Writeln('GetTlsCallbackAddr: ' + E.ClassName + ': ' + E.Message);
end;
end;
// directly patch file
function Patch(const FilePath, MapPath: string; TlsTable, CallbackAddr: DWORD): Boolean;
var
F: TFileStream;
NewFilePath, BackUpFilePath: string;
OldCallbackTableAddr: DWORD;
begin
Result := False;
try
NewFilePath := ExtractFilePath(FilePath) + 'tls_aded_' +
ExtractFileName(FilePath);
Writeln('I create a copy of the file, the path: ', NewFilePath);
CopyFile(PChar(FilePath), PChar(NewFilePath), False);
F := TFileStream.Create(NewFilePath, fmOpenReadWrite);
try
Writeln('File open');
F.Position := TlsTable;
// read the address where the previous callback referred
F.ReadBuffer(OldCallbackTableAddr, 4);
// in a delphi image, it refers to the SizeOfZeroFill structure of IMAGE_TLS_DIRECTORY
// in which both last fields are filled with zeros (supposedly there is no callback chain)
// Therefore, we will not spoil the working structure and make it refer to the address
// immediately outside of this structure (plus 2 yards in 32 bit, in 64 bit)
Inc(OldCallbackTableAddr, SizeOf(DWORD) * 2);
F.Position := TlsTable;
// write a new address to the old place
F.WriteBuffer(OldCallbackTableAddr, 4);
Writeln('Assigned a new address to the chain of processors, offset: ', IntToHex(TlsTable, 8),
', new value: ', IntToHex(OldCallbackTableAddr, 8));
// now we jump to the place where the VA address of the handler (not RVA) should be written
// skip SizeOfZeroFill and Characteristics and get right behind them
F.Position := TlsTable + SizeOf(DWORD) * 3;
// and now write the address of our callback
F.WriteBuffer(CallbackAddr, 4);
Writeln('Callback address set, offset: ', IntToHex(TlsTable + SizeOf(DWORD) * 3, 8));
// after which we write zero to indicate the end of the callback chain
CallbackAddr := 0;
F.WriteBuffer(CallbackAddr, 4);
finally
F.Free;
end;
// if everything is fine, then rename back
Writeln('I create a backup');
BackUpFilePath := FilePath + '.bak';
DeleteFile(BackUpFilePath);
RenameFile(FilePath, BackUpFilePath);
Writeln('I keep the result');
RenameFile(NewFilePath, FilePath);
Writeln('All tasks completed');
Result := True;
except
// we suppress all exceptions. there are error codes
on E: Exception do
begin
// in the event of an error, we clean ourselves up - returning everything back
DeleteFile(NewFilePath);
RenameFile(BackUpFilePath, FilePath);
Writeln('Patch: ' + E.ClassName + ': ' + E.Message);
end;
end;
end;
var
MapPath: string;
TlsTable, CallbackAddr: DWORD;
SectionDataList: TSectionDataList;
Index: Integer;
begin
ExitCode := 0;
if ParamCount = 0 then
begin
ShowHelp;
ExitCode := 1;
ExitProcess(ExitCode);
end;
if not FileExists(ParamStr(1)) then
begin
Writeln('No executable found: ', ParamStr(1));
ExitCode := 2;
ExitProcess(ExitCode);
end;
TlsTable := GetTlsTableAddr(ParamStr(1));
if TlsTable = 0 then
begin
ExitCode := 6;
ExitProcess(ExitCode);
end;
MapPath := ChangeFileExt(ParamStr(1), '.map');
if not FileExists(MapPath) then
begin
Writeln('MAP file not found: ', MapPath);
ExitCode := 3;
ExitProcess(ExitCode);
end;
Index := 0;
SectionDataList := GetSectionDataList(MapPath, Index);
try
if SectionDataList.Count = 0 then
begin
Writeln('Could not build partition table');
ExitCode := 9;
ExitProcess(ExitCode);
end;
CallbackAddr := GetTlsCallbackAddr(MapPath, SectionDataList, Index);
if CallbackAddr = 0 then
begin
ExitCode := 4;
ExitProcess(ExitCode);
end;
if not Patch(ParamStr(1), MapPath, TlsTable, CallbackAddr) then
ExitCode := 5;
finally
SectionDataList.Free;
end;
ExitProcess(ExitCode);
end.
test_app:
program test_app;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows;
// this callback will be called if the file is correctly patched
procedure tls_callback(hModule: HMODULE;
ul_reason_for_call: DWORD; lpReserved: Pointer); stdcall;
begin
if ul_reason_for_call = DLL_PROCESS_ATTACH then
MessageBox(0, 'TLS Callback Message', nil, 0);
end;
const
ptls_callback: Pointer = #tls_callback;
begin
// so that the tls_callback procedure appears in the MAP file
// you need a link to it, it’s corny like this:
if ptls_callback <> nil then
MessageBox(0, 'Entry Point Message', nil, 0);
end.
If your aim is to have some code execute as soon as possible, here is something which works on any Delphi revision, and on any platform (not only Windows).
Create a small unit with no dependency (no uses clause at all).
unit FirstLoaded;
interface
// NO "uses" clause!
implementation
procedure SomeThingToDoEarly;
begin
end;
initialization
SomeThingToDoEarly;
end.
Then put it as first unit in the uses clause of your project .dpr - before anything else.
program Project1;
uses
FirstLoaded, // before anything!
Forms,
Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
The code in the initialization part of your unit will be called just after system.pas.
Be aware that if you add something in your uses clause of your unit, those units (and their dependencies) would be initialized first.
What is wrong in this code ? I don't understend, if I remove the "Try" my app dont open, and if don't remove always appear "need login" ...
procedure TF_login.FormActivate(Sender: TObject);
var
Result: Integer;
TextFile: TStringList;
VarArquivo: string;
text: string;
dataI, dataF : string;
begin
TextFile := TStringList.Create;
VarArquivo := System.IOUtils.TPath.GetDocumentsPath + PathDelim + 'Limit.txt';
try
TextFile.LoadFromFile(VarArquivo);
text := TextFile.Text;
// ShowMessage(TextFile.Text); // there is the text
// ShowMessage(text); // there is the text
dataI := FormatDateTime('dd/mm/yyyy', Now);
dataF := FormatDateTime('dd/mm/yyyy', StrToDate(text));
Result := CompareDate(StrToDate(dataI), StrToDate(dataF));
ShowMessage(dataF +' data f');
ShowMessage(dataI +' data I');
if ( Result = LessThanValue ) then
begin
ShowMessage('data F low');
end
else
begin
ShowMessage('data F high');
F_inicio.Show;
end;
FreeAndNil(TextFile);
except on E:
Exception do ShowMessage('An error happened!' + sLineBreak + '[' +
E.ClassName + '] ' + E.Message);
end;
end;
The error : [EConvertError] '09/11/2019' is not a valid date
to create the file, i do:
procedure TF_login.btn_entrarClick(Sender: TObject);
var
data : tdatetime;
Resposta, data_s: string;
begin
PathFile := System.IOUtils.TPath.GetDocumentsPath;
NameFile := 'Limit.txt';
data := Now; //data actual
data := IncMonth(data, 2);
data_s := FormatDateTime('dd/mm/yyyy', data);
TFile.WriteAllText(TPath.Combine(PathFile, NameFile), data_s );
F_inicio.Show;
end;
The file exists, because the first (and second) ShowMessage (what is commented) show me the "09/11/19" but the third and fourth not appear to me...
OBS: Delphi 10.3 (RIO), Plataform: Android
There are a couple of things that you should change in your code:
procedure TF_login.FormActivate(Sender: TObject);
var
TextFile: TStringList;
VarArquivo: string;
text: string;
dataI, dataF : string;
begin
// If an exception (unlikely, but on principle) happens in your VarArquivo
// assignment, then the original version will leak the allocated TStringList.
// Always place the TRY right after allocation of a memory block. That way
// you ensure that the FINALLY block will always release the allocated
// memory. Also, always include a FINALLY block to release the memory. Don't
// count on your code to reach the FreeAndNIL code (it doesn't in this
// instance, as you can see) to make sure that you actually release the
// memory.
VarArquivo := System.IOUtils.TPath.GetDocumentsPath + PathDelim + 'Limit.txt';
TextFile := TStringList.Create;
try // - except
try // - finally
TextFile.LoadFromFile(VarArquivo);
text := TextFile.Text;
// ShowMessage(TextFile.Text); // there is the text
// ShowMessage(text); // there is the text
dataI := FormatDateTime('yyyy/mm/dd', Date);
dataF := FormatDateTime('yyyy/mm/dd', StrToDate(text));
ShowMessage(dataF +' data f');
ShowMessage(dataI +' data I');
if ( dataF < dataI ) then
begin
ShowMessage('data F low');
end
else
begin
ShowMessage('data F high');
F_inicio.Show;
end;
finally
FreeAndNil(TextFile);
end
except
// NEVER just "eat" an exception. Especially not while developing the
// application.
// Always either log the exception or show it to the user.
on E:Exception do ShowMessage('Exception '+E.ClassName+': '+E.Message+#13#10+
'need login');
end;
end;
Now - if you do this, what exception and error message is shown. This is needed in order to properly diagnose the error. Perhaps you can even figure it out for yourself when you see what exactly goes wrong...
This program raises an I/O 104 error on EoF when first entering the while loop.
The purpose of the program is to look up if a username is already taken. The existing usernames are stored in a text file.
procedure TForm1.btnRegisterClick(Sender: TObject);
begin
sCUser := edtUserName.Text;
AssignFile(tNames, 'Names.txt');
begin
try
Reset(tNames);
except
ShowMessage('File not found');
Exit;
end;
end;
rewrite(tNames);
while not EoF(tNames) do // I get a I/O 104 Error here `
begin
Readln(tNames, sLine);
iPosComme := Pos(',', sLine);
sUser := Copy(sLine, 1, iPosComme - 1);
Delete(sLine, 1, iPosComme - 1);
if sCUser = sUser then begin
ShowMessage('Username taken');
end
else
begin
rewrite(tNames);
Writeln(tNames, sCUser + ',' + '0');
CloseFile(tNames);
end;
end;
end;
Remove the call to Rewrite()before Eof(). Even if you were not getting an IO error, your code would still fail because Rewrite() closes the file you opened with Reset() and then it creates a new bank file, so Eof() would always be True.
Update: error 104 is file not open for input, which means Reset() is not opening the file but is not raising an exception (which sounds like an RTL bug if Eof() is raising an exception, indicating that {I+} is active).
In any case, using AssignFile() and related routines is the old way to do file I/O. You should use newer techniques, like FileOpen() with FileRead(), TFileStream with TStreamReader, TStringList, etc...
Update: your loop logic is wrong. You are comparing only the first line. If it does not match the user, you are wiping out the file, writing the user to a new file, closing the file, and then continuing the loop. EoF() will then fail at that point. You need to rewrite your loop to the following:
procedure TForm1.btnRegisterClick(Sender: TObject
var
SCUser, sUser: String;
tNames: TextFile;
iPosComme: Integer;
Found: Boolean;
begin
sCUser := edtUserName.Text;
AssignFile(tNames,'Names.txt');
try
Reset(tNames);
except
ShowMessage('File not found');
Exit;
end;
try
Found := False;
while not EoF(tNames) do
begin
Readln(tNames,sLine);
iPosComme := Pos(',', sLine);
sUser := Copy(sLine ,1,iPosComme -1);
if sCUser = sUser then
begin
ShowMessage('Username taken') ;
Found := True;
Break;
end;
end;
if not Found then
Writeln(tNames,sCUser + ',0');
finally
CloseFile(tNames);
end;
end;
For the sake of completeness, this Version works for me, but it is hard to guess what the code is intended to do. Especially the while loop seems a bit displaced, since the file will contain exactly one line after the rewrite-case has ben hit once.
program wtf;
{$APPTYPE CONSOLE}
{$I+}
uses
SysUtils;
procedure Sample( sCUser : string);
var sUser, sLine : string;
iPosComme : Integer;
tnames : textfile;
begin
AssignFile(tNames,'Names.txt');
try
Reset(tNames);
except
Writeln('File not found');
Exit;
end;
while not EoF(tNames) do
begin
Readln(tNames,sLine);
iPosComme := Pos(',', sLine);
sUser := Copy(sLine ,1,iPosComme -1);
Delete( sLine,1, iPosComme -1);
if sCuser = sUser then begin
Writeln('Username taken') ;
end
else begin
Rewrite(tNames);
Writeln(tNames,sCUser + ',' + '0');
CloseFile(tNames);
Break; // file has been overwritten and closed
end;
end;
end;
begin
try
Sample('foobar');
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
end.
I wrote a version of this method that uses the newer TStreamReader and TStreamWriter classes.
This won't work with Delphi 7 of course, it's just to show how this could be done in newer versions of Delphi.
The code was heavily inspired by Remys answer.
procedure TForm1.btnRegisterClick(Sender: TObject);
var
Stream: TStream;
Reader: TStreamReader;
Writer: TStreamWriter;
Columns: TStringList;
UserName: string;
Found: Boolean;
FileName: string;
Encoding: TEncoding;
begin
FileName := ExpandFileName('Names.txt'); // An absolute path would be even better
UserName := edtUsername.Text;
Found := False;
Encoding := TEncoding.Default; // or another encoding, e.g. TEncoding.Unicode for Unicode
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Reader := TStreamReader.Create(Stream, Encoding);
try
Columns := TStringList.Create;
try
Columns.Delimiter := ',';
Columns.StrictDelimiter := True; // or False, depending on the file format
while not Reader.EndOfStream do
begin
Columns.DelimitedText := Reader.ReadLine;
if Columns.Count > 0 then
begin
if AnsiSameStr(Columns[0], UserName) then // or AnsiSameText if UserName is not case-sensitive
begin
ShowMessage('Username taken') ;
Found := True;
Break;
end;
end;
end;
finally
Columns.Free;
end;
finally
Reader.Free;
end;
finally
Stream.Free;
end;
if not Found then
begin
Writer := TStreamWriter.Create(FileName, True, Encoding);
try
// Warning: This will cause problems when the file does not end with a new line
Writer.WriteLine(UserName + ',0');
finally
Writer.Free;
end;
end;
end;
If performance and memory usage are not a concern:
procedure TForm1.btnRegisterClick(Sender: TObject);
var
Rows: TStringList;
Columns: TStringList;
UserName: string;
Found: Boolean;
FileName: string;
Encoding: TEncoding;
Row: string;
begin
FileName := ExpandFileName('Names.txt'); // An absolute path would be even better
UserName := edtUsername.Text;
Found := False;
Encoding := TEncoding.Default; // or another encoding, e.g. TEncoding.Unicode for Unicode
Rows := TStringList.Create;
try
Rows.LoadFromFile(FileName, Encoding);
Columns := TStringList.Create;
try
Columns.Delimiter := ',';
Columns.StrictDelimiter := True; // or False, depending on the file format
for Row in Rows do
begin
Columns.DelimitedText := Row;
if Columns.Count > 0 then
begin
if AnsiSameStr(Columns[0], UserName) then // or AnsiSameText if UserName is not case-sensitive
begin
ShowMessage('Username taken') ;
Found := True;
Break;
end;
end;
end;
finally
Columns.Free;
end;
if not Found then
begin
Rows.Add(UserName + ',0');
Rows.SaveToFile(FileName, Encoding);
end;
finally
Rows.Free;
end;
end;
This solution can be adapted to Delphi 7 by removing the Encoding variable.
If it's part of a bigger database it should be stored in a real database management system rather than a text file.
I have a function like this, that I would like to refactor
function Myfunction(sUrl, sFile: String) : Boolean;
var
GetData : TFileStream;
begin
Result := False;
//if the line below fails, I get an unhandled exception
GetData := TFileStream.Create(sFile, fmOpenWrite or fmCreate);
try
try
IdHTTP.Get(sUrl, GetData);
Result := (IdHTTP.ResponseCode = 200);
except
on E: Exception do begin
MessageBox(0, PChar(E.message), 'Niðurhala skrá', MB_ICONERROR or MB_OK);
end;
end;
finally
GetData.Free;
end;
end;
Procedure SomeOtherCode;
Begin
//How can I best defend against the unhandled exception above
//unless the call to the function is packed in a try .. except block
//the code jumps skips the if statement an goes to next
//exception block on the stack
if MyFunction('http://domain.com/file.html', 'c:\folder\file.html') then
ShowMessage('Got the file')
else
ShowMessage('Error !');
End
end;
Question:
Please refer to the comment within the procedure SomeOtherCode above.
Best Regards
Just wrap the code where you want to trap exceptions in a try..except block:
function MyFunction(...): Boolean;
var
Stream: TFileStream;
begin
Result := False;
try
Stream := TFileStream.Create(...);
try
// more code
Result := ...
finally
Stream.Free;
end;
except
// handle exception
end
end;
The whole point about exception handling is two-fold:
finally is for resource cleanup; you see this often in business logic
except is for reacting on specific exception (and getting rid of state logic through function results and intermediate variables); you hardly see it in business logic
In your case:
Myfunction should not return a Boolean, not contain an except block, and not perform a MessageBox but just let the exceptions propagate.
SomeOtherCode should contain the except block and tell the user what went wrong.
Example:
procedure Myfunction(sUrl, sFile: String);
var
GetData: TFileStream;
begin
Result := False;
//if the line below fails, I get an unhandled exception
GetData := TFileStream.Create(sFile, fmOpenWrite or fmCreate);
try
IdHTTP.Get(sUrl, GetData);
if (IdHTTP.ResponseCode <> 200) <> then
raise Exception.CreateFmt('Download of %s failed, return code %d', [sURl, IdHTTP.ResponseCode]);
finally
GetData.Free;
end;
end;
procedure SomeOtherCode:
begin
try
MyFunction('http://domain.com/file.html', 'c:\folder\file.html');
except
on E: Exception do begin
MessageBox(0, PChar(E.message), 'Niðurhala skrá', MB_ICONERROR or MB_OK);
end;
end;
end;
Now the code is much cleaner:
no more UI in your business logic
one place where your except is being handled
all failures are handled equally (cannot create file, download failure)
Good luck with this.
--jeroen
If you want your function to show messages to the user and return false on any failure, code it as follows:
function Myfunction(sUrl, sFile: String) : Boolean;
var
GetData : TFileStream;
begin
Result := False;
try
//if the line below fails, I get an unhandled exception
GetData := TFileStream.Create(sFile, fmOpenWrite or fmCreate);
try
try
IdHTTP.Get(sUrl, GetData);
Result := (IdHTTP.ResponseCode = 200);
except
on E: Exception do begin
MessageBox(0, PChar(E.message), 'Niðurhala skrá', MB_ICONERROR or MB_OK);
end;
end;
finally
GetData.Free;
end;
except
// you can handle specific exceptions (like file creation errors) or any exception here
end;
end;
Warning
IMHO this design is mixing business logic (such as get a resource/file from the Internet and save it to a file) and user interface logic (such as showing messages to the user in case of errors).
In general, is a better approach to separate business from UI logic, because your code is reusable.
For example you might want to re-factor as this:
function DownloadToAFile(const sUrl, sFile: string): boolean;
var
GetData : TFileStream;
begin
GetData := TFileStream.Create(sFile, fmOpenWrite or fmCreate);
try
IdHTTP.Get(sUrl, GetData);
Result := (IdHTTP.ResponseCode = 200);
finally
GetData.Free;
end;
end;
function UIDownloadToAFile(const sUrl, sFile: string): boolean;
begin
try
Result := DownloadToAFile(sURL, sFile);
except
on E: EIDException do //IndyError
MessageBox(0, PChar(E.message), 'Internet Error', MB_ICONERROR or MB_OK);
on E: EFileCreateError do //just can't remember the extact class name for this error
MessageBox(0, PChar(E.message), 'File create Error', MB_ICONERROR or MB_OK);
end;
end;
procedure SomeOtherCode:
begin
if UIDownloadToAFile('http://domain.com/file.html', 'c:\folder\file.html') then
ShowMessage('Got the file')
else
ShowMessage('Error !');
end;
Tomorrow, if you're writing a service, or a DataSnap module, you're free to use the DownloadToAFile or maybe to write a new ServiceDownloadToAFile wich in turns writes errors to a log or windows events, or maybe send a email notifying the HostAdmin about it.
One solution which is quite popular is to avoid 'success' or 'failure' return values completely. Instead of a function, use a procedure and handle failures using exceptions instead:
procedure Download(sUrl, sFile: String);
and then
try
Download ('http://domain.com/file.html', 'c:\folder\file.html');
ShowMessage('Got the file')
except
on E:Exxx do
begin
// handle exception
ShowMessage('Error !');
end
end;
This has also the effect that nobody can invoke the function and silently ignore the return value.
For some reason most people misuse except-finally combination. Correct sequence is
try
// allocate resource here
try
finally
// free resource here
end;
except
// handle exception here
end;
This lets you catch exceptions in constructor and destructor.
You should use only one try and get in this your all function code.