2

I’m trying to make a temporary directory that is removed when the script exits.

#!/usr/bin/env ruby

require 'pathname'
require 'tmpdir'

Tmp_dir = Pathname.new(Dir.mktmpdir)

Signal.trap('EXIT') {
  Tmp_dir.rmtree
  puts 'Doing cleanup'
}

puts 'Will exit after this message'

The Doing cleanup message never fires and the directory remains intact. After some tests, it seems that Tmp_dir.rmtree never runs, nor does anything after it.

The documentation suggests Pathname.rmtree calls FileUtils.rm_r, so I tried it like that:

#!/usr/bin/env ruby

require 'fileutils'
require 'pathname'
require 'tmpdir'

Tmp_dir = Pathname.new(Dir.mktmpdir)

Signal.trap('EXIT') {
  FileUtils.rm_r(Tmp_dir.to_path)
  puts 'Doing cleanup'
}

puts 'Will exit after this message'

And it works. So why doesn’t the rmtree version?

user137369
  • 5,219
  • 5
  • 31
  • 54

1 Answers1

1

Signal Traps Aren't Identical to Kernel's At_Exit Handler

You said in comments that you don't want to use Kernel#at_exit, but Signal#trap is not really an exact synonym. Pragmatically, the behavior of the two methods is demonstrably different. You can see this easily as follows:

# prints nil because variable not in scope; can't raise exceptions
Signal.trap(0) { p defined?(dir); p dir; raise dir }; dir="foo"; exit

# instance variables accessible; won't raise when undefined
Signal.trap(0) { p defined?(@dir); p @dir }; exit

# raises NameError because variable not in scope
at_exit { p defined?(dir); p dir }; dir="foo"; exit

# instance variable in scope, but won't raise when undefined
at_exit { p defined?(@dir); p @dir }; exit

# instance variable in scope; can raise manually
at_exit { p defined?(@dir); p @dir; raise @dir }; exit

If you want to know why they're different, you'll likely have to examine the parser or the underlying implementation of your current Ruby engine. It may be a bug, or a deliberate choice by the Ruby Core Team. Either way, it's clear that they have distinct behavior in Ruby 2.7.1.

Use Instance Variables in At_Exit Handler

As demonstrated, using local variables with a trapped signal presents a scoping problem, and other implementations of your desired code may not be reliable or consistent for your use case. You should register a handler through Kernel#at_exit, and store your temporary directory name in an instance or class variable instead.

Registering a handler through Kernel#at_exit will do what you expect from irb or the command line when @dir is defined as an instance variable in the top-level scope or the handler's registered binding. Pathname is not strictly necessary, but is included to match your current code.

require 'pathname'
require 'tmpdir'

at_exit { @dir.rmtree }

@dir = Pathname.new Dir.mktmpdir
p @dir

Use a Block for Automatic Cleanup

You can avoid scope issues in your signal/exit handlers altogether by using the block form of the Dir#mktmpdir command instead. For example:

require 'tmpdir'

Dir.mktmpdir do |dir|
  # do something with dir
end

This will clean up the temporary directory when the block exits, rather than having to register an exit handler or signal trap. In my experience, this is generally much easier to test and debug than deferred closures, but your mileage may certainly vary.

Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199
  • If it were a scoping problem, the `FileUtils` solution wouldn’t have worked, but it does. If I use `at_exit` instead of `Signal.trap` (and change nothing else in my code) it also works, but that doesn’t explain why `Signal.trap` doesn’t, and why every command after the `rmtree` doesn’t run. The block form isn’t ideal for my use-case. Also, I want to understand why the code fails, not just get a workaround. – user137369 Aug 22 '20 at 21:20