[BUG]: Compiler firing twice causing race issues with deletion #1318

Closed
opened 2026-09-08 21:02:56 +00:00 by Tom van der Laan · 2 comments
Member

What Happened?

I found out the compiler is triggered twice in most browser causing race issues during compilation resulting int folder and file creation and deletion errors.

Steps to reproduce the Bug

compile any component in any browser and wait for compilation errors. this has been a longstanding issue and the fix was always to use firefox. But as of lately firefox also began behaving badly.

Which Joomla version are you compiling in?

6.1.3

Which PHP version are you compiling in?

8.4

Which Joomla versions are you targeting?

6

Which PHP version are you targeting?

8.4

Which Web server is JCB running on?

litespeed

Which Relational Database is JCB running on?

mariadb 11.x

Which OS is JCB running on?

almalinux 10

Which JCB version are you using?

6.1.6 stable

Where in JCB did this issue occur?

Compilation of a component

On which browsers did you encounter the issue?

Firefox, Chrome, Safari

Additional Comments

PR: compile button submits the form twice, causing two concurrent compiles

Repo: https://git.vdm.dev/joomla/Component-Builder
Found on JCB 6.1.6, Joomla 6.1.3, PHP 8.3.

Symptom

Compiling a component intermittently ends on the Joomla error page with one of:

Joomla\Filesystem\Folder::create: Could not create directory.
  Path: [ROOT]/administrator/components/com_componentbuilder/compiler/<component>/…

Joomla\Filesystem\Folder::delete: Could not delete folder.
  Path: [ROOT]/administrator/components/com_componentbuilder/compiler/<component>/…

The path is different every time. The compile itself completes — refreshing the
page shows the success screen and the component can be installed or downloaded.
It usually takes a few compiles in a row before it happens.

Cause

administrator/components/com_componentbuilder/tmpl/compiler/default.php:235

<button class="btn btn-primary btn-lg px-4 me-sm-3" style="width: 100%;"
        onclick="Joomla.submitbutton('compiler.compiler')">

The button declares no type, so it defaults to type="submit", and it sits
inside the <form> opened on line 223. The inline handler does not cancel the
default action either (no return false). One click therefore fires both:

  1. onclick -> Joomla.submitbutton() which sets form.task.value, hides
    #form, and schedules setTimeout(() => form.submit(), 100)
  2. the browser's own native submit of the same form

Both requests carry task=compiler.compiler, because the onclick runs before the
default action. Two compilers then run against the same build directory.

Line 219 of the same file already gets this right with type="button".

Evidence

Logging one line per Compiler\Utilities\Folder::remove() call:

22:00:52.1296 pid=123016  remove(.../compiler/com_component_v1_1_29__J6)
22:00:52.1316 pid=124403  remove(.../compiler/com_component_v1_1_29__J6)

Two PIDs, 2 ms apart, resetting the same build directory. Too fast to be a
double-click.

Joomla's Folder::create() calls @mkdir(), so the real reason is discarded.
Capturing error_get_last() at the throw site gives it:

path      : .../com_component_v1_1_29__J6/libraries/tlweb_component/Component.Joomla/src/Data
mkdir said: mkdir(): File exists
ancestors : exists dir perms=0755  .../Component.Joomla/src/Data
listing   : .../src/Data/index.html
            .../src/Data/Factory.php

The directory already exists and already holds files written by the other
process. So: process A checks is_dir() -> false, process B creates the folder
and writes into it, process A calls mkdir() -> EEXIST.

The Could not delete folder variant is the same race in
Initializer::resetBuildDirectory(), where one process removes a subtree while
the other is iterating it.

Confirmed independently by running two compiles over one build directory in
isolation: both messages reproduce, at varying paths.

Verification

With the one-line fix below applied, 10 consecutive compiles of the same
component: no failures, and no second PID ever touching the build directory
within the same second.

22:06:44 .. 22:07:45   pid=253633   (6 compiles)
22:08:32 .. 22:09:03   pid=284468   (4 compiles)

Two PIDs appear, but sequentially - the pool recycled the worker during the ~47s
gap. Before the fix the two PIDs hit the same path 2 ms apart.

Fix

Paths below are from an installed JCB 6.1.6; adjust for the repo layout if it
differs.

# File Line Change
1 administrator/components/com_componentbuilder/tmpl/compiler/default.php 235 add type="button"
2 administrator/components/com_componentbuilder/src/Controller/CompilerController.php 110-127 guard compiler() against concurrent runs
3 libraries/vendor_jcb/VDM.Joomla/src/Componentbuilder/Compiler/Utilities/Folder.php 69-80 surface the OS error / tolerate the race

1. The trigger — one attribute (this alone fixes the reported bug)

administrator/components/com_componentbuilder/tmpl/compiler/default.php line 235:

--- a/administrator/components/com_componentbuilder/tmpl/compiler/default.php
+++ b/administrator/components/com_componentbuilder/tmpl/compiler/default.php
@@ -235,1 +235,1 @@
-					<button class="btn btn-primary btn-lg px-4 me-sm-3" style="width: 100%;" onclick="Joomla.submitbutton('compiler.compiler')"><span class="icon-cog icon-white"></span>
+					<button type="button" class="btn btn-primary btn-lg px-4 me-sm-3" style="width: 100%;" onclick="Joomla.submitbutton('compiler.compiler')"><span class="icon-cog icon-white"></span>

For reference, line 219 of the same file already uses type="button".

2. Suggested — make the compiler safe against concurrency

administrator/components/com_componentbuilder/src/Controller/CompilerController.php,
compiler() starts at line 110. It checks the token (line 113) and
core.manage (line 117), then calls $model->compile() at line 123 with no
guard of any kind:

	public function compiler(): bool
	{
		Session::checkToken() or \jexit(Text::_('JINVALID_TOKEN'));      // 113

		$user = $this->app->getIdentity();                              // 116
		if (!$user->authorise('core.manage', 'com_componentbuilder'))    // 117
		{
			return false;
		}

		$model = $this->getModel('Compiler');
		if (!$model->compile())                                         // 123
		{
			return false;
		}

Even with the button fixed, a resubmitted POST (refreshing while a compile runs)
or a second browser tab still collides. An exclusive lock held across
$model->compile(), refusing or queueing a second run, removes the whole class
of failure rather than one trigger for it.

3. Suggested — stop discarding the OS error

libraries/vendor_jcb/VDM.Joomla/src/Componentbuilder/Compiler/Utilities/Folder.php,
create() at line 69, the call at lines 75-77:

	public function create(string $path, bool $addHtml = true)     // 69
	{
		// check if the path exist
		if (!is_dir($path))                                        // 72
		{
			// create the path
			JoomlaFolder::create(                                  // 75
				$path
			);

Joomla\Filesystem\Folder::create() uses @mkdir(), so every report of this
bug says only "Could not create directory" with no reason. error_get_last()
still holds it — including it in the rethrown message would have made this
diagnosable immediately instead of requiring a patched file to find.

The same spot is also a check-then-act race. Tolerating it makes the compiler
resilient regardless of trigger:

		if (!is_dir($path))
		{
			try
			{
				JoomlaFolder::create($path);
			}
			catch (\Throwable $e)
			{
				clearstatcache(true, $path);

				// another process created it between the check and the call
				if (!is_dir($path))
				{
					throw $e;
				}

				return;
			}
### What Happened? I found out the compiler is triggered twice in most browser causing race issues during compilation resulting int folder and file creation and deletion errors. ### Steps to reproduce the Bug compile any component in any browser and wait for compilation errors. this has been a longstanding issue and the fix was always to use firefox. But as of lately firefox also began behaving badly. ### Which Joomla version are you compiling in? 6.1.3 ### Which PHP version are you compiling in? 8.4 ### Which Joomla versions are you targeting? 6 ### Which PHP version are you targeting? 8.4 ### Which Web server is JCB running on? litespeed ### Which Relational Database is JCB running on? mariadb 11.x ### Which OS is JCB running on? almalinux 10 ### Which JCB version are you using? 6.1.6 stable ### Where in JCB did this issue occur? Compilation of a component ### On which browsers did you encounter the issue? Firefox, Chrome, Safari ### Additional Comments # PR: compile button submits the form twice, causing two concurrent compiles Repo: https://git.vdm.dev/joomla/Component-Builder Found on JCB 6.1.6, Joomla 6.1.3, PHP 8.3. ## Symptom Compiling a component intermittently ends on the Joomla error page with one of: ``` Joomla\Filesystem\Folder::create: Could not create directory. Path: [ROOT]/administrator/components/com_componentbuilder/compiler/<component>/… Joomla\Filesystem\Folder::delete: Could not delete folder. Path: [ROOT]/administrator/components/com_componentbuilder/compiler/<component>/… ``` The path is different every time. The compile itself completes — refreshing the page shows the success screen and the component can be installed or downloaded. It usually takes a few compiles in a row before it happens. ## Cause `administrator/components/com_componentbuilder/tmpl/compiler/default.php:235` ```html <button class="btn btn-primary btn-lg px-4 me-sm-3" style="width: 100%;" onclick="Joomla.submitbutton('compiler.compiler')"> ``` The button declares no `type`, so it defaults to `type="submit"`, and it sits inside the `<form>` opened on line 223. The inline handler does not cancel the default action either (no `return false`). One click therefore fires both: 1. `onclick` -> `Joomla.submitbutton()` which sets `form.task.value`, hides `#form`, and schedules `setTimeout(() => form.submit(), 100)` 2. the browser's own native submit of the same form Both requests carry `task=compiler.compiler`, because the onclick runs before the default action. Two compilers then run against the same build directory. Line 219 of the same file already gets this right with `type="button"`. ## Evidence Logging one line per `Compiler\Utilities\Folder::remove()` call: ``` 22:00:52.1296 pid=123016 remove(.../compiler/com_component_v1_1_29__J6) 22:00:52.1316 pid=124403 remove(.../compiler/com_component_v1_1_29__J6) ``` Two PIDs, 2 ms apart, resetting the same build directory. Too fast to be a double-click. Joomla's `Folder::create()` calls `@mkdir()`, so the real reason is discarded. Capturing `error_get_last()` at the throw site gives it: ``` path : .../com_component_v1_1_29__J6/libraries/tlweb_component/Component.Joomla/src/Data mkdir said: mkdir(): File exists ancestors : exists dir perms=0755 .../Component.Joomla/src/Data listing : .../src/Data/index.html .../src/Data/Factory.php ``` The directory already exists and already holds files written by the other process. So: process A checks `is_dir()` -> false, process B creates the folder and writes into it, process A calls `mkdir()` -> `EEXIST`. The `Could not delete folder` variant is the same race in `Initializer::resetBuildDirectory()`, where one process removes a subtree while the other is iterating it. Confirmed independently by running two compiles over one build directory in isolation: both messages reproduce, at varying paths. ## Verification With the one-line fix below applied, 10 consecutive compiles of the same component: no failures, and no second PID ever touching the build directory within the same second. ``` 22:06:44 .. 22:07:45 pid=253633 (6 compiles) 22:08:32 .. 22:09:03 pid=284468 (4 compiles) ``` Two PIDs appear, but sequentially - the pool recycled the worker during the ~47s gap. Before the fix the two PIDs hit the same path 2 ms apart. ## Fix Paths below are from an installed JCB 6.1.6; adjust for the repo layout if it differs. | # | File | Line | Change | |---|------|------|--------| | 1 | `administrator/components/com_componentbuilder/tmpl/compiler/default.php` | 235 | add `type="button"` | | 2 | `administrator/components/com_componentbuilder/src/Controller/CompilerController.php` | 110-127 | guard `compiler()` against concurrent runs | | 3 | `libraries/vendor_jcb/VDM.Joomla/src/Componentbuilder/Compiler/Utilities/Folder.php` | 69-80 | surface the OS error / tolerate the race | ### 1. The trigger — one attribute (this alone fixes the reported bug) `administrator/components/com_componentbuilder/tmpl/compiler/default.php` line 235: ```diff --- a/administrator/components/com_componentbuilder/tmpl/compiler/default.php +++ b/administrator/components/com_componentbuilder/tmpl/compiler/default.php @@ -235,1 +235,1 @@ - <button class="btn btn-primary btn-lg px-4 me-sm-3" style="width: 100%;" onclick="Joomla.submitbutton('compiler.compiler')"><span class="icon-cog icon-white"></span> + <button type="button" class="btn btn-primary btn-lg px-4 me-sm-3" style="width: 100%;" onclick="Joomla.submitbutton('compiler.compiler')"><span class="icon-cog icon-white"></span> ``` For reference, line 219 of the same file already uses `type="button"`. ### 2. Suggested — make the compiler safe against concurrency `administrator/components/com_componentbuilder/src/Controller/CompilerController.php`, `compiler()` starts at line 110. It checks the token (line 113) and `core.manage` (line 117), then calls `$model->compile()` at line 123 with no guard of any kind: ```php public function compiler(): bool { Session::checkToken() or \jexit(Text::_('JINVALID_TOKEN')); // 113 $user = $this->app->getIdentity(); // 116 if (!$user->authorise('core.manage', 'com_componentbuilder')) // 117 { return false; } $model = $this->getModel('Compiler'); if (!$model->compile()) // 123 { return false; } ``` Even with the button fixed, a resubmitted POST (refreshing while a compile runs) or a second browser tab still collides. An exclusive lock held across `$model->compile()`, refusing or queueing a second run, removes the whole class of failure rather than one trigger for it. ### 3. Suggested — stop discarding the OS error `libraries/vendor_jcb/VDM.Joomla/src/Componentbuilder/Compiler/Utilities/Folder.php`, `create()` at line 69, the call at lines 75-77: ```php public function create(string $path, bool $addHtml = true) // 69 { // check if the path exist if (!is_dir($path)) // 72 { // create the path JoomlaFolder::create( // 75 $path ); ``` `Joomla\Filesystem\Folder::create()` uses `@mkdir()`, so every report of this bug says only "Could not create directory" with no reason. `error_get_last()` still holds it — including it in the rethrown message would have made this diagnosable immediately instead of requiring a patched file to find. The same spot is also a check-then-act race. Tolerating it makes the compiler resilient regardless of trigger: ```php if (!is_dir($path)) { try { JoomlaFolder::create($path); } catch (\Throwable $e) { clearstatcache(true, $path); // another process created it between the check and the call if (!is_dir($path)) { throw $e; } return; } ```
Tom van der Laan added the Bug label 2026-09-08 21:02:57 +00:00

I can confirm that this is an issue with JCB 4.1.5 as well, and just started breaking on Librewolf also.

I can confirm that this is an issue with JCB 4.1.5 as well, and just started breaking on Librewolf also.
Robot referenced this issue from a commit 2026-10-08 15:36:34 +00:00
Owner

This should be fixed now, please confirm.

This should be fixed now, please confirm.
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: joomla/Component-Builder#1318