acorn icon indicating copy to clipboard operation
acorn copied to clipboard

Acorn scheduler is not loading wordpress correctly

Open tgeorgel opened this issue 1 year ago • 5 comments

Version

v4.2.2

What did you expect to happen?

I'm using Acorn in combinaison with Radicle.

I did register a Kernel instance :

\Roots\bootloader()->boot(function ($app) {
    $app->singleton(
        \Illuminate\Contracts\Console\Kernel::class,
        \App\Console\Kernel::class
    );
});

Which provides schedules :

namespace App\Console;

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Foundation\Application;
use Roots\Acorn\Console\Kernel as RootsKernel;
use Illuminate\Console\Scheduling\Schedule;

class Kernel extends RootsKernel
{
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('debug:dump')->everySecond();
    }

    public function __construct(Application $app, Dispatcher $events)
    {
        if (! defined('ARTISAN_BINARY')) {
            define('ARTISAN_BINARY', dirname(__DIR__, 2) . '/vendor/roots/acorn/bin/acorn');
        }

        $this->app = $app;
        $this->events = $events;

        // Make sure our schedules are getting registered
        $this->app->extend(Schedule::class, function ($app) {
            return tap(new Schedule($this->scheduleTimezone()), function ($schedule) {
                $this->schedule($schedule->useCache($this->scheduleCache()));
            });
        });

        $this->app->booted(function () {
            $this->defineConsoleSchedule();
        });
    }
}

Schedules are working nicely, but the load of Wordpress is not complete.
For example, If I would want to get the permalink of a page inside my command :

// App\Console\Commands\DebugDump

public function handle()
{
    ray(get_permalink(2));
}

When using WP-CLI, this works perfectly :

wp acorn debug:dump 

// http://site.test/my-page-name

But in the scheduler context, I get :

 wp acorn schedule:test

Running ['vendor/roots/acorn/bin/acorn' debug:dump] ................................................................................... 344ms DONE
  ⇂ '/opt/homebrew/Cellar/[email protected]/8.2.18/bin/php' 'vendor/roots/acorn/bin/acorn' debug:dump > '/dev/null' 2>&1
  
 // http://site.test/?page_id=2

What actually happens?

When running the scheduler with acorn, the acorn binary is called.

This binary loads wordpress and then expect acorn to boot on top :

require_once "{$rootPath}/{$composer['extra']['wordpress-install-dir']}/wp-blog-header.php";

Roots\Acorn\Bootloader::getInstance()->boot();

The seconds line nevers gets called.

What actually happens is that the wp-settings.php file is going to load the mu-plugins, which loads Acorn (00-acorn-boot.php), and the load of Acorn "captures" the "request", and so any code that should have run after the load of mu-plugins (inside wp-settings.php) won't run, including settings up default post_types and so on :

// file: wp-settings.php

// Load must-use plugins.
foreach ( wp_get_mu_plugins() as $mu_plugin ) {
    $_wp_plugin_file = $mu_plugin;
    include_once $mu_plugin; // <-- loading acorn will stop here.
    $mu_plugin = $_wp_plugin_file;
    do_action( 'mu_plugin_loaded', $mu_plugin );
}

/** Never reach this part */

/** ... */

create_initial_taxonomies();
create_initial_post_types();

/** ... */

This means that if I run a command with the scheduler :

 wp acorn schedule:test
  Running ['vendor/roots/acorn/bin/acorn' debug:dump] ................................................................................... 344ms DONE
  ⇂ '/opt/homebrew/Cellar/[email protected]/8.2.18/bin/php' 'vendor/roots/acorn/bin/acorn' debug:dump > '/dev/null' 2>&1

The debug:dump command won't be able to get a permalink (eg: get_permalink(1)) as the internal get_post_status_object() function could not return values because the core is not fully loaded.

Steps to reproduce

Create a Kernel class :

namespace App\Console;

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Foundation\Application;
use Roots\Acorn\Console\Kernel as RootsKernel;
use Illuminate\Console\Scheduling\Schedule;

class Kernel extends RootsKernel
{
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('debug:dump')->everySecond();
    }

    public function __construct(Application $app, Dispatcher $events)
    {
        if (! defined('ARTISAN_BINARY')) {
            define('ARTISAN_BINARY', dirname(__DIR__, 2) . '/vendor/roots/acorn/bin/acorn');
        }

        $this->app = $app;
        $this->events = $events;

        // Make sure our schedules are getting registered
        $this->app->extend(Schedule::class, function ($app) {
            return tap(new Schedule($this->scheduleTimezone()), function ($schedule) {
                $this->schedule($schedule->useCache($this->scheduleCache()));
            });
        });

        $this->app->booted(function () {
            $this->defineConsoleSchedule();
        });
    }
}

Make sure to register the kernel class when booting acorn :

\Roots\bootloader()->boot(function ($app) {
    $app->singleton(
        \Illuminate\Contracts\Console\Kernel::class,
        \App\Console\Kernel::class
    );
});

Then, after creating a command which tries to display a permalink, test it thru the scheduler using wp acorn schedule:test.

System info

MacBook Air M1 MacOS Sonoma 14.5 PHP 8.2.18

Log output

No response

Please confirm this isn't a support request.

Yes

tgeorgel avatar Jul 12 '24 08:07 tgeorgel

up

mfr75 avatar Oct 02 '24 10:10 mfr75

I also had a similar setup and noticed it would boot the Acorn Kernel AND my custom Kernel.

Maybe you can try to overwrite the implementation for \Roots\Acorn\Console\Kernel instead:

\Roots\bootloader()->boot(function ($app) {
    $app->singleton(
        \Roots\Acorn\Console\Kernel::class,
        \App\Console\Kernel::class
    );
});

// see comment below
// }, 0);

Also the Acorn boot priority was updated recently: https://github.com/roots/radicle/commit/5af169578560f6b1b7ec0879b4d4ce2d7842f25c

This is also something you could try.

Recently I got rid of overwriting the Kernel and moved my logic to a ServiceProvider instead. But I don't know if that's also possible for scheduling.

Details
<?php

declare(strict_types=1);

namespace App\Providers;

// [...]
use BabDev\ServerPushManager\Http\Middleware\ServerPush;
use Illuminate\Support\ServiceProvider;
use Roots\Acorn\Http\Kernel;

class WordPressOptimizationProvider extends ServiceProvider
{
    // [...]

    public function __construct($app)
    {
        parent::__construct($app);

        $app->extend(Kernel::class, function (Kernel $kernel) {
            // We need to add this first to make sure we're able to set headers
            return $kernel->prependMiddlewareToGroup('web', ServerPush::class);
        });
    }

    // [...]
}

RafaelKr avatar Oct 04 '24 12:10 RafaelKr

still need help on this one

mfr75 avatar Dec 03 '24 14:12 mfr75

@mfr75 The first bump wasn't necessary, nor was the second one. Post on Roots Discourse if you want to hire someone from the community if you're looking for immediate help.

retlehs avatar Dec 03 '24 14:12 retlehs

@mfr75 Try the following:

  1. Install WP-CLI via composer (wp-cli/wp-cli-bundle)
  2. Creating the file below in bin/artisan
  3. Add define('ARTISAN_BINARY', realpath(__DIR__ . '/../bin/artisan')); to wp-config.php (adjusting the path appropriately).

This completely bypasses vendor/bin/acorn and instead uses the WP-CLI integration which seems to work.

This probably works with a globally installed WP-CLI as well if you adjust the script but YMMV https://github.com/wp-cli/wp-cli/issues/4632.

#!/usr/bin/env php
<?php

$basePath = dirname(__DIR__);

$wpBin = realpath($basePath . '/vendor/bin/wp');

$args = array_slice($argv, 1);

pcntl_exec($wpBin, ['acorn',... $args]);

stefanfisk avatar May 26 '25 13:05 stefanfisk

I ran into the similar issue and figured out a workaround. Sharing it here in case it helps someone 😇 Instead of defining a custom ARTISAN_BINARY and using wp acorn schedule:run, I made a ServiceProvider that uses wp-cron to trigger Acorn’s scheduled tasks:

namespace App\Providers;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\ServiceProvider;

class WpCronScheduleProvider extends ServiceProvider {
    public const string SCHEDULE_RUN_ACTION = 'acorn_schedule_run';

    public function boot(Schedule $schedule): void {
        add_action(self::SCHEDULE_RUN_ACTION, function () use ($schedule) {
            require_once app_path('schedule.php');

            foreach ($schedule->events() as $event) {
                if ($event->isDue(app()) && $event->filtersPass(app())) {
                    $event->run(app());
                }
            }
        });

        add_filter('cron_schedules', static function (array $schedules): array {
            if (! isset($schedules['minutely'])) {
                $schedules['minutely'] = [
                    'interval' => 60,
                    'display' => __('Every Minute'),
                ];
            }

            return $schedules;
        });

        if (wp_doing_cron() && ! wp_next_scheduled(self::SCHEDULE_RUN_ACTION)) {
            wp_schedule_event(time(), 'minutely', self::SCHEDULE_RUN_ACTION);
        }
    }
}

And in schedule.php:

use App\Console\Commands\PublishMissedScheduledPosts;
use Illuminate\Support\Facades\{Artisan, Schedule};

Schedule::call(fn () => Artisan::call(PublishMissedScheduledPosts::class))
    ->name('Publish missed scheduled WordPress posts')
    ->everyTwoMinutes()
    ->withoutOverlapping();

I’m using Schedule::call instead of Schedule::command, so commands run in the same PHP process instead of spawning new CLI processes. This also works on shared hostings where proc_open is blocked.

rafaucau avatar Oct 09 '25 12:10 rafaucau