feat(system): fix(windows): handler guard backslash path fix — all 11 __init__.py files (#304). caller_file.replace('\\', '/') normalizes Windows paths before the same-branch check. This is the actual fix for #293 which was incorrectly closed. drone is completely broken on Windows without this. (#306)
* feat(system): ci: add Windows setup test workflow — runs setup.sh + drone CLI verification on windows-latest GitHub Actions runner. Triggers on changes to setup.sh, handler __init__.py files, cli.py, or pyproject.toml. Closes the 'we never tested on Windows' gap. Co-Authored-By: @devpulse <devpulse@aipass> * feat(system): fix(windows): SIGPIPE guard in flow.py (#301) + fcntl platform guards in trigger/config.py and watchdog/registry.py (#302) — lazy import fcntl on Unix only, no-op on Windows. inbox_lock.py already cross-platform (msvcrt). ai_mail.py already guarded (hasattr check). Co-Authored-By: @devpulse <devpulse@aipass> * feat(system): fix(windows): handler guard backslash path fix — all 11 __init__.py files (#304). caller_file.replace('\\', '/') normalizes Windows paths before the same-branch check. This is the actual fix for #293 which was incorrectly closed. drone is completely broken on Windows without this. Co-Authored-By: @devpulse <devpulse@aipass> --------- Co-authored-by: @devpulse <devpulse@aipass>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
name: Windows Setup Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'setup.sh'
|
||||
- 'src/aipass/*/apps/handlers/__init__.py'
|
||||
- 'src/aipass/drone/cli.py'
|
||||
- 'pyproject.toml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'setup.sh'
|
||||
- 'src/aipass/*/apps/handlers/__init__.py'
|
||||
- 'src/aipass/drone/cli.py'
|
||||
- 'pyproject.toml'
|
||||
|
||||
jobs:
|
||||
windows-setup:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Run setup.sh
|
||||
shell: bash
|
||||
run: bash setup.sh
|
||||
|
||||
- name: Verify drone CLI
|
||||
shell: bash
|
||||
run: |
|
||||
export PYTHONUTF8=1
|
||||
drone --version
|
||||
drone systems
|
||||
drone @seedgo --help
|
||||
@@ -78,7 +78,7 @@ def _guard_branch_access():
|
||||
return # Allow if truly can't determine
|
||||
|
||||
# Check if caller is from our branch
|
||||
if f"/{MY_BRANCH}/" in caller_file:
|
||||
if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"):
|
||||
return # Same branch, allowed
|
||||
|
||||
# External caller - block access
|
||||
|
||||
@@ -53,7 +53,7 @@ def _guard_branch_access():
|
||||
return # Allow command-line Python through
|
||||
return
|
||||
|
||||
if f"/{MY_BRANCH}/" in caller_file:
|
||||
if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"):
|
||||
return
|
||||
|
||||
caller_branch = _extract_branch_name(caller_file)
|
||||
|
||||
@@ -99,7 +99,7 @@ def _guard_branch_access():
|
||||
return # Allow if truly can't determine
|
||||
|
||||
# Check if caller is from our branch
|
||||
if f"/{MY_BRANCH}/" in caller_file:
|
||||
if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"):
|
||||
return # Same branch, allowed
|
||||
|
||||
# External caller - block access
|
||||
|
||||
@@ -40,7 +40,6 @@ Registry file schema (version 1):
|
||||
}
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
@@ -122,7 +121,7 @@ def _atomic_write_unlocked(storage_path: Path, data: dict) -> None:
|
||||
|
||||
|
||||
class _FileLock:
|
||||
"""fcntl.flock-based exclusive lock on a sibling .lock file.
|
||||
"""Exclusive lock on a sibling .lock file (fcntl on Unix, no-op on Windows).
|
||||
|
||||
Using a sibling avoids racing with the atomic replace of the data file:
|
||||
if we locked the data file itself, os.replace would swap the inode out
|
||||
@@ -134,6 +133,9 @@ class _FileLock:
|
||||
self._fh = None
|
||||
|
||||
def __enter__(self) -> "_FileLock":
|
||||
if sys.platform == "win32":
|
||||
return self # Windows: skip file locking (single-user typical)
|
||||
import fcntl
|
||||
self._lock_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# 'a+' so the file is created if missing and lock survives concurrent opens.
|
||||
self._fh = open(self._lock_path, "a+", encoding='utf-8')
|
||||
@@ -143,6 +145,7 @@ class _FileLock:
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
if self._fh is not None:
|
||||
try:
|
||||
import fcntl
|
||||
fcntl.flock(self._fh.fileno(), fcntl.LOCK_UN)
|
||||
finally:
|
||||
self._fh.close()
|
||||
|
||||
@@ -80,7 +80,7 @@ def _guard_branch_access():
|
||||
# Check if caller is from our branch
|
||||
# MY_BRANCH is "aipass.drone" (dotted), but filesystem uses "/aipass/drone/"
|
||||
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
|
||||
if branch_path in caller_file:
|
||||
if branch_path in caller_file.replace("\\", "/"):
|
||||
return # Same branch, allowed
|
||||
|
||||
# External caller - block access
|
||||
|
||||
@@ -26,7 +26,9 @@ import signal
|
||||
from typing import List, Any
|
||||
|
||||
# Handle broken pipe gracefully (e.g. output piped to head)
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
# SIGPIPE does not exist on Windows
|
||||
if hasattr(signal, 'SIGPIPE'):
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
|
||||
# Prax logger
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
@@ -78,7 +78,7 @@ def _guard_branch_access():
|
||||
return # Allow if truly can't determine
|
||||
|
||||
# Check if caller is from our branch
|
||||
if f"/{MY_BRANCH}/" in caller_file:
|
||||
if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"):
|
||||
return # Same branch, allowed
|
||||
|
||||
# External caller - block access
|
||||
|
||||
@@ -80,7 +80,7 @@ def _guard_branch_access():
|
||||
# Check if caller is from our branch
|
||||
# MY_BRANCH is "aipass.memory" (dotted), but filesystem uses "/aipass/memory/"
|
||||
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
|
||||
if branch_path in caller_file:
|
||||
if branch_path in caller_file.replace("\\", "/"):
|
||||
return # Same branch, allowed
|
||||
|
||||
# External caller - block access
|
||||
|
||||
@@ -80,7 +80,7 @@ def _guard_branch_access():
|
||||
# Check if caller is from our branch
|
||||
# MY_BRANCH is "aipass.prax" (dotted), but filesystem uses "/aipass/prax/"
|
||||
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
|
||||
if branch_path in caller_file:
|
||||
if branch_path in caller_file.replace("\\", "/"):
|
||||
return # Same branch, allowed
|
||||
|
||||
# External caller - block access
|
||||
|
||||
@@ -85,7 +85,7 @@ def _guard_branch_access():
|
||||
# Check if caller is from our branch
|
||||
# MY_BRANCH is "aipass.seedgo" (dotted), but filesystem uses "/aipass/seedgo/"
|
||||
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
|
||||
if branch_path in caller_file:
|
||||
if branch_path in caller_file.replace("\\", "/"):
|
||||
return # Same branch, allowed
|
||||
|
||||
# External caller - block access
|
||||
|
||||
@@ -59,7 +59,7 @@ def _guard_branch_access():
|
||||
return
|
||||
|
||||
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
|
||||
if branch_path in caller_file:
|
||||
if branch_path in caller_file.replace("\\", "/"):
|
||||
return
|
||||
|
||||
caller_branch = _extract_branch_name(caller_file)
|
||||
|
||||
@@ -59,7 +59,7 @@ def _guard_branch_access():
|
||||
return
|
||||
|
||||
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
|
||||
if branch_path in caller_file:
|
||||
if branch_path in caller_file.replace("\\", "/"):
|
||||
return
|
||||
|
||||
caller_branch = _extract_branch_name(caller_file)
|
||||
|
||||
@@ -13,8 +13,8 @@ Provides package-relative paths for trigger data directories.
|
||||
Works in both pip-installed and development environments.
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
@@ -81,12 +81,17 @@ def json_file_lock(path: Path):
|
||||
"""
|
||||
lock_path = path.with_suffix('.lock')
|
||||
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(lock_path, 'w', encoding='utf-8') as lock_f:
|
||||
fcntl.flock(lock_f, fcntl.LOCK_EX)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
fcntl.flock(lock_f, fcntl.LOCK_UN)
|
||||
if sys.platform == "win32":
|
||||
# Windows: no fcntl, skip file locking (single-user typical)
|
||||
yield
|
||||
else:
|
||||
import fcntl
|
||||
with open(lock_path, 'w', encoding='utf-8') as lock_f:
|
||||
fcntl.flock(lock_f, fcntl.LOCK_EX)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
fcntl.flock(lock_f, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def print_introspection():
|
||||
|
||||
@@ -53,7 +53,7 @@ def _guard_branch_access():
|
||||
return # Allow command-line Python through
|
||||
return
|
||||
|
||||
if f"/trigger/" in caller_file:
|
||||
if f"/trigger/" in caller_file.replace("\\", "/"):
|
||||
return
|
||||
|
||||
caller_branch = _extract_branch_name(caller_file)
|
||||
|
||||
Reference in New Issue
Block a user