From f2021e119a519cb315e47a6e19f992cc59dbddc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:58:52 +0000 Subject: [PATCH 1/5] Initial plan From df8a5d7917d93fe18533c851bc9b8ca5028d1381 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:09:58 +0000 Subject: [PATCH 2/5] Fix dev server exit on Windows by using custom process manager Co-authored-by: timonweb <483900+timonweb@users.noreply.github.com> --- src/tailwind/management/commands/tailwind.py | 132 ++++++++++++++++--- tests/test_dev_command.py | 90 +++++++++++++ 2 files changed, 202 insertions(+), 20 deletions(-) diff --git a/src/tailwind/management/commands/tailwind.py b/src/tailwind/management/commands/tailwind.py index e3b4a03..f6ccfb8 100644 --- a/src/tailwind/management/commands/tailwind.py +++ b/src/tailwind/management/commands/tailwind.py @@ -1,5 +1,6 @@ import os import subprocess +import sys from django.core.management.base import CommandError from django.core.management.base import LabelCommand @@ -143,21 +144,92 @@ def handle_build_command(self, **options): def handle_start_command(self, **options): self.npm_command("run", "start") - def handle_dev_command(self, **options): - # Check if honcho is installed - try: - subprocess.run(["honcho", "--version"], check=True, capture_output=True) - except (subprocess.CalledProcessError, FileNotFoundError): - self.stdout.write("Honcho is not installed. Installing honcho...") + def _run_dev_processes_windows(self, procfile_path): + """Run dev processes on Windows using direct subprocess management.""" + import signal + import threading + import time + + # Parse Procfile to get commands + commands = {} + with open(procfile_path) as f: + for line in f: + line = line.strip() + if line and ":" in line: + name, command = line.split(":", 1) + commands[name.strip()] = command.strip() + + if not commands: + raise CommandError("No commands found in Procfile.tailwind") + + # Track processes + processes = [] + stop_event = threading.Event() + + def run_process(name, command): + """Run a single process.""" try: - install_pip_package("honcho") - self.stdout.write(self.style.SUCCESS("Honcho installed successfully!")) - except Exception as err: - raise CommandError( - "Failed to install 'honcho' via pip. Please install it manually " - "(https://pypi.org/project/honcho/) and run 'python manage.py tailwind dev' again." - ) from err + # On Windows, use CREATE_NEW_PROCESS_GROUP to allow proper termination + # This constant is only available on Windows + kwargs = {"shell": True} + if hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"): + kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP + + proc = subprocess.Popen(command, **kwargs) + processes.append(proc) + proc.wait() + except Exception as e: + if not stop_event.is_set(): + self.stdout.write(f"\n{name} process error: {e}") + + # Start all processes in separate threads + threads = [] + for name, command in commands.items(): + thread = threading.Thread(target=run_process, args=(name, command), daemon=True) + thread.start() + threads.append(thread) + + try: + # Wait for KeyboardInterrupt + while True: + if not any(t.is_alive() for t in threads): + break + threading.Event().wait(0.1) + except KeyboardInterrupt: + self.stdout.write("\nStopping development servers...") + stop_event.set() + + # Terminate all processes + for proc in processes: + try: + if proc.poll() is None: # Process is still running + # On Windows, send CTRL_BREAK_EVENT to the process group + if hasattr(signal, "CTRL_BREAK_EVENT"): + proc.send_signal(signal.CTRL_BREAK_EVENT) + else: + proc.terminate() + except Exception: + pass + + # Wait for processes to terminate, with timeout + start_time = time.time() + while any(proc.poll() is None for proc in processes): + if time.time() - start_time > 5: # 5 second timeout + # Force kill if needed + for proc in processes: + try: + if proc.poll() is None: + proc.kill() + except Exception: + pass + break + time.sleep(0.1) + + # Join threads + for thread in threads: + thread.join(timeout=1) + def handle_dev_command(self, **options): # Check if Procfile.tailwind exists, create if not procfile_path = os.path.join(os.getcwd(), "Procfile.tailwind") if not os.path.exists(procfile_path): @@ -184,13 +256,33 @@ def handle_dev_command(self, **options): self.stdout.write(line) self.stdout.write("") - # Start honcho with the Procfile - try: - subprocess.run(["honcho", "-f", "Procfile.tailwind", "start"], check=True) - except subprocess.CalledProcessError as err: - raise CommandError(f"Failed to start honcho: {err}") from err - except KeyboardInterrupt: - self.stdout.write("\nStopping development servers...") + # Use different approach based on platform + if sys.platform == "win32": + # On Windows, use custom process management for better cleanup + self._run_dev_processes_windows(procfile_path) + else: + # On Unix-like systems, use honcho + # Check if honcho is installed + try: + subprocess.run(["honcho", "--version"], check=True, capture_output=True) + except (subprocess.CalledProcessError, FileNotFoundError): + self.stdout.write("Honcho is not installed. Installing honcho...") + try: + install_pip_package("honcho") + self.stdout.write(self.style.SUCCESS("Honcho installed successfully!")) + except Exception as err: + raise CommandError( + "Failed to install 'honcho' via pip. Please install it manually " + "(https://pypi.org/project/honcho/) and run 'python manage.py tailwind dev' again." + ) from err + + # Start honcho with the Procfile + try: + subprocess.run(["honcho", "-f", "Procfile.tailwind", "start"], check=True) + except subprocess.CalledProcessError as err: + raise CommandError(f"Failed to start honcho: {err}") from err + except KeyboardInterrupt: + self.stdout.write("\nStopping development servers...") def handle_check_updates_command(self, **options): self.npm_command("outdated") diff --git a/tests/test_dev_command.py b/tests/test_dev_command.py index c099b9b..55cae21 100644 --- a/tests/test_dev_command.py +++ b/tests/test_dev_command.py @@ -1,3 +1,4 @@ +import contextlib import os from unittest import mock @@ -199,3 +200,92 @@ def test_procfile_content_format(): assert lines[1].startswith("tailwind:") assert "python manage.py runserver" in lines[0] assert "python manage.py tailwind start" in lines[1] + + +def test_tailwind_dev_command_windows_platform(settings, app_name, procfile_path): + """ + GIVEN a Tailwind app is initialized on Windows + WHEN the dev command is run + THEN it should use the Windows-specific process manager instead of honcho + """ + call_command("tailwind", "init", "--app-name", app_name, "--no-input") + settings.INSTALLED_APPS += [app_name] + settings.TAILWIND_APP_NAME = app_name + + # Ensure Procfile.tailwind doesn't exist + assert not os.path.exists(procfile_path) + + # Mock sys.platform to simulate Windows + with mock.patch("sys.platform", "win32"), mock.patch("subprocess.Popen") as MockPopen, mock.patch( + "threading.Thread" + ) as mock_thread: + + class MockPopenClass: + def __init__(self, *args, **kwargs): + self.returncode = None + + def poll(self): + return self.returncode + + def wait(self): + # Simulate immediate completion + self.returncode = 0 + + def send_signal(self, sig): + self.returncode = 0 + + def kill(self): + self.returncode = -1 + + MockPopen.side_effect = MockPopenClass + + # Set up threads to appear not alive + mock_thread_instance = mock.Mock() + mock_thread_instance.is_alive.return_value = False + mock_thread.return_value = mock_thread_instance + + # Call dev command - should use Windows path + with contextlib.suppress(KeyboardInterrupt): + call_command("tailwind", "dev") + + # Verify Procfile was created + assert os.path.exists(procfile_path), "Procfile.tailwind should be created" + + +def test_tailwind_dev_command_windows_keyboard_interrupt(settings, app_name, procfile_path): + """ + GIVEN a Tailwind app is initialized on Windows and the dev command is running + WHEN a KeyboardInterrupt is received (user presses Ctrl+C) + THEN the command should properly terminate all child processes and exit gracefully + """ + call_command("tailwind", "init", "--app-name", app_name, "--no-input") + settings.INSTALLED_APPS += [app_name] + settings.TAILWIND_APP_NAME = app_name + + # Create Procfile + procfile_content = """django: python manage.py runserver +tailwind: python manage.py tailwind start""" + with open(procfile_path, "w") as f: + f.write(procfile_content) + + # Mock sys.platform to simulate Windows + with mock.patch("sys.platform", "win32"): + # Track if Windows process manager was called + windows_method_called = [False] + + # Patch the Windows-specific method + def mock_windows_method(self, procfile_path): + windows_method_called[0] = True + # Just return without actually running processes + return + + # We need to patch the method on the Command class in the module + with mock.patch( + "tailwind.management.commands.tailwind.Command._run_dev_processes_windows", + mock_windows_method, + ): + # Call dev command - should use Windows code path + call_command("tailwind", "dev") + + # Verify Windows-specific method was called + assert windows_method_called[0], "Windows-specific process manager should be used on Windows" From 9f4290766925864fca6f8f55ef15fa1ab6141aba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:13:22 +0000 Subject: [PATCH 3/5] Address code review feedback: improve error handling and add comments Co-authored-by: timonweb <483900+timonweb@users.noreply.github.com> --- src/tailwind/management/commands/tailwind.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/tailwind/management/commands/tailwind.py b/src/tailwind/management/commands/tailwind.py index f6ccfb8..0adc0a3 100644 --- a/src/tailwind/management/commands/tailwind.py +++ b/src/tailwind/management/commands/tailwind.py @@ -1,6 +1,9 @@ import os +import signal import subprocess import sys +import threading +import time from django.core.management.base import CommandError from django.core.management.base import LabelCommand @@ -146,10 +149,6 @@ def handle_start_command(self, **options): def _run_dev_processes_windows(self, procfile_path): """Run dev processes on Windows using direct subprocess management.""" - import signal - import threading - import time - # Parse Procfile to get commands commands = {} with open(procfile_path) as f: @@ -171,6 +170,9 @@ def run_process(name, command): try: # On Windows, use CREATE_NEW_PROCESS_GROUP to allow proper termination # This constant is only available on Windows + # Note: shell=True is used here because commands from Procfile.tailwind + # are meant to be shell commands (e.g., "python manage.py runserver"). + # The Procfile is part of the project and should be trusted content. kwargs = {"shell": True} if hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"): kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP @@ -178,7 +180,7 @@ def run_process(name, command): proc = subprocess.Popen(command, **kwargs) processes.append(proc) proc.wait() - except Exception as e: + except (OSError, subprocess.SubprocessError) as e: if not stop_event.is_set(): self.stdout.write(f"\n{name} process error: {e}") @@ -208,7 +210,8 @@ def run_process(name, command): proc.send_signal(signal.CTRL_BREAK_EVENT) else: proc.terminate() - except Exception: + except (OSError, ProcessLookupError): + # Process already terminated or not found pass # Wait for processes to terminate, with timeout @@ -220,7 +223,8 @@ def run_process(name, command): try: if proc.poll() is None: proc.kill() - except Exception: + except (OSError, ProcessLookupError): + # Process already terminated or not found pass break time.sleep(0.1) From a132488e6a01338c0bbafb5433c99e8be94cc213 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:51:01 +0000 Subject: [PATCH 4/5] Simplify dev command: remove honcho dependency, use simple subprocess approach Co-authored-by: timonweb <483900+timonweb@users.noreply.github.com> --- src/tailwind/management/commands/tailwind.py | 158 +++++----------- .../__init__.py | 0 .../apps.py | 5 + .../static_src/.gitignore | 1 + .../static_src/package.json | 26 +++ .../static_src/postcss.config.js | 7 + .../static_src/src/styles.css | 10 + .../templates/base.html | 20 ++ tests/test_dev_command.py | 175 +++++------------- 9 files changed, 155 insertions(+), 247 deletions(-) create mode 100644 test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/__init__.py create mode 100644 test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/apps.py create mode 100644 test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/.gitignore create mode 100644 test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/package.json create mode 100644 test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/postcss.config.js create mode 100644 test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/src/styles.css create mode 100644 test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/templates/base.html diff --git a/src/tailwind/management/commands/tailwind.py b/src/tailwind/management/commands/tailwind.py index 0adc0a3..4141728 100644 --- a/src/tailwind/management/commands/tailwind.py +++ b/src/tailwind/management/commands/tailwind.py @@ -1,9 +1,5 @@ import os -import signal import subprocess -import sys -import threading -import time from django.core.management.base import CommandError from django.core.management.base import LabelCommand @@ -147,93 +143,9 @@ def handle_build_command(self, **options): def handle_start_command(self, **options): self.npm_command("run", "start") - def _run_dev_processes_windows(self, procfile_path): - """Run dev processes on Windows using direct subprocess management.""" - # Parse Procfile to get commands - commands = {} - with open(procfile_path) as f: - for line in f: - line = line.strip() - if line and ":" in line: - name, command = line.split(":", 1) - commands[name.strip()] = command.strip() - - if not commands: - raise CommandError("No commands found in Procfile.tailwind") - - # Track processes - processes = [] - stop_event = threading.Event() - - def run_process(name, command): - """Run a single process.""" - try: - # On Windows, use CREATE_NEW_PROCESS_GROUP to allow proper termination - # This constant is only available on Windows - # Note: shell=True is used here because commands from Procfile.tailwind - # are meant to be shell commands (e.g., "python manage.py runserver"). - # The Procfile is part of the project and should be trusted content. - kwargs = {"shell": True} - if hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"): - kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP - - proc = subprocess.Popen(command, **kwargs) - processes.append(proc) - proc.wait() - except (OSError, subprocess.SubprocessError) as e: - if not stop_event.is_set(): - self.stdout.write(f"\n{name} process error: {e}") - - # Start all processes in separate threads - threads = [] - for name, command in commands.items(): - thread = threading.Thread(target=run_process, args=(name, command), daemon=True) - thread.start() - threads.append(thread) - - try: - # Wait for KeyboardInterrupt - while True: - if not any(t.is_alive() for t in threads): - break - threading.Event().wait(0.1) - except KeyboardInterrupt: - self.stdout.write("\nStopping development servers...") - stop_event.set() - - # Terminate all processes - for proc in processes: - try: - if proc.poll() is None: # Process is still running - # On Windows, send CTRL_BREAK_EVENT to the process group - if hasattr(signal, "CTRL_BREAK_EVENT"): - proc.send_signal(signal.CTRL_BREAK_EVENT) - else: - proc.terminate() - except (OSError, ProcessLookupError): - # Process already terminated or not found - pass - - # Wait for processes to terminate, with timeout - start_time = time.time() - while any(proc.poll() is None for proc in processes): - if time.time() - start_time > 5: # 5 second timeout - # Force kill if needed - for proc in processes: - try: - if proc.poll() is None: - proc.kill() - except (OSError, ProcessLookupError): - # Process already terminated or not found - pass - break - time.sleep(0.1) - - # Join threads - for thread in threads: - thread.join(timeout=1) - def handle_dev_command(self, **options): + import time + # Check if Procfile.tailwind exists, create if not procfile_path = os.path.join(os.getcwd(), "Procfile.tailwind") if not os.path.exists(procfile_path): @@ -260,33 +172,47 @@ def handle_dev_command(self, **options): self.stdout.write(line) self.stdout.write("") - # Use different approach based on platform - if sys.platform == "win32": - # On Windows, use custom process management for better cleanup - self._run_dev_processes_windows(procfile_path) - else: - # On Unix-like systems, use honcho - # Check if honcho is installed - try: - subprocess.run(["honcho", "--version"], check=True, capture_output=True) - except (subprocess.CalledProcessError, FileNotFoundError): - self.stdout.write("Honcho is not installed. Installing honcho...") + # Parse Procfile to get commands + commands = [] + with open(procfile_path) as f: + for line in f: + line = line.strip() + if line and ":" in line: + _, command = line.split(":", 1) + commands.append(command.strip()) + + if not commands: + raise CommandError("No commands found in Procfile.tailwind") + + # Start all processes + processes = [] + try: + for cmd in commands: + proc = subprocess.Popen(cmd, shell=True) + processes.append(proc) + + # Wait for any process to exit (or KeyboardInterrupt) + while True: + # Check if any process has exited + for proc in processes: + if proc.poll() is not None: + raise CommandError(f"A process exited unexpectedly with code {proc.returncode}") + # Small sleep to avoid busy waiting + time.sleep(0.1) + + except KeyboardInterrupt: + self.stdout.write("\nStopping development servers...") + finally: + # Terminate all processes + import contextlib + + for proc in processes: try: - install_pip_package("honcho") - self.stdout.write(self.style.SUCCESS("Honcho installed successfully!")) - except Exception as err: - raise CommandError( - "Failed to install 'honcho' via pip. Please install it manually " - "(https://pypi.org/project/honcho/) and run 'python manage.py tailwind dev' again." - ) from err - - # Start honcho with the Procfile - try: - subprocess.run(["honcho", "-f", "Procfile.tailwind", "start"], check=True) - except subprocess.CalledProcessError as err: - raise CommandError(f"Failed to start honcho: {err}") from err - except KeyboardInterrupt: - self.stdout.write("\nStopping development servers...") + proc.terminate() + proc.wait(timeout=5) + except Exception: + with contextlib.suppress(Exception): + proc.kill() def handle_check_updates_command(self, **options): self.npm_command("outdated") diff --git a/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/__init__.py b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/apps.py b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/apps.py new file mode 100644 index 0000000..265c7f6 --- /dev/null +++ b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class Test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3Config(AppConfig): + name = 'test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3' diff --git a/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/.gitignore b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/package.json b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/package.json new file mode 100644 index 0000000..53d6555 --- /dev/null +++ b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/package.json @@ -0,0 +1,26 @@ +{ + "name": "test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3", + "version": "4.3.0", + "description": "", + "scripts": { + "start": "npm run dev", + "build": "npm run build:clean && npm run build:tailwind", + "build:clean": "rimraf ../static/css/dist", + "build:tailwind": "cross-env NODE_ENV=production postcss ./src/styles.css -o ../static/css/dist/styles.css --minify", + "dev": "cross-env NODE_ENV=development postcss ./src/styles.css -o ../static/css/dist/styles.css --watch" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "@tailwindcss/postcss": "^4.1.16", + + "cross-env": "^10.1.0", + "postcss": "^8.5.6", + "postcss-cli": "^11.0.1", + "postcss-nested": "^7.0.2", + "postcss-simple-vars": "^7.0.1", + "rimraf": "^6.0.1", + "tailwindcss": "^4.1.16" + } +} diff --git a/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/postcss.config.js b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/postcss.config.js new file mode 100644 index 0000000..19a4c6a --- /dev/null +++ b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + "@tailwindcss/postcss": {}, + "postcss-simple-vars": {}, + "postcss-nested": {} + }, +} diff --git a/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/src/styles.css b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/src/styles.css new file mode 100644 index 0000000..9bf3c00 --- /dev/null +++ b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/static_src/src/styles.css @@ -0,0 +1,10 @@ +@import "tailwindcss"; + +/** + * A catch-all path to Django template files, JavaScript, and Python files + * that contain Tailwind CSS classes and will be scanned by Tailwind to generate the final CSS file. + * + * If your final CSS file is not being updated after code changes, you may want to broaden or narrow + * the scope of this path. + */ +@source "../../../**/*.{html,py,js}"; diff --git a/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/templates/base.html b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/templates/base.html new file mode 100644 index 0000000..3b72748 --- /dev/null +++ b/test_theme_0512e3b0_b8a2_11f0_bdab_6045bd02a4a3/templates/base.html @@ -0,0 +1,20 @@ +{% load static tailwind_tags %} + + +
+