Clean up after yourself

September 4, 2024

Just like in your real life, you gotta clean up after yourself in code too!

If we start a process, we should kill it when we're done. If we create a temporary file, we should delete it when we're done. Flip a switch on? Flip it back off.

(Prefer to watch instead of read? Check out the video version of this article on YouTube: Poppin Off: Clean Up After Yourself.)

The problem with not cleaning up

Let me give you an example, shown here in PHP but the point applies regardless of language. In this example we're going to use Ngrok to share our local app with the world. (That detail isn't important, but it is interesting!)

We have a main handle function where we start a process, touch a file, write out some text, and then run an infinite loop until we interrupt the command at which point it will exit.

public function handle(): void
{
$this->startProcess();
$this->touchFile();
 
$this->line('Waiting...');
$this->line('(Press ctrl+c to stop.)');
 
while ($this->shouldRun) {
echo ".";
Sleep::for(1)->seconds();
}
 
// @TODO Cleanup
 
$this->line('Exiting');
}
Code highlighting powered by torchlight.dev (A service I created!)

When we run the app in the terminal, we'll start a child process, touch a file, and then enter into an infinite loop. When we hit ctrl+c, the process stops and we exit.

This is what we'll see:

Starting ngrok process...
Touching a file...
Waiting...
(Press ctrl+c to stop.)
......^C
Stopping
Exiting

But here's the catch: this command leaves a mess behind. The file you touched? It's still there. We don't delete it. Here's the naive touchFile implementation.

public function touchFile()
{
$this->line('Touching a file...');
touch(base_path('sharing'));
}

The process we started? It's probably terminated when the parent process ends, but there's no guarantee. Here's the naive startProcess implementation.

public function startProcess()
{
$this->line('Starting ngrok...');
Process::start('ngrok');
}

We need to clean up after ourselves to ensure everything is tidy when we're done.

So how do we do that? There are two approaches: returning a callback or registering an undo stack. Let's explore both.

Approach 1: returning a callback

The idea here is to return a callback from each method that undoes the work done by that method.

In the startProcess function, we assign Process::start(‘ngrok') to a variable.

public function startProcess()
{
$this->line('Starting ngrok...');
$process = Process::start('ngrok');
}

In the "undo" function, we can use the $process variable to see if the process is still running and send a SIGINT to interrupt it if so. Finally, we'll print out text that we're stopping Ngrok, as a little treat.

public function startProcess()
{
$this->line('Starting ngrok...');
$process = Process::start('ngrok');
 
return function () use ($process) {
if ($process->running()) {
$process->signal(SIGINT);
}
$this->line('Stopping Ngrok');
};
}
Code highlighting powered by torchlight.dev (A service I created!)

So now, in our main handle function, our startProcess function will return a callback that will stop the process if it's running. We can save this callback to a variable and then call it at the end of the handle function.

public function handle(): void
{
$stopProcess = $this->startProcess();
$this->touchFile();
 
$this->line('Waiting...');
$this->line('(Press ctrl+c to stop.)');
 
while ($this->shouldRun) {
echo ".";
Sleep::for(1)->seconds();
}
 
// Cleanup
$stopProcess();
 
$this->line('Exiting');
 
}

So clean, right? We love it.

We'll update the touchFile function similarly now to return a function that unlinks the file. If the file is gone, we don't actually care, but we do want to make sure that it is in fact gone.

So we update the touchFile function to add in the return function:

public function touchFile()
{
$this->line('Touching a file...');
touch(base_path('sharing'));
 
return function () {
@unlink(base_path('sharing'));
$this->line('Unlinking file.');
};
}

Now back in our handle function, we can assign the result of the touchFile function to a variable called destroyFile and then call it at the end of the handle function.

public function handle(): void
{
$stopProcess = $this->startProcess();
$destroyFile = $this->touchFile();
 
$this->line('Waiting...');
$this->line('(Press ctrl+c to stop.)');
 
while ($this->shouldRun) {
echo ".";
Sleep::for(1)->seconds();
}
 
// Cleanup
$stopProcess();
$destroyFile();
 
$this->line('Exiting');
 
}

Each function now returns its own undo function.

When we run the app again, we see the process start, the file is touched, the infinite loop runs until we hit ctrl+c to interrupt it and we see ngrok stops, the file is unlinked, and we exit.

Approach 2: register an undo stack

The second method involves using a callback stack. This approach is particularly useful if your process could have multiple cleanup tasks.

To start, we'll add a protected array $cleanupCallbacks to our class:

class Run extends Command implements SignalableCommandInterface
{
proctected $signature = 'app:run';
 
protected bool $shouldRun = true;
 
protected array $cleanupCallbacks = [];

This array will hold each of our cleanup callbacks.

Now in our startProcess function, instead of returning a function, we can push the previous returned function into our stack:

public function startProcess()
{
$this->line('Starting ngrok...');
$process = Process::start('ngrok');
 
return function () use ($process) {
$this->cleanupCallbacks[] = function () use ($process) {
if ($process->running()) {
$process->signal(SIGINT);
}
$this->line('Stopping Ngrok');
};
}

Similarly with touchFile, we change the return function to push the function into the callback stack:

public function touchFile()
{
$this->line('Touching a file...');
touch(base_path('sharing'));
 
return function () {
$this->cleanupCallbacks[] = function () {
@unlink(base_path('sharing'));
$this->line('Unlinking file.');
};
}

Then we just need to write a clean-up function. For the cleanup function, we're going to iterate through each callback function in the stack:

public function cleanup()
{
foreach ($this->cleanupCallbacks as $cb) {
$cb();
}
}

When it's time to clean up, simply call our new cleanup method and we should get the exact same output as Approach 1.

public function handle(): void
{
$this->startProcess();
$this->touchFile();
 
$this->line('Waiting...');
$this->line('(Press ctrl+c to stop.)');
 
while ($this->shouldRun) {
echo ".";
Sleep::for(1)->seconds();
}
 
// Cleanup
$this->cleanup();
 
$this->line('Exiting');
 
}

This approach keeps your code neat and organized. The actions and their corresponding reversals stay close together, ensuring that nothing is left behind.

Wrapping up

Both methods, returning a callback and using a callback stack, are effective ways to clean up after yourself in code. Regardless of which implementation you choose, it keeps the action and the reversal of the action very close together. So a single method is responsible for declaring both how to instantiate and how to tear down. Even if the tear down comes in a different place and much, much later, it keeps the logic all together.

The choice between them depends on your specific needs and taste! Either way, you'll be sure that no processes are left running and no files are left lingering.

It's important to take pride in writing code that's not just functional but also clean and considerate of future maintainers, because honestly, you're probably the future maintainer.

YouTube video

Check out the video version of this article on YouTube!

Me

Thanks for reading! My name is Aaron and I write, make videos , and generally try really hard .

If you ever have any questions or want to chat, I'm always on Twitter.

If you want to give me money (please do), you can buy my course on SQLite at HighPerformanceSQLite.com or my course on screencasting at Screencasting.com . On the off chance you're a sophomore at Texas A&M University, you can buy my accounting course at acct229.com .

You can find me on YouTube on my personal channel . If you love podcasts, I got you covered. You can listen to me on Mostly Technical .