crystal icon indicating copy to clipboard operation
crystal copied to clipboard

Temp exe cleanup error

Open retronoodle opened this issue 3 months ago • 13 comments

Bug Report

module Ll
  VERSION = "0.1.0"

  # get the directory contents of the current directory
  def self.get_directory_contents
    Dir.children(".")
  end

  
end

Response:

PS C:\Users\tsmon\crystal\ll> crystal run .\src\ll.cr
Error deleting file: 'C:\\Users\\tsmon\\AppData\\Local\\crystal\\cache\\crystal-run-ll.exe.tmp.exe': Access is denied. (File::AccessDeniedError)
  from C:\a\crystal\crystal\src\crystal\system\win32\file.cr:303 in 'delete:raise_on_missing'
  from C:\a\crystal\crystal\src\compiler\crystal\command.cr:292 in 'execute'
  from C:\a\crystal\crystal\src\compiler\crystal\command.cr:257 in 'run_command'
  from C:\a\crystal\crystal\src\compiler\crystal\command.cr:56 in 'run'
  from C:\a\crystal\crystal\src\compiler\crystal.cr:11 in '__crystal_main'
  from C:\a\crystal\crystal\src\crystal\main.cr:141 in 'main'
  from C:\a\crystal\crystal\src\crystal\system\win32\wmain.cr:36 in 'wmain'
  from D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288 in '__scrt_common_main_seh'
  from C:\WINDOWS\System32\KERNEL32.DLL +190679 in 'BaseThreadInitThunk'
  from C:\WINDOWS\SYSTEM32\ntdll.dll +246604 in 'RtlUserThreadStart'
Error: you've found a bug in the Crystal compiler. Please open an issue, including source code that will allow us to reproduce the bug: https://github.com/crystal-lang/crystal/issues
PS C:\Users\tsmon\crystal\ll> 

retronoodle avatar Sep 05 '25 20:09 retronoodle

I cannot reproduce this on my machine.

Does this problem reproduce consistently on your system?

straight-shoota avatar Sep 05 '25 21:09 straight-shoota

Yes it does, but it compiles and runs fine, so for now I am just skipping the crystal run ... command I'm just compiling and testing.

Editing for clarity:

crystal run - is where we get this error crystal build has no errors, and the resulting exe runs fine.

retronoodle avatar Sep 05 '25 23:09 retronoodle

~~Ahh I think that's the clue. Building and running the resulting binary would run it within the context of C:\Users\tsmon\crystal\ll, while maybe on windows crystal run is executing it in the context of the cache dir and not C:\Users\tsmon\crystal\ll like it does on linux?~~

NVM, after re-reading it, it seems the root issue is when we try to cleanup the temp binary and not the actual runtime program itself.

Blacksmoke16 avatar Sep 05 '25 23:09 Blacksmoke16

Yeah, it might even be a permissions issue. I think this is a great use for Crystal btw - for my job I switch between command line Linux, mac, and windows, and since I am most comfortable with Linux, ll is just what my hand types automatically when I want to see the dir listing. So I made an ll for windows (and to learn the language)

If it helps any, I do have other crystal projects on the same machine that do not exhibit this behavior.

retronoodle avatar Sep 06 '25 13:09 retronoodle

I do have other crystal projects on the same machine that do not exhibit this behavior.

That's really odd. There is something shady going on and I would expect this to affect any crystal run. The fact that this is only for a specific program, is very surprising. Maybe the program does something with the path in question? The code you posted is essentially a no-op, similar to an empty source file. Is that really enough to trigger the behaviour, or is anything missing from the example? What happens with an actually empty source file?

As Blacksmoke mentioned above the error happens when crystal run tries to clean up the temporary executable it built previously and then executed. That happens only after the child process finished, so it's to be expected that your program itself works just fine. The error is only afterwards.

As to why this fails, this is a total mystery. ERROR_ACCESS_DENIED could indicate that the file is read-only. Could you check the file properties of the failing path?

straight-shoota avatar Sep 06 '25 13:09 straight-shoota

The permissions of that file and the directory its in are all mine:

Owner  : ME\tsmon
Group  : ME\tsmon
Access : NT AUTHORITY\SYSTEM Allow  FullControl
         BUILTIN\Administrators Allow  FullControl
         me\tsmon Allow  FullControl

Sometimes windows hangs on to processes for a second after they're done, if that exe hadn't fully cleared when it tried to delete the file, this might cause the issue.

I was able to delete the temp file myself, just now.

retronoodle avatar Sep 06 '25 14:09 retronoodle

Another update, i reduced the file to this:

module Ll
  VERSION = "0.1.0"

  # get the directory contents of the current directory
  def self.get_directory_contents
    # Dir.children(".")
  end
end

Basically commenting out the Dir.children part - and no error.

I wonder if whenever this resolves to a system call if that makes windows unable to let go of it right away. Does Dir.children() eventually end up doing a system call?

retronoodle avatar Sep 06 '25 14:09 retronoodle

Ok another update. I think it was a timing issue basically. At that point the app was so fast (it didn't do anything), I wasn't even calling it with Ll.get_directory_contents yet .

Once I started digging in I thought I'd put a 2 second sleep in there to see if that gave windows enough time to let go of the process.

Here is what we have now:

module Ll
  VERSION = "0.1.0"

  # get the directory contents of the current directory
  def self.get_directory_contents
    Dir.children(".")
    # lets see if we get this far
    puts "waiting for 2 seconds"
    sleep(2.seconds)
    puts "done waiting"
  end
end

Ll.get_directory_contents

And it works:

PS C:\Users\tsmon\crystal\ll> crystal run .\src\ll.cr
waiting for 2 seconds
done waiting

Comment out that sleep(2.seconds) and it fails in the same way. So I think possibly whatever mechanism that Dir uses to get the directory contents in windows - is slower than the crystal script takes to run and windows cleanup can be really slow.

retronoodle avatar Sep 06 '25 14:09 retronoodle

Okay, actually running the method with Dir.children makes a little more sense now. Only just a little, though. It seems there's indeed some remnant of the process lingering around even after it completes, keeping the file open and thus preventing it from deletion. It escapes me what exactly might be happening here, and how this relates to the Dir.children. If you want to do some more digging you could try to resolve that method into smaller pieces and reduce further (i.e. look at the source of Dir.children and use that instead, then remove parts of it).

I still cannot reproduce on my machine.

I suppose we could work around this issue in crystal run by catching the exception and trying the delete again after a short sleep.

straight-shoota avatar Sep 06 '25 14:09 straight-shoota

It could be Windows' own anti-virus: https://stackoverflow.com/questions/1753209/deletefile-fails-on-recently-closed-file

HertzDevil avatar Sep 06 '25 16:09 HertzDevil

I reproduce on GitHub Actions when using the run command to build and run the minitest test suite. The windows-latest (x86_64) runner is fine, but the windows-11-arm runner fails with this exact error:

Error deleting file: 'C:\\Users\\runneradmin\\AppData\\Local\\crystal\\cache\\crystal-run-assertions_test.exe.tmp.exe': Access is denied. (File::AccessDeniedError)
  from D:\a\crystal\crystal\src\crystal\system\win32\file.cr:303 in 'delete:raise_on_missing'
  from D:\a\crystal\crystal\src\compiler\crystal\command.cr:292 in 'execute'
  from D:\a\crystal\crystal\src\compiler\crystal\command.cr:257 in 'run_command'
  from D:\a\crystal\crystal\src\compiler\crystal\command.cr:113 in 'run'
  from D:\a\crystal\crystal\src\compiler\crystal.cr:11 in '__crystal_main'

See https://github.com/ysbaddaden/minitest.cr/actions/runs/18980107223/job/54210224278

ysbaddaden avatar Oct 31 '25 17:10 ysbaddaden

Maybe we could rescue the exception, sleep, and retry a few times?

ysbaddaden avatar Oct 31 '25 17:10 ysbaddaden

Not sure if it's related but I encountered a bunch of similar errors in this CI job: https://github.com/crystal-lang/crystal/actions/runs/19103122801/job/54579813469

The errors are coming from the linker, not Crystal itself.

  2) hardware exception reports invalid memory access
     Failure/Error: fail "Compiler command `#{compiler} #{args.join(" ")}` failed with status #{status}.#{"\n" if output}#{output}"

       Compiler command `C:/a/_temp/msys64/clangarm64/bin/crystal.exe build -o C:\a\_temp\msys64\tmp\cr-spec-daed7dee\kernel\executable_file.exe C:\a\_temp\msys64\tmp\cr-spec-daed7dee\kernel\source_file` failed with status 1.
       ld.lld: error: failed to write output 'C:\a\_temp\msys64\tmp\cr-spec-daed7dee\kernel\executable_file.exe': Permission denied
       cc: error: unable to remove file: Permission denied
       cc: error: linker command failed with exit code 1 (use -v to see invocation)
       Error: execution of command failed with exit status 1: cc _main.o0.obj E-xception5858C-allS-tack.o0.obj 

Another expectation timed out which might suggest the system was under high load at this time:

  1) hardware exception detects stack overflow on the main stack
     Failure/Error: {% if flag?(:openbsd) %}

       spec timed out after 00:01:00

     # spec\std\kernel_spec.cr:254

The job succeeded without issues in a retry attempt.

straight-shoota avatar Nov 05 '25 16:11 straight-shoota