MacOS, Brew, Supervisor, Laravel & Horizon

This deals with the discovery, investigation and solution I found for a problem unique to the combination of technologies in the title.  It’s very specific, so if this applies to you, I hope this solution gets you moving again, as it was a very frustrating experience for me!

The Problem

I discovered a unique situation that didn’t appear to have an answer on Google this week. The background is as follows:

I run a local development environment using technologies installed with Brew on MacOS (version 10.14 currently). I was setting up a new Laravel installation, including Horizon and Telescope as packages. In order to get Horizon working properly, I needed to set up my worker queue as well as start the horizon service. Rather than tie these processes to a terminal window that I wouldn’t be able to close lest these processes be halted, I opted to use Supervisor to do the job for me, ensuring they stay running. Great idea, right? Should be easy to implement, right? I mean, Laravel documentation even has the supervisor configuration right there in the documentation.

It quickly went south when I realized MacOS has its own ideas on how things should interact.

The immediately problem that presented itself was, upon installing Supervisor and configuring the files for both queues and Horizon, Horizon start began to throw these errors:

Horizon started successfully.
   Symfony\Component\Debug\Exception\FatalThrowableError  : Call to undefined function Laravel\Horizon\Console\pcntl_async_signals()
  at /path/to/my/laravel/install/vendor/laravel/horizon/src/Console/HorizonCommand.php:55
    51|         );
    52| 
    53|         $this->info('Horizon started successfully.');
    54| 
  > 55|         pcntl_async_signals(true);
    56| 
    57|         pcntl_signal(SIGINT, function () use ($master) {
    58|             $this->line('Shutting down...');
    59| 

  Exception trace:

  1   Laravel\Horizon\Console\HorizonCommand::handle(Object(Laravel\Horizon\Repositories\RedisMasterSupervisorRepository))
      /path/to/my/laravel/install/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:32

  2   call_user_func_array()
      /path/to/my/laravel/install/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:32

  Please use the argument -v to see more details.

The Investigation

Commence head scratching.

A quick Google search DID produce results, but all the results related to trying to run this on Windows, which doesn’t support pcntl, so while they are “valid” results, they weren’t applicable to me.  I’m running PHP 7.3.5 and have verified independently that pcntl was enabled via php -m.  Some results said you needed to use PHP 7.1+, which clearly, I was.  So why was I getting this error?

Searching Google lead me in circles and I was getting nowhere, just about to hair ripping stage, when a thought occurred to me.  I wasn’t specifying the full PHP path in my supervisor config, so what if that would fix it?

A quick “whereis php” showed me that the system was sometimes defaulting to /usr/bin/php.  Sounds normal right?  WRONG.  That’s the default PHP installation that ships with MacOS.  I was using a Brew installation of PHP 7.3, so the PHP executable most definitely didn’t live there.  I had confirmed that my active version of Apache was indeed serving up PHP 7.3 long ago as well, so the command line version of PHP was getting mixed up.

I was on to something.

Sure enough, a few quick searches and I found my PHP 7.3 executable in the Brew cellar:  /usr/local/Cellar/php/7.3.5/bin/php

/usr/bin/php -v resulted in telling me that it was PHP 7.1.23 while /usr/local/Cellar/php/7.3.5/bin/php -v resulted in PHP 7.3.5.

AH HA

Supervisor (and a few other places I had been attempting tests via various terminal portals) was defaulting to the default MacOS shipped version of 7.1.23, which apparently doesn’t support pcntl_async_signals() (even though it should apparently).  However, not my problem at the moment.  I immediately changed the path in the supervisor config file for my Horizon command and….

VOILA!  Everything ran smoothly.

So what do you need to do to get this working smoothly on your MacOS machine that uses Brew?  The following is a list of steps I used that leads to happy days and smooth sailing.

The Solution

1. Install Laravel, Horizon, etc…  All the fun pieces you want to play with and configure all appropriately until you are ready to work with Supervisor.

2. Install Supervisor via Brew.

brew install supervisor

Don’t worry about starting supervisor yet.  That’ll come soon enough.

3. Set up your supervisor config files.  Now Brew’s version of supervisor uses .ini files, and doesn’t create its own config directory.  You need to do this manually.  Your main supervisor config file is located at: /usr/local/etc/supervisord.ini

If you look in that file, you’ll notice the included files reference a supervisor.d directory, which doesn’t exist.

Content in supervisord.ini:

[include]
files = /usr/local/etc/supervisor.d/*.ini

Add your own directory, then create a Horizon config file within.

mkdir /usr/local/etc/supervisor.d
vi /usr/local/etc/supervisor.d/horizon.ini

The content of your horizon.ini file should be similar to the following.   Be sure you configure your specialty paths:

  • Path to your php installation (your version may differ from mine).
  • Path to your local Laravel installation.
  • Your current local user.
  • Custom logging file paths. I chose to dump them in my laravel installation for the time being.
[program:horizon]
process_name=%(program_name)s
command=/usr/local/Cellar/php/7.3.5/bin/php /path/to/your/laravel/install/artisan horizon
directory=/path/to/your/laravel/install
autostart=true
autorestart=true
user=your_local_user
redirect_stderr=true
stdout_logfile=/path/to/your/laravel/install/horizon.log
stderr_logfile=/path/to/your/laravel/install/horizon.err.log

Since I use Supervisor for two processes, here’s a copy of the config file for my Laravel queue process.  Be sure to customize your paths as necessary!

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/your/laravel/install/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=your_local_user
numprocs=8
redirect_stderr=true
stdout_logfile=/path/to/your/laravel/install/queue.log
stderr_logfile=/path/to/your/laravel/install/queue.err.log

4. After saving your config file, it’s time to start Supervisor!

brew services start supervisor

And that’s it!  Your brew installation should function properly, picking up your default config, as well as the custom config files you created in the supervisor.d directory.

I hope this helps someone that has run into the problem I ran into.  It was very frustrated for parts of two days before I was able to find this solution and have everything running smoothly again.

Enjoy!