3

鉴于此代码:

  FN := 'c:\temp\test_file.log';
  AFile := TFile.Open(FN, TFileMode.fmOpenOrCreate, TFileAccess.faReadWrite, TFileShare.fsRead);
  try
    with TFile.OpenRead(FN) do
    try

    finally
      Free;
    end;

  finally
    AFile.Free;
  end;

尝试在 TFile.OpenRead(FN) 行打开时出现错误:
在此处输入图像描述

使用:

with TFile.Open('c:\temp\test_file.log', TFileMode.fmOpen, TFileAccess.faRead, TFileShare.fsRead) do
try

finally
  Free;
end;

也会导致同样的错误。就像这样:

  FS := TFileStream.Create('c:\temp\test_file.log', fmOpenRead or fmShareDenyWrite);
  try

  finally
    FS.Free;
  end;

但是,我可以愉快地在记事本中打开文件(如只读),或者如果我将初始 TFileShare.fsRead 更改为 TFileShare.fsNone,我无法按预期打开它(在记事本中)。

但是,如果我运行这个虚拟应用程序的两个实例,第一个使用 TFileShare.fsRead 打开我可以打开它。那么我是否无法在同一个应用程序中重新打开文件两次?似乎不对。

如果我最初使用以下命令打开文件:

  FS := TFileStream.Create('c:\temp\test_file.log', fmOpenReadWrite or fmShareDenyWrite);
  try

  finally
    FS.Free;
  end;

我可以使用上述方法(使用 fsRead)再次打开它。令人困惑的是单步执行 TFile.Open 代码,它最终执行的代码与上面的 TFileStream.Create 完全相同。

最后注。如果我使用顶部(第一种)方式打开但将其分配给“全局”变量,请删除内部 TFile.OpenRead(FN) 调用,然后尝试通过单击另一个按钮打开文件,错误仍然存​​在。这证明它与嵌套调用无关。

4

3 回答 3

10

你打电话时

TFile.OpenRead(Path)

这是由

TFileStream.Create(Path, fmOpenRead, 0)

这反过来又会导致调用

FileOpen(Path, fmOpenRead or 0)

最终将CreateFile传递0称为dwShareMode. 并且文档CreateFiledwShareMode0意思是:

阻止其他进程在请求删除、读取或写入访问权限时打开文件或设备。

换句话说,TFile.OpenRead(Path)就是试图以独占共享模式打开文件。由于文件已经打开,这显然会失败。

我认为这TFile.OpenRead(Path)是使用错误的共享模式。它应该允许读取访问。但是,即使是这种情况,它也无济于事,因为您的另一个句柄具有写访问权限。

通过避免来解决问题TFile.OpenRead。而是像这样打开它:

TFileStream.Create(Path, fmOpenRead or fmShareDenyNone)

你必须通过fmShareDenyNone。您无法拒绝任何形式的共享,因为您已经打开它进行阅读和写作。


当我最初写这个答案时,还有一个我没有掌握的问题。确实,TFile.OpenRead()总是试图获得独占访问权。但是,您对 的使用(您TFile.Open()拨打的第一个电话)也确实会导致独占访问。即使您指定了TFileShare.fsRead.

创建文件流的代码TFile.Open()如下所示:

if Exists(Path) then
  Result := TFileStream.Create(Path, LFileStrmAccess, LFileStrmShare)
else
  Result := TFileStream.Create(Path, fmCreate, LFileStrmShare);

马上,这就是一场灾难。文件创建行为打开文件存在检查是简单而简单的错误。文件创建需要是原子操作。如果文件是在返回之后创建的,但在内部Exists调用该文件之前呢?但我猜代码之所以写成这样,是因为没有办法使用,已经传到. 因此,这个可怕的拙劣。CreateFileTFileStream.CreateTFileStream.CreateOPEN_ALWAYSCreateFile

事实证明,如果选择了该fmCreate选项,因为Exists()返回False,那么您的共享选项将被忽略。那是因为它们被传递给Rights参数TFileStream.Create而不是与fmCreate. 正如文档所说,在 Windows 上,该Rights参数被忽略。

所以正确的代码应该是:

Result := TFileStream.Create(Path, fmCreate or LFileStrmShare);

if 的另一个分支呢?那肯定也是错的。由于传递给的值Rights被忽略,那么肯定会忽略 的值LFileStrmShare。好吧,事实证明文件撒谎了。中的代码TFileStream.Create如下:

constructor TFileStream.Create(const AFileName: string; Mode: Word; Rights: Cardinal);
var
  LShareMode: Word;
begin
  if (Mode and fmCreate = fmCreate) then
  begin
    LShareMode := Mode and $FF;
    if LShareMode = $FF then
      LShareMode := fmShareExclusive; // For compat in case $FFFF passed as Mode
    inherited Create(FileCreate(AFileName, LShareMode, Rights));
    if FHandle = INVALID_HANDLE_VALUE then
      raise EFCreateError.CreateResFmt(@SFCreateErrorEx, [ExpandFileName(AFileName), SysErrorMessage(GetLastError)]);
  end
  else
  begin
    inherited Create(FileOpen(AFileName, Mode or Rights));
    if FHandle = INVALID_HANDLE_VALUE then
      raise EFOpenError.CreateResFmt(@SFOpenErrorEx, [ExpandFileName(AFileName), SysErrorMessage(GetLastError)]);
  end;
  FFileName := AFileName;
end;

查看传递到的else分支。这看起来不太像被忽略。Mode or RightsFileOpenRights

TFile.Open因此,所有这一切都解释了为什么当且仅当文件已经存在时,才能在您的调用中正确设置共享模式。

所以,不仅不能用TFile.OpenRead,而且TFile.Open也out了。在你领先时退出并完全放弃TFile。我不知道 Embarcadero 的 QA 在TFile引入时发生了什么,但显然有一个重大失败。将这种失败与奇怪的设计缺陷结合起来TFileStream.Create,你就有了一个名副其实的错误工厂。

我提交了一份质量控制报告:QC#115020TFileStream.Create非常有趣的是,在不应该使用的地方使用的错误行为Rights对于 XE3 来说是新的。我相信这是一种尝试处理TFile.Open已被报告为QC#107005的虚假代码,该代码被错误地标记为已修复。TFile.Open可悲的是,修复叶子的尝试TFile.Open仍然坏了,反过来又打破了TFileStream.Create以前的工作!

于 2013-04-16T08:32:26.043 回答
1

从 TFileStream.Create 调用的 FileCreate 和 FileOpen 中的 Mode 中提取具有 windows 访问权限的权限。
CreateFile在这里用 ShareMode[(Mode and $F0) shr 4] 调用。

TFileMode.fmOpenOrCreate 将调用
TFileStream.Create(Path, fmCreate, LFileStrmShare); 是文件不存在。

行为可以展示

function OpenReadShareALL(const Path: string): TFileStream;
begin
  If FileExists(Path) then
    begin
      try
        Result := TFileStream.Create(Path, fmOpenRead or fmShareDenyNone);
        // will work too at lest up to XE since rights are ignored in oder versions
        //Result := TFileStream.Create(Path, fmOpenRead or fmShareDenyNone,fmShareExclusive);
      except
        on E: EFileStreamError do
          raise EInOutError.Create(E.Message);
      end;
    end;
end;



var
  FN:String;
  AFile:TFileStream;
begin
  FN := 'c:\temp\test_file.log';
 // this will lock file at least until Delphi XE if file has to be created
 //AFile := TFile.Open(FN, TFileMode.fmOpenOrCreate, TFileAccess.faReadWrite, TFileShare.fsReadWrite);


 // the following will work
 if Fileexists(FN) then
    AFile := TFileStream.Create(fn,fmOpenRead or fmOpenWrite or fmShareDenyNone)
 else
    AFile := TFileStream.Create(fn,fmCreate  or fmShareDenyNone);      

 // won't work if file does not exists , and will not work with existing file up to at least Delphi XE (fixed in XE 3 , maybe XE 2 too)
 //  AFile := TFileStream.Create(fn,fmCreate or fmOpenReadWrite ,fmShareDenyNone);
  try
    with OpenReadShareAll(FN) do
    try
      Showmessage('Worked');
    finally
      Free;
    end;

  finally
    AFile.Free;
  end;
end;

在 XE3 中,似乎更正了 fmCreate 以外的访问模式的权限忽略

inherited Create(FileOpen(AFileName, Mode or Rights));
于 2013-04-16T06:39:54.323 回答
1

至少在 Delphi 2010 之前存在类似的问题。基本问题是,如果文件被标记为“创建”,则根本不考虑某些共享标志。你可以阅读更多关于

http://cc.embarcadero.com/Item/21636

至少应该对“旧样式”文件创建进行修复,您可以在其中“或”将标志放在一起。

于 2013-04-16T06:58:12.023 回答