无法第二次打开已使用TFile标记为共享读取的文件

4

考虑以下代码:

  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)时出现错误:
enter image description here 使用:
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)调用,然后尝试通过另一个按钮单击打开文件,则错误仍然存在。这证明它与嵌套调用无关。

你的问题在于fmOpenOrCreate标志位。分3步操作:检查文件是否存在,如果不存在则执行fmCreate。关闭文件,先以readwrite模式打开,然后再以readonly模式打开... - whosrdaddy
@whosrdaddy 不,那不是。 - David Heffernan
@whosrdaddy,正如David所提到的,那样做是没有帮助的。当传递fmOpenOrCreate参数时,TFile.Open会检查文件是否存在。 - Jason
3个回答

13

当你调用

TFile.OpenRead(Path)

这是通过实现

TFileStream.Create(Path, fmOpenRead, 0)

这反过来导致调用

FileOpen(Path, fmOpenRead or 0)
最终调用 CreateFile 并将 0 作为 dwShareMode 传递。而CreateFile的文档说明,dwShareMode 的值为0时:

如果请求删除、读取或写入访问权限,则防止其他进程打开文件或设备。

换句话说,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返回之后但在TFileStream.Create内部调用CreateFile之前文件被创建会怎么样?但我猜代码被写成这样的原因是没有办法同时使用TFileStream.Create和将OPEN_ALWAYS传递给CreateFile。因此这是一个可怕的拼凑。

而且,如果选择了fmCreate选项,因为Exists()返回False,那么你的共享选项会被忽略。这是因为它们被传递到TFileStream.CreateRights参数中,而不是与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 Rights 传递给了 FileOpen。这看起来不太像忽略了 Rights

因此,这就解释了为什么只有文件已经存在时,你调用 TFile.Open 才能正确设置共享模式。

所以,不仅你不能使用 TFile.OpenRead,而且 TFile.Open 也行不通。在你还没到头时退出,并放弃对 TFile 的使用。我不知道 Embarcadero 在引入 TFile 时发生了什么,但显然出现了重大失误。结合 TFileStream.Create 的奇怪设计缺陷,就形成了一个真正的错误工厂。

我提交了一个 QC 报告:QC#115020。非常有趣的是,TFileStream.Create 中错误的行为(在应该不使用 Rights 的情况下使用了它)是新的 XE3 版本才出现的。我认为这是为了解决 TFile.Open 中错误的代码而做出的尝试,而该问题已经被报告为 QC#107005,但错误地标记为“已修复”。可悲的是,试图修正 TFile.Open 仍然使其失效,并且进而破坏了以前能够正常工作的 TFileStream.Create


谢谢。我没有想到要深入研究Windows API(最终)。谢谢。 - Jason
@DavidHeffernan 我不同意将 TFile.OpenRead 设置为唯一有罪的行为,因为 TFile.Open 已经会锁定文件了(除非在 XE3 上已经存在)。 - bummi
2
@bummi 你说得对。我会更新的。恐怕直到现在我才完全理解你一直在强调的重点。我认为你的回答应该多些文字,少些代码!;-) - David Heffernan
1
在Windows上,FileCreate忽略第三个参数。它仅用于在创建Unix文件时设置权限。 - David Heffernan
1
@bummi Emba论坛上有趣的帖子:http://https://forums.embarcadero.com/thread.jspa?messageID=553235#553235 - David Heffernan
显示剩余9条评论

1

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

如果文件不存在,则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));

很抱歉,我无法真正理解你的解释。抱歉。 - David Heffernan
最后我赶上了。+1。我试图在我的回答中进行扩展。 - David Heffernan
Rights参数很有趣。你不应该在该参数中传递共享模式。它与共享无关。它是在Kylix时期添加的,以便可以将其传递给libc open()。它指定了Unix权限。因此,当文档说在Windows上忽略Rights时,文档是正确的。它们应该被忽略。文档明确说明:Mode参数由打开模式和(可能)共享模式或组合在一起。因此,在Windows上,您应始终使用TFileStream.Create的两个参数版本。TFile类完全错误! - David Heffernan
@DavidHeffernan 我一直使用两个参数的版本,但发现 TFile 的实现方式很奇怪,而且因为在新版本上的行为部分(仅部分)改变了,所以这很有趣。 - bummi
1
我认为我发现了更多。TFileStream.Create中的更改,即Mode或Rights的引入,我相信这是Emba试图修复TFile.Open问题的尝试。可悲的是开发者把它弄得非常糟糕!现在我们有两个错误而不仅仅是一个! - David Heffernan
显示剩余3条评论

1
至少在 Delphi 2010 中存在类似的问题。基本问题是,如果文件被标记为“创建”,则某些共享标志根本就不会被考虑在内。 您可以在此处阅读更多相关信息。

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

这应该至少修复“旧样式”文件创建的问题,其中您将标志“or”在一起。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接