Skip to content

Commit 2d599f4

Browse files
authored
Merge pull request #8999 from ProcessMaker/FOUR-32760
FOUR-32760 After install/enable/disable a plugin, should be executed the command: php artisan horizon:terminate
2 parents 0ac5c82 + 5cf0368 commit 2d599f4

2 files changed

Lines changed: 77 additions & 29 deletions

File tree

‎ProcessMaker/Managers/PluginManager.php‎

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public function install(string $repoUrl, ?string $branch = null, ?string $tag =
9292
$this->logRunning('Running plugin install command...', $repoName, $userId);
9393
$this->runCommand($installCommand, $repoName, $userId);
9494

95-
$this->rebuildRouteCache($repoName, $userId);
95+
$this->refreshRuntimeAfterPluginChange($repoName, $userId);
9696

9797
$this->logDone('Plugin installed successfully', $repoName, $userId);
9898
}
@@ -127,7 +127,7 @@ public function uninstall(string $pluginName, ?int $userId = null): void
127127
$this->logRunning('Removing plugin directory...', $pluginName, $userId);
128128
$this->deleteDirectory($pluginPath, $pluginName, $userId);
129129

130-
$this->rebuildRouteCache($pluginName, $userId);
130+
$this->refreshRuntimeAfterPluginChange($pluginName, $userId);
131131

132132
$this->logDone('Plugin uninstalled successfully', $pluginName, $userId);
133133
}
@@ -145,18 +145,24 @@ private function runCommand(string $command, string $pluginName, ?int $userId =
145145

146146
if (!$result->successful()) {
147147
if (str_contains($result->output(), 'is not defined')) {
148-
\Log::info("Plugin does not have a {$command} command", ['output' => $result->output()]);
149148
$this->logRunning("Plugin does not have a {$command} command", $pluginName, $userId);
150149
} else {
151-
\Log::info("Plugin {$command} command failed. Got output:\n\n{$result->output()}", ['output' => $result->output()]);
152150
$this->logError("Plugin {$command} command failed. Got output:\n\n{$result->output()}", $pluginName, $userId);
153151
}
154152
} else {
155-
\Log::info("Plugin {$command} command output:\n\n{$result->output()}", ['output' => $result->output()]);
156153
$this->logRunning("Plugin {$command} command output:\n\n{$result->output()}", $pluginName, $userId);
157154
}
158155
}
159156

157+
/**
158+
* Refresh routes and queue workers after plugin code changes.
159+
*/
160+
private function refreshRuntimeAfterPluginChange(string $pluginName, ?int $userId = null): void
161+
{
162+
$this->rebuildRouteCache($pluginName, $userId);
163+
$this->terminateHorizon($pluginName, $userId);
164+
}
165+
160166
/**
161167
* Rebuild the tenant's route cache in a fresh artisan process.
162168
*
@@ -172,6 +178,15 @@ private function rebuildRouteCache(string $pluginName, ?int $userId = null): voi
172178
$this->reloadCachedRoutes();
173179
}
174180

181+
/**
182+
* Always stop current Horizon workers so a new process picks up plugin code.
183+
*/
184+
private function terminateHorizon(string $pluginName, ?int $userId = null): void
185+
{
186+
$this->logRunning('Restarting queue workers...', $pluginName, $userId);
187+
$this->runCommand('horizon:terminate', $pluginName, $userId);
188+
}
189+
175190
/**
176191
* Environment variables for artisan subprocesses started from a web request.
177192
*/
@@ -273,7 +288,7 @@ public function installFromZip(string $zipPath, ?int $userId = null): void
273288
$this->logRunning('Running plugin install command...', $repoName, $userId);
274289
$this->runCommand($installCommand, $repoName, $userId);
275290

276-
$this->rebuildRouteCache($repoName, $userId);
291+
$this->refreshRuntimeAfterPluginChange($repoName, $userId);
277292

278293
$this->logDone('Plugin installed successfully', $repoName, $userId);
279294
} finally {
@@ -311,7 +326,7 @@ public function toggle(string $pluginName, ?int $userId = null): bool
311326
throw new RuntimeException("Failed to disable plugin: {$pluginName}");
312327
}
313328

314-
$this->rebuildRouteCache($pluginName, $userId);
329+
$this->refreshRuntimeAfterPluginChange($pluginName, $userId);
315330

316331
return false;
317332
}
@@ -325,7 +340,7 @@ public function toggle(string $pluginName, ?int $userId = null): bool
325340
throw new RuntimeException("Failed to enable plugin: {$pluginName}");
326341
}
327342

328-
$this->rebuildRouteCache($enabledName, $userId);
343+
$this->refreshRuntimeAfterPluginChange($enabledName, $userId);
329344

330345
return true;
331346
}
@@ -775,16 +790,14 @@ protected function logDone(string $message, string $pluginName, ?int $userId = n
775790
*/
776791
protected function log(string $message, string $type, string $pluginName, ?int $userId = null): void
777792
{
793+
if ($type === 'error') {
794+
\Log::error($message);
795+
} else {
796+
\Log::info($message);
797+
}
798+
778799
if ($userId) {
779800
event(new PluginLog($message, $type, $pluginName, $userId));
780-
} else {
781-
if ($type === 'done') {
782-
\Log::info($message);
783-
} elseif ($type === 'error') {
784-
\Log::error($message);
785-
} else {
786-
\Log::info($message);
787-
}
788801
}
789802
}
790803
}

‎tests/Managers/PluginManagerTest.php‎

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
namespace Tests\Managers;
44

5-
use Illuminate\Support\Facades\Artisan;
65
use Illuminate\Support\Facades\Event;
76
use Illuminate\Support\Facades\Process;
87
use ProcessMaker\Events\PluginLog;
98
use ProcessMaker\Managers\PluginManager;
10-
use ProcessMaker\Models\Plugin;
119
use RuntimeException;
1210
use Tests\TestCase;
1311

@@ -62,10 +60,6 @@ public function testInstallClonesRepositoryAndValidatesPlugin()
6260
'*' => Process::result('', exitCode: 0),
6361
]);
6462

65-
// Mock Artisan::all() to return empty array (no plugin install command)
66-
Artisan::shouldReceive('all')
67-
->andReturn([]);
68-
6963
Event::fake([PluginLog::class]);
7064

7165
$manager = new PluginManager();
@@ -74,6 +68,7 @@ public function testInstallClonesRepositoryAndValidatesPlugin()
7468
Event::assertDispatched(PluginLog::class, function ($event) {
7569
return str_contains($event->message, 'installed successfully');
7670
});
71+
$this->assertProcessRanCommand('horizon:terminate');
7772
}
7873

7974
public function testInstallValidatesComposerJsonNamespace()
@@ -127,10 +122,6 @@ public function testUninstallRemovesPluginDirectory()
127122
mkdir($this->tempPluginDir, 0755, true);
128123
file_put_contents($this->tempPluginDir . '/test.txt', 'test');
129124

130-
// Mock Artisan::all() to return empty array (no plugin uninstall command)
131-
Artisan::shouldReceive('all')
132-
->andReturn([]);
133-
134125
Event::fake([PluginLog::class]);
135126

136127
Process::fake([
@@ -144,6 +135,7 @@ public function testUninstallRemovesPluginDirectory()
144135
Event::assertDispatched(PluginLog::class, function ($event) {
145136
return str_contains($event->message, 'uninstalled successfully');
146137
});
138+
$this->assertProcessRanCommand('horizon:terminate');
147139
}
148140

149141
public function testUninstallThrowsExceptionForNonExistentPlugin()
@@ -266,7 +258,7 @@ public function testArtisanEnvironmentIncludesCurrentTenant()
266258
$this->assertSame(['TENANT' => '7'], $environment);
267259
}
268260

269-
public function testToggleRebuildsRouteCache()
261+
public function testToggleRebuildsRouteCacheAndTerminatesHorizon()
270262
{
271263
$this->setUpTestPluginManager();
272264

@@ -286,12 +278,55 @@ public function testToggleRebuildsRouteCache()
286278
$this->assertFalse(is_dir($pluginPath));
287279
$this->assertTrue(is_dir($this->pluginsDir . '/_' . $pluginName));
288280

289-
Process::assertRan(function ($process) {
281+
$this->assertProcessRanCommand('route:cache');
282+
$this->assertProcessRanCommand('horizon:terminate');
283+
}
284+
285+
public function testToggleEnableTerminatesHorizon()
286+
{
287+
$this->setUpTestPluginManager();
288+
289+
$pluginName = 'toggle-plugin';
290+
$disabledPath = $this->pluginsDir . '/_' . $pluginName;
291+
$this->tempPluginDir = $this->pluginsDir . '/' . $pluginName;
292+
mkdir($disabledPath, 0755, true);
293+
294+
Process::fake([
295+
'*' => Process::result('', exitCode: 0),
296+
]);
297+
298+
$manager = new PluginManager();
299+
$enabled = $manager->toggle($pluginName);
300+
301+
$this->assertTrue($enabled);
302+
$this->assertTrue(is_dir($this->pluginsDir . '/' . $pluginName));
303+
$this->assertProcessRanCommand('horizon:terminate');
304+
}
305+
306+
public function testRunCommandLogsSuccessfulOutputOnce()
307+
{
308+
Process::fake([
309+
'*' => Process::result("Routes cached successfully.\n", exitCode: 0),
310+
]);
311+
Event::fake([PluginLog::class]);
312+
313+
$method = new \ReflectionMethod(PluginManager::class, 'runCommand');
314+
$method->invoke(new PluginManager(), 'route:cache', 'test-plugin', 1);
315+
316+
Event::assertDispatchedTimes(PluginLog::class, 1);
317+
Event::assertDispatched(PluginLog::class, function (PluginLog $event) {
318+
return str_contains($event->message, 'Plugin route:cache command output');
319+
});
320+
}
321+
322+
private function assertProcessRanCommand(string $artisanCommand): void
323+
{
324+
Process::assertRan(function ($process) use ($artisanCommand) {
290325
$command = is_array($process->command)
291326
? implode(' ', $process->command)
292327
: (string) $process->command;
293328

294-
return str_contains($command, 'route:cache');
329+
return str_contains($command, $artisanCommand);
295330
});
296331
}
297332

0 commit comments

Comments
 (0)