feat: Windows support (#2119)

Closes #83 #880 #1286.

Working patch for Windows support.

Supports linking to the [official PHP release (TS
version)](https://www.php.net/downloads.php).
Includes some work from #1286 (thanks @TenHian!!)

This patch allows using Visual Studio to compile the cgo code. To do so,
it must be compiled with Go 1.26 (RC) with the following setup:

```powershell
winget install -e --id Microsoft.VisualStudio.2022.Community --override "--passive --wait --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --includeRecommended"
winget install -e --id GoLang.Go

$env:PATH += ';C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\bin'

cd c:\
gh repo clone microsoft/vcpkg
.\vcpkg\bootstrap-vcpkg.bat
.\vcpkg\vcpkg install pthreads brotli

# build watcher
Invoke-WebRequest -Uri "https://github.com/e-dant/watcher/releases/download/0.14.3/x86_64-pc-windows-msvc.tar" -OutFile "$env:TEMP\watcher.tar"
tar -xf "$env:TEMP\watcher.tar" -C C:\
Rename-Item -Path "C:\x86_64-pc-windows-msvc" -NewName "watcher-x86_64-pc-windows-msvc"
Remove-Item "$env:TEMP\watcher.tar"

# download php
Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php.zip"
Expand-Archive -Path "$env:TEMP\php.zip" -DestinationPath "C:\"
Remove-Item "$env:TEMP\php.zip"

# download php development package
Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-devel-pack-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php-devel.zip"
Expand-Archive -Path "$env:TEMP\php-devel.zip" -DestinationPath "C:\"
Remove-Item "$env:TEMP\php-devel.zip"

$env:GOTOOLCHAIN = 'go1.26rc1'
$env:CC = 'clang'
$env:CXX = 'clang++'
$env:CGO_CFLAGS = "-I$env:C:\vcpkg\installed\x64-windows\include -IC:\watcher-x86_64-pc-windows-msvc -IC:\php-8.5.1-devel-vs17-x64\include -IC:\php-8.5.1-devel-vs17-x64\include\main -IC:\php-8.5.1-devel-vs17-x64\include\TSRM -IC:\php-8.5.1-devel-vs17-x64\include\Zend -IC:\php-8.5.1-devel-vs17-x64\include\ext"
$env:CGO_LDFLAGS = '-LC:\vcpkg\installed\x64-windows\lib -lbrotlienc -LC:\watcher-x86_64-pc-windows-msvc -llibwatcher-c -LC:\php-8.5.1-Win32-vs17-x64 -LC:\php-8.5.1-devel-vs17-x64\lib -lphp8ts -lphp8embed'

# clone frankenphp and build
git clone -b windows https://github.com/php/frankenphp.git
cd frankenphp\caddy\frankenphp
go build -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx

# Tests

$env:PATH += ";$env:VCPKG_ROOT\installed\x64-windows\bin;C:\watcher-x86_64-pc-windows-msvc";C:\php-8.5.1-Win32-vs17-x64"
"opcache.enable=0`r`nopcache.enable_cli=0" | Out-File -Encoding ascii php.ini
$env:PHPRC = Get-Location
go test -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx .
```

TODO:

- [x] Fix remaining skipped tests (scaling and watcher)
- [x] Test if the watcher mode works as expected
- [x] Automate the build with GitHub Actions

---------

Signed-off-by: Marc <m@pyc.ac>
Co-authored-by: Kévin Dunglas <kevin@dunglas.dev>
Co-authored-by: DubbleClick <m@pyc.ac>
This commit is contained in:
Kévin Dunglas
2026-02-26 12:38:14 +01:00
committed by GitHub
parent 0ae33863d0
commit 25ed020036
42 changed files with 532 additions and 149 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -453,12 +453,12 @@ jobs:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
- uses: actions/setup-go@v6
with:
with: # zizmor: ignore[cache-poisoning]
go-version: "1.26"
cache-dependency-path: |
go.sum
caddy/go.sum
cache: false
cache: ${{ github.event_name != 'release' }}
- name: Set FRANKENPHP_VERSION
run: |
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then

View File

@@ -35,6 +35,7 @@ jobs:
env:
GOMAXPROCS: 10
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
GOFLAGS: "-tags=nobadger,nomysql,nopgx"
steps:
- uses: actions/checkout@v6
with:
@@ -69,7 +70,7 @@ jobs:
run: ./frankenphp.test -test.v
- name: Run Caddy module tests
working-directory: caddy/
run: go test -tags nobadger,nomysql,nopgx -race -v ./...
run: go test -race -v ./...
- name: Run Fuzzing Tests
working-directory: caddy/
run: go test -fuzz FuzzRequest -fuzztime 20s
@@ -100,6 +101,8 @@ jobs:
fail-fast: false
matrix:
php-versions: ["8.3", "8.4", "8.5"]
env:
XCADDY_GO_BUILD_FLAGS: "-tags=nobadger,nomysql,nopgx"
steps:
- uses: actions/checkout@v6
with:
@@ -141,6 +144,7 @@ jobs:
runs-on: macos-latest
env:
HOMEBREW_NO_AUTO_UPDATE: 1
GOFLAGS: "-tags=nowatcher,nobadger,nomysql,nopgx"
steps:
- uses: actions/checkout@v6
with:
@@ -172,4 +176,4 @@ jobs:
run: go test -tags nowatcher -race -v ./...
- name: Run Caddy module tests
working-directory: caddy/
run: go test -tags nowatcher,nobadger,nomysql,nopgx -race -v ./...
run: go test -race -v ./...

223
.github/workflows/windows.yaml vendored Normal file
View File

@@ -0,0 +1,223 @@
name: Build Windows release
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on:
pull_request:
branches:
- main
paths:
- "docker-bake.hcl"
- "vcpkg.json"
- ".github/workflows/static.yaml"
- "**cgo.go"
- "**Dockerfile"
- "**.c"
- "**.h"
- "**.sh"
- "**.stub.php"
push:
branches:
- main
tags:
- v*.*.*
workflow_dispatch:
inputs:
#checkov:skip=CKV_GHA_7
version:
description: "FrankenPHP version"
required: false
type: string
schedule:
- cron: "0 8 * * *"
permissions:
contents: read
env:
GOTOOLCHAIN: local
GOFLAGS: "-ldflags=-extldflags=-fuse-ld=lld -tags=nobadger,nomysql,nopgx"
PHP_DOWNLOAD_BASE: "https://downloads.php.net/~windows/releases/"
CC: clang
CXX: clang++
jobs:
build:
runs-on: windows-latest
defaults:
run:
shell: powershell
steps:
- name: Configure Git
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout Code
uses: actions/checkout@v6
with:
path: frankenphp
persist-credentials: false
- name: Set FRANKENPHP_VERSION
run: |
if ($env:GITHUB_REF_TYPE -eq "tag") {
$frankenphpVersion = $env:GITHUB_REF_NAME.Substring(1)
} elseif ($env:GITHUB_EVENT_NAME -eq "schedule") {
$frankenphpVersion = $env:GITHUB_REF
} else {
$frankenphpVersion = $env:GITHUB_SHA
}
echo "FRANKENPHP_VERSION=$frankenphpVersion" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Setup Go
uses: actions/setup-go@v6
with: # zizmor: ignore[cache-poisoning]
go-version: "1.26"
cache-dependency-path: |
frankenphp/go.sum
frankenphp/caddy/go.sum
cache: ${{ github.event_name != 'release' }}
check-latest: true
- name: Install Vcpkg Libraries
working-directory: frankenphp
run: "vcpkg install"
- name: Download Watcher
run: |
$latestTag = gh release list --repo e-dant/watcher --limit 1 --exclude-drafts --exclude-pre-releases --json tagName --jq '.[0].tagName'
Write-Host "Latest Watcher version: $latestTag"
gh release download $latestTag --repo e-dant/watcher --pattern "*x86_64-pc-windows-msvc.tar" -O watcher.tar
tar -xf "watcher.tar" -C "$env:GITHUB_WORKSPACE"
Rename-Item -Path "$env:GITHUB_WORKSPACE\x86_64-pc-windows-msvc" -NewName "watcher"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download PHP
run: |
$webContent = Invoke-WebRequest -Uri $env:PHP_DOWNLOAD_BASE -UseBasicParsing
$links = $webContent.Links.Href | Where-Object { $_ -match "php-\d+\.\d+\.\d+-Win32-vs17-x64\.zip$" }
if (-not $links) { throw "Could not find PHP zip files at $env:PHP_DOWNLOAD_BASE" }
$latestFile = $links | Sort-Object { if ($_ -match '(\d+\.\d+\.\d+)') { [version]$matches[1] } } | Select-Object -Last 1
$version = if ($latestFile -match '(\d+\.\d+\.\d+)') { $matches[1] }
Write-Host "Detected latest PHP version: $version"
"PHP_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append
$phpZip = "php-$version-Win32-vs17-x64.zip"
$develZip = "php-devel-pack-$version-Win32-vs17-x64.zip"
$dirName = "frankenphp-$env:FRANKENPHP_VERSION-php-$version-Win32-vs17-x64"
echo "DIR_NAME=$dirName" | Out-File -FilePath $env:GITHUB_ENV -Append
Invoke-WebRequest -Uri "$env:PHP_DOWNLOAD_BASE/$phpZip" -OutFile "$env:TEMP\php.zip"
Expand-Archive -Path "$env:TEMP\php.zip" -DestinationPath "$env:GITHUB_WORKSPACE\$dirName"
Invoke-WebRequest -Uri "$env:PHP_DOWNLOAD_BASE/$develZip" -OutFile "$env:TEMP\php-devel.zip"
Expand-Archive -Path "$env:TEMP\php-devel.zip" -DestinationPath "$env:GITHUB_WORKSPACE\php-devel"
- name: Prepare env
run: |
$vcpkgRoot = "$env:GITHUB_WORKSPACE\frankenphp\vcpkg_installed\x64-windows"
$watcherRoot = "$env:GITHUB_WORKSPACE\watcher"
$phpBin = "$env:GITHUB_WORKSPACE\$env:DIR_NAME"
$phpDevel = "$env:GITHUB_WORKSPACE\php-devel\php-$env:PHP_VERSION-devel-vs17-x64"
echo "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\Llvm\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
echo "$vcpkgRoot\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
echo "$watcherRoot" | Out-File -FilePath $env:GITHUB_PATH -Append
echo "$phpBin" | Out-File -FilePath $env:GITHUB_PATH -Append
echo "CGO_CFLAGS=-DFRANKENPHP_VERSION=$env:FRANKENPHP_VERSION -I$vcpkgRoot\include -I$watcherRoot -I$phpDevel\include -I$phpDevel\include\main -I$phpDevel\include\TSRM -I$phpDevel\include\Zend -I$phpDevel\include\ext" | Out-File -FilePath $env:GITHUB_ENV -Append
echo "CGO_LDFLAGS=-L$vcpkgRoot\lib -lbrotlienc -L$watcherRoot -llibwatcher-c -L$phpBin -L$phpDevel\lib -lphp8ts -lphp8embed" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Embed Windows icon and metadata
working-directory: frankenphp\caddy\frankenphp
run: |
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest
$major = 0; $minor = 0; $patch = 0; $build = 0
if ($env:FRANKENPHP_VERSION -match '^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$') {
$major = [int]$Matches['major']
$minor = [int]$Matches['minor']
$patch = [int]$Matches['patch']
}
$json = @{
FixedFileInfo = @{
FileVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }
ProductVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }
}
StringFileInfo = @{
CompanyName = "FrankenPHP"
FileDescription = "The modern PHP app server"
FileVersion = $env:FRANKENPHP_VERSION
InternalName = "frankenphp"
OriginalFilename = "frankenphp.exe"
LegalCopyright = "(c) 2022 Kévin Dunglas, MIT License"
ProductName = "FrankenPHP"
ProductVersion = $env:FRANKENPHP_VERSION
Comments = "https://frankenphp.dev/"
}
VarFileInfo = @{
Translation = @{ LangID = 9; CharsetID = 1200 }
}
} | ConvertTo-Json -Depth 10
[System.IO.File]::WriteAllText("versioninfo.json", $json, [System.Text.Encoding]::Default)
goversioninfo -64 -icon ..\..\frankenphp.ico versioninfo.json -o resource.syso
- name: Build FrankenPHP
run: |
$customVersion = "FrankenPHP $env:FRANKENPHP_VERSION PHP $env:PHP_VERSION Caddy"
go build -ldflags="-extldflags=-fuse-ld=lld -X 'github.com/caddyserver/caddy/v2.CustomVersion=$customVersion' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'"
working-directory: frankenphp\caddy\frankenphp
- name: Create Directory
run: |
Copy-Item frankenphp\caddy\frankenphp\frankenphp.exe $env:DIR_NAME
Copy-Item watcher\libwatcher-c.dll $env:DIR_NAME
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlienc.dll $env:DIR_NAME
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlidec.dll $env:DIR_NAME
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlicommon.dll $env:DIR_NAME
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\pthreadVC3.dll $env:DIR_NAME
- name: Upload Artifact
if: github.event_name != 'release'
uses: actions/upload-artifact@v6
with:
name: ${{ env.DIR_NAME }}
path: ${{ env.DIR_NAME }}
if-no-files-found: error
- name: Zip Release Artifact
if: github.event_name == 'release'
run: Compress-Archive -Path "$env:DIR_NAME\*" -DestinationPath "$env:DIR_NAME.zip"
- name: Upload Release Asset
if: github.event_name == 'release'
run: gh release upload $env:GITHUB_EVENT_RELEASE_TAG_NAME "$env:DIR_NAME.zip" --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
- name: Run Tests
run: |
"opcache.enable=0`r`nopcache.enable_cli=0" | Out-File php.ini
$env:PHPRC = Get-Location
go test ./...
cd caddy
go test ./...
working-directory: ${{ github.workspace }}\frankenphp

View File

@@ -42,6 +42,25 @@ func waitForServerReady(t *testing.T, url string) {
var testPort = "9080"
// skipIfSymlinkNotValid skips the test if the given path is not a valid symlink
func skipIfSymlinkNotValid(t *testing.T, path string) {
t.Helper()
info, err := os.Lstat(path)
if err != nil {
t.Skipf("symlink test skipped: cannot stat %s: %v", path, err)
}
if info.Mode()&os.ModeSymlink == 0 {
t.Skipf("symlink test skipped: %s is not a symlink (git may not support symlinks on this platform)", path)
}
}
// escapeMetricLabel escapes backslashes in label values for Prometheus text format
func escapeMetricLabel(s string) string {
return strings.ReplaceAll(s, "\\", "\\\\")
}
func TestPHP(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
@@ -573,6 +592,7 @@ func TestWorkerMetrics(t *testing.T) {
`, "caddyfile")
workerName, _ := fastabs.FastAbs("../testdata/index.php")
workerName = escapeMetricLabel(workerName)
// Make some requests
for i := range 10 {
@@ -760,6 +780,7 @@ func TestAutoWorkerConfig(t *testing.T) {
`, "caddyfile")
workerName, _ := fastabs.FastAbs("../testdata/index.php")
workerName = escapeMetricLabel(workerName)
// Make some requests
for i := range 10 {
@@ -835,6 +856,7 @@ func TestAllDefinedServerVars(t *testing.T) {
expectedBody = strings.ReplaceAll(expectedBody, "{documentRoot}", documentRoot)
expectedBody = strings.ReplaceAll(expectedBody, "\r\n", "\n")
expectedBody = strings.ReplaceAll(expectedBody, "{testPort}", testPort)
expectedBody = strings.ReplaceAll(expectedBody, documentRoot+"/", documentRoot+string(filepath.Separator))
tester := caddytest.NewTester(t)
tester.InitServer(`
{
@@ -1545,6 +1567,7 @@ func TestLog(t *testing.T) {
func TestSymlinkWorkerPaths(t *testing.T) {
cwd, _ := os.Getwd()
publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
skipIfSymlinkNotValid(t, publicDir)
t.Run("NeighboringWorkerScript", func(t *testing.T) {
// Scenario: neighboring worker script
@@ -1680,6 +1703,7 @@ func TestSymlinkResolveRoot(t *testing.T) {
cwd, _ := os.Getwd()
testDir := filepath.Join(cwd, "..", "testdata", "symlinks", "test")
publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
skipIfSymlinkNotValid(t, publicDir)
t.Run("ResolveRootSymlink", func(t *testing.T) {
// Tests that resolve_root_symlink directive works correctly
@@ -1738,6 +1762,7 @@ func TestSymlinkResolveRoot(t *testing.T) {
func TestSymlinkWorkerBehavior(t *testing.T) {
cwd, _ := os.Getwd()
publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
skipIfSymlinkNotValid(t, publicDir)
t.Run("WorkerScriptFailsWithoutWorkerMode", func(t *testing.T) {
// Tests that accessing a worker-only script without configuring it as a worker actually results in an error

View File

@@ -0,0 +1,5 @@
//go:build !nobrotli
package main
import _ "github.com/dunglas/caddy-cbrotli"

View File

@@ -5,7 +5,6 @@ import (
// plug in Caddy modules here.
_ "github.com/caddyserver/caddy/v2/modules/standard"
_ "github.com/dunglas/caddy-cbrotli"
_ "github.com/dunglas/frankenphp/caddy"
_ "github.com/dunglas/mercure/caddy"
_ "github.com/dunglas/vulcain/caddy"

View File

@@ -64,7 +64,7 @@ require (
github.com/dunglas/skipfilter v1.0.0 // indirect
github.com/dunglas/vulcain v1.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a // indirect
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect

View File

@@ -165,8 +165,8 @@ github.com/dunglas/vulcain/caddy v1.2.1/go.mod h1:8QrmLTfURmW2VgjTR6Gb9a53FrZjsp
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a h1:e/m9m8cJgjzw2Ol7tKTu4B/lM5F3Ym7ryKI+oyw0T8Y=
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be h1:vqHrvilasyJcnru/0Z4FoojsQJUIfXGVplte7JtupfY=
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be/go.mod h1:PmV4IVmBJVqT2NcfTGN4+sZ+qGe3PA0qkphAtOHeFG0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=

View File

@@ -7,6 +7,7 @@ import (
"log/slog"
"net/http"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
@@ -493,7 +494,13 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
if indexFile != "off" {
dirRedir := false
dirIndex := "{http.request.uri.path}/" + indexFile
// On Windows, first_exist_fallback doesn't work correctly because
// glob is skipped and patterns are returned as-is without checking existence.
// Use first_exist instead to ensure all files are checked.
tryPolicy := "first_exist_fallback"
if runtime.GOOS == "windows" {
tryPolicy = "first_exist"
}
// if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 {

2
cgi.go
View File

@@ -8,8 +8,8 @@ package frankenphp
// #cgo noescape frankenphp_register_variables_from_request_info
// #cgo noescape frankenphp_register_variable_safe
// #cgo noescape frankenphp_register_single
// #include <php_variables.h>
// #include "frankenphp.h"
// #include <php_variables.h>
import "C"
import (
"context"

6
cgo.go
View File

@@ -1,9 +1,11 @@
package frankenphp
// #cgo darwin pkg-config: libxml-2.0
// #cgo CFLAGS: -Wall -Werror
// #cgo unix CFLAGS: -Wall -Werror
// #cgo linux CFLAGS: -D_GNU_SOURCE
// #cgo LDFLAGS: -lphp -lm -lutil
// #cgo unix LDFLAGS: -lphp -lm -lutil
// #cgo linux LDFLAGS: -ldl -lresolv
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -liconv -ldl
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256
// #cgo windows LDFLAGS: -lpthreadVC3
import "C"

29
cli.go Normal file
View File

@@ -0,0 +1,29 @@
package frankenphp
// #include "frankenphp.h"
import "C"
import "unsafe"
// ExecuteScriptCLI executes the PHP script passed as parameter.
// It returns the exit status code of the script.
func ExecuteScriptCLI(script string, args []string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
cScript := C.CString(script)
defer C.free(unsafe.Pointer(cScript))
argc, argv := convertArgs(args)
defer freeArgs(argv)
return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0])), false))
}
func ExecutePHPCode(phpCode string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
cCode := C.CString(phpCode)
defer C.free(unsafe.Pointer(cCode))
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
}

55
cli_test.go Normal file
View File

@@ -0,0 +1,55 @@
package frankenphp_test
import (
"errors"
"log"
"os"
"os/exec"
"testing"
"github.com/dunglas/frankenphp"
"github.com/stretchr/testify/assert"
)
func TestExecuteScriptCLI(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}
cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
stdoutStderr, err := cmd.CombinedOutput()
assert.Error(t, err)
var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 3, exitError.ExitCode())
}
stdoutStderrStr := string(stdoutStderr)
assert.Contains(t, stdoutStderrStr, `"foo"`)
assert.Contains(t, stdoutStderrStr, `"bar"`)
assert.Contains(t, stdoutStderrStr, "From the CLI")
}
func TestExecuteCLICode(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}
cmd := exec.Command("internal/testcli/testcli", "-r", "echo 'Hello World';")
stdoutStderr, err := cmd.CombinedOutput()
assert.NoError(t, err)
stdoutStderrStr := string(stdoutStderr)
assert.Equal(t, stdoutStderrStr, `Hello World`)
}
func ExampleExecuteScriptCLI() {
if len(os.Args) <= 1 {
log.Println("Usage: my-program script.php")
os.Exit(1)
}
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
}

2
ext.go
View File

@@ -1,6 +1,6 @@
package frankenphp
//#include "frankenphp.h"
// #include "frankenphp.h"
import "C"
import (
"sync"

View File

@@ -1,8 +1,8 @@
#include "frankenphp.h"
#include <SAPI.h>
#include <Zend/zend_alloc.h>
#include <Zend/zend_exceptions.h>
#include <Zend/zend_interfaces.h>
#include <Zend/zend_types.h>
#include <errno.h>
#include <ext/spl/spl_exceptions.h>
#include <ext/standard/head.h>
@@ -11,7 +11,11 @@
#endif
#include <inttypes.h>
#include <php.h>
#ifdef PHP_WIN32
#include <config.w32.h>
#else
#include <php_config.h>
#endif
#include <php_ini.h>
#include <php_main.h>
#include <php_output.h>
@@ -22,7 +26,9 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef ZEND_WIN32
#include <unistd.h>
#endif
#if defined(__linux__)
#include <sys/prctl.h>
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
@@ -279,7 +285,7 @@ bool frankenphp_shutdown_dummy_request(void) {
return true;
}
PHPAPI void get_full_env(zval *track_vars_array) {
void get_full_env(zval *track_vars_array) {
go_getfullenv(thread_index, track_vars_array);
}
@@ -1033,6 +1039,7 @@ static void *php_thread(void *arg) {
}
static void *php_main(void *arg) {
#ifndef ZEND_WIN32
/*
* SIGPIPE must be masked in non-Go threads:
* https://pkg.go.dev/os/signal#hdr-Go_programs_that_use_cgo_or_SWIG
@@ -1045,6 +1052,7 @@ static void *php_main(void *arg) {
perror("failed to block SIGPIPE");
exit(EXIT_FAILURE);
}
#endif
set_thread_name("php-main");
@@ -1340,7 +1348,7 @@ static zend_module_entry **modules = NULL;
static int modules_len = 0;
static int (*original_php_register_internal_extensions_func)(void) = NULL;
PHPAPI int register_internal_extensions(void) {
int register_internal_extensions(void) {
if (original_php_register_internal_extensions_func != NULL &&
original_php_register_internal_extensions_func() != SUCCESS) {
return FAILURE;

View File

@@ -14,10 +14,10 @@ package frankenphp
// #include <stdlib.h>
// #include <stdint.h>
// #include "frankenphp.h"
// #include <php_variables.h>
// #include <zend_llist.h>
// #include <SAPI.h>
// #include "frankenphp.h"
import "C"
import (
"bytes"
@@ -750,30 +750,6 @@ func go_is_context_done(threadIndex C.uintptr_t) C.bool {
return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)
}
// ExecuteScriptCLI executes the PHP script passed as parameter.
// It returns the exit status code of the script.
func ExecuteScriptCLI(script string, args []string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
cScript := C.CString(script)
defer C.free(unsafe.Pointer(cScript))
argc, argv := convertArgs(args)
defer freeArgs(argv)
return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0])), false))
}
func ExecutePHPCode(phpCode string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
cCode := C.CString(phpCode)
defer C.free(unsafe.Pointer(cCode))
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
}
func convertArgs(args []string) (C.int, []*C.char) {
argc := C.int(len(args))
argv := make([]*C.char, argc)

View File

@@ -1,6 +1,46 @@
#ifndef _FRANKENPHP_H
#define _FRANKENPHP_H
#ifdef _WIN32
// Define this to prevent windows.h from including legacy winsock.h
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
// Explicitly include Winsock2 BEFORE windows.h
#include <windows.h>
#include <winerror.h>
#include <winsock2.h>
#include <ws2tcpip.h>
// Fix for missing IntSafe functions (LongLongAdd) when building with Clang
#ifdef __clang__
#ifndef INTSAFE_E_ARITHMETIC_OVERFLOW
#define INTSAFE_E_ARITHMETIC_OVERFLOW ((HRESULT)0x80070216L)
#endif
#ifndef LongLongAdd
static inline HRESULT LongLongAdd(LONGLONG llAugend, LONGLONG llAddend,
LONGLONG *pllResult) {
if (__builtin_add_overflow(llAugend, llAddend, pllResult)) {
return INTSAFE_E_ARITHMETIC_OVERFLOW;
}
return S_OK;
}
#endif
#ifndef LongLongSub
static inline HRESULT LongLongSub(LONGLONG llMinuend, LONGLONG llSubtrahend,
LONGLONG *pllResult) {
if (__builtin_sub_overflow(llMinuend, llSubtrahend, pllResult)) {
return INTSAFE_E_ARITHMETIC_OVERFLOW;
}
return S_OK;
}
#endif
#endif
#endif
#include <Zend/zend_modules.h>
#include <Zend/zend_types.h>
#include <stdbool.h>

BIN
frankenphp.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View File

@@ -784,40 +784,6 @@ func testFileUpload(t *testing.T, opts *testOptions) {
}, opts)
}
func TestExecuteScriptCLI(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}
cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
stdoutStderr, err := cmd.CombinedOutput()
assert.Error(t, err)
var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 3, exitError.ExitCode())
}
stdoutStderrStr := string(stdoutStderr)
assert.Contains(t, stdoutStderrStr, `"foo"`)
assert.Contains(t, stdoutStderrStr, `"bar"`)
assert.Contains(t, stdoutStderrStr, "From the CLI")
}
func TestExecuteCLICode(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}
cmd := exec.Command("internal/testcli/testcli", "-r", "echo 'Hello World';")
stdoutStderr, err := cmd.CombinedOutput()
assert.NoError(t, err)
stdoutStderrStr := string(stdoutStderr)
assert.Equal(t, stdoutStderrStr, `Hello World`)
}
func ExampleServeHTTP() {
if err := frankenphp.Init(); err != nil {
panic(err)
@@ -837,15 +803,6 @@ func ExampleServeHTTP() {
log.Fatal(http.ListenAndServe(":8080", nil))
}
func ExampleExecuteScriptCLI() {
if len(os.Args) <= 1 {
log.Println("Usage: my-program script.php")
os.Exit(1)
}
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
}
func BenchmarkHelloWorld(b *testing.B) {
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)

View File

@@ -1,3 +1,5 @@
//go:build unix
package cpu
// #include <time.h>

View File

@@ -5,7 +5,7 @@ import (
)
// ProbeCPUs fallback that always determines that the CPU limits are not reached
func ProbeCPUs(probeTime time.Duration, maxCPUUsage float64, abort chan struct{}) bool {
func ProbeCPUs(probeTime time.Duration, _ float64, abort chan struct{}) bool {
select {
case <-abort:
return false

View File

@@ -3,6 +3,7 @@ package extgen
import (
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
@@ -68,6 +69,10 @@ func TestWriteFile(t *testing.T) {
assert.NoError(t, err, "Failed to stat file")
expectedMode := os.FileMode(0644)
if runtime.GOOS == "windows" {
expectedMode = os.FileMode(0666)
}
assert.Equal(t, expectedMode, info.Mode().Perm(), "writeFile() wrong permissions")
})
}

View File

@@ -9,5 +9,6 @@ import (
// FastAbs can't be optimized on Windows because the
// syscall.FullPath function takes an input.
func FastAbs(path string) (string, error) {
return filepath.Abs(path)
// Normalize forward slashes to backslashes for Windows compatibility
return filepath.Abs(filepath.FromSlash(path))
}

View File

@@ -1,6 +1,7 @@
#ifndef _EXTENSIONS_H
#define _EXTENSIONS_H
#include "../../frankenphp.h"
#include <php.h>
extern zend_module_entry module1_entry;

View File

@@ -1,3 +1,4 @@
#include "extension.h"
#include <php.h>
#include <zend_exceptions.h>

View File

@@ -1,13 +1,14 @@
package testext
// #cgo darwin pkg-config: libxml-2.0
// #cgo CFLAGS: -Wall -Werror
// #cgo CFLAGS: -I/usr/local/include -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
// #cgo unix CFLAGS: -Wall -Werror
// #cgo unix CFLAGS: -I/usr/local/include -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
// #cgo linux CFLAGS: -D_GNU_SOURCE
// #cgo darwin CFLAGS: -I/opt/homebrew/include
// #cgo LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lm -lutil
// #cgo unix LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lm -lutil
// #cgo linux LDFLAGS: -ldl -lresolv
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -L/opt/homebrew/lib -L/opt/homebrew/opt/libiconv/lib -liconv -ldl
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256
// #include "extension.h"
import "C"
import (

View File

@@ -11,6 +11,8 @@ import (
"github.com/e-dant/watcher/watcher-go"
)
const sep = string(filepath.Separator)
type pattern struct {
patternGroup *PatternGroup
value string
@@ -39,8 +41,11 @@ func (p *pattern) parse() (err error) {
p.value = absPattern
volumeName := filepath.VolumeName(p.value)
p.value = strings.TrimPrefix(p.value, volumeName)
// then we split the pattern to determine where the directory ends and the pattern starts
splitPattern := strings.Split(absPattern, string(filepath.Separator))
splitPattern := strings.Split(p.value, sep)
patternWithoutDir := ""
for i, part := range splitPattern {
isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".")
@@ -57,11 +62,15 @@ func (p *pattern) parse() (err error) {
// now we split the pattern according to the recursive '**' syntax
p.parsedValues = strings.Split(patternWithoutDir, "**")
for i, pp := range p.parsedValues {
p.parsedValues[i] = strings.Trim(pp, string(filepath.Separator))
p.parsedValues[i] = strings.Trim(pp, sep)
}
// remove the trailing separator and add leading separator
p.value = string(filepath.Separator) + strings.Trim(p.value, string(filepath.Separator))
// remove the trailing separator and add leading separator (except on Windows)
if volumeName == "" {
p.value = sep + strings.Trim(p.value, sep)
} else {
p.value = volumeName + sep + strings.Trim(p.value, sep)
}
// try to canonicalize the path
canonicalPattern, err := filepath.EvalSymlinks(p.value)
@@ -123,7 +132,7 @@ func (p *pattern) isValidPattern(fileName string) bool {
}
// remove the directory path and separator from the filename
fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, p.value), string(filepath.Separator))
fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, p.value), sep)
// if the pattern has size 1 we can match it directly against the filename
if len(p.parsedValues) == 1 {
@@ -134,12 +143,12 @@ func (p *pattern) isValidPattern(fileName string) bool {
}
func (p *pattern) matchPatterns(fileName string) bool {
partsToMatch := strings.Split(fileName, string(filepath.Separator))
partsToMatch := strings.Split(fileName, sep)
cursor := 0
// if there are multiple parsedValues due to '**' we need to match them individually
for i, pattern := range p.parsedValues {
patternSize := strings.Count(pattern, string(filepath.Separator)) + 1
patternSize := strings.Count(pattern, sep) + 1
// if we are at the last pattern we will start matching from the end of the filename
if i == len(p.parsedValues)-1 {
@@ -157,7 +166,7 @@ func (p *pattern) matchPatterns(fileName string) bool {
}
cursor = j
subPattern := strings.Join(partsToMatch[j:j+patternSize], string(filepath.Separator))
subPattern := strings.Join(partsToMatch[j:j+patternSize], sep)
if matchCurlyBracePattern(pattern, subPattern) {
cursor = j + patternSize - 1

View File

@@ -4,6 +4,7 @@ package watcher
import (
"path/filepath"
"strings"
"testing"
"github.com/e-dant/watcher/watcher-go"
@@ -11,16 +12,42 @@ import (
"github.com/stretchr/testify/require"
)
func normalizePath(t *testing.T, path string) string {
t.Helper()
if filepath.Separator == '/' {
return path
}
path = filepath.FromSlash(path)
if strings.HasPrefix(path, "\\") {
path = "C:\\" + path[1:]
}
return path
}
func newPattern(t *testing.T, value string) pattern {
t.Helper()
p := pattern{value: normalizePath(t, value)}
require.NoError(t, p.parse())
return p
}
func TestDisallowOnEventTypeBiggerThan3(t *testing.T) {
w := pattern{value: "/some/path"}
require.NoError(t, w.parse())
t.Parallel()
w := newPattern(t, "/some/path")
assert.False(t, w.allowReload(&watcher.Event{PathName: "/some/path/watch-me.php", EffectType: watcher.EffectTypeOwner}))
}
func TestDisallowOnPathTypeBiggerThan2(t *testing.T) {
w := pattern{value: "/some/path"}
require.NoError(t, w.parse())
t.Parallel()
w := newPattern(t, "/some/path")
assert.False(t, w.allowReload(&watcher.Event{PathName: "/some/path/watch-me.php", PathType: watcher.PathTypeSymLink}))
}
@@ -59,7 +86,7 @@ func TestValidRecursiveDirectories(t *testing.T) {
data := []struct {
pattern string
dir string
file string
}{
{"/path", "/path/file.php"},
{"/path", "/path/subpath/file.php"},
@@ -77,7 +104,7 @@ func TestValidRecursiveDirectories(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.file)
})
}
}
@@ -98,7 +125,7 @@ func TestInvalidRecursiveDirectories(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
@@ -122,7 +149,7 @@ func TestValidNonRecursiveFilePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.dir)
})
}
}
@@ -145,7 +172,7 @@ func TestInValidNonRecursiveFilePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
@@ -170,7 +197,7 @@ func TestValidRecursiveFilePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.dir)
})
}
}
@@ -198,7 +225,7 @@ func TestInvalidRecursiveFilePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
@@ -225,13 +252,14 @@ func TestValidDirectoryPatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.dir)
})
}
}
func TestInvalidDirectoryPatterns(t *testing.T) {
t.Parallel()
data := []struct {
pattern string
dir string
@@ -254,15 +282,17 @@ func TestInvalidDirectoryPatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
func TestValidCurlyBracePatterns(t *testing.T) {
t.Parallel()
data := []struct {
pattern string
dir string
file string
}{
{"/path/*.{php}", "/path/file.php"},
{"/path/*.{php,twig}", "/path/file.php"},
@@ -282,12 +312,14 @@ func TestValidCurlyBracePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldMatch(t, d.pattern, d.dir)
assertPatternMatch(t, d.pattern, d.file)
})
}
}
func TestInvalidCurlyBracePatterns(t *testing.T) {
t.Parallel()
data := []struct {
pattern string
dir string
@@ -306,52 +338,52 @@ func TestInvalidCurlyBracePatterns(t *testing.T) {
t.Run(d.pattern, func(t *testing.T) {
t.Parallel()
shouldNotMatch(t, d.pattern, d.dir)
assertPatternNotMatch(t, d.pattern, d.dir)
})
}
}
func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
w := pattern{value: "/**/*.php"}
require.NoError(t, w.parse())
t.Parallel()
w := newPattern(t, "/**/*.php")
w.events = make(chan eventHolder)
e := &watcher.Event{PathName: "/path/temporary_file", AssociatedPathName: "/path/file.php"}
e := &watcher.Event{PathName: normalizePath(t, "/path/temporary_file"), AssociatedPathName: normalizePath(t, "/path/file.php")}
go w.handle(e)
assert.Equal(t, e, (<-w.events).event)
}
func relativeDir(t *testing.T, relativePath string) string {
t.Helper()
dir, err := filepath.Abs("./" + relativePath)
assert.NoError(t, err)
return dir
}
func hasDir(t *testing.T, p string, dir string) {
t.Helper()
w := pattern{value: p}
require.NoError(t, w.parse())
w := newPattern(t, p)
assert.Equal(t, dir, w.value)
assert.Equal(t, normalizePath(t, dir), w.value)
}
func shouldMatch(t *testing.T, p string, fileName string) {
func assertPatternMatch(t *testing.T, p, fileName string) {
t.Helper()
w := pattern{value: p}
require.NoError(t, w.parse())
w := newPattern(t, p)
assert.True(t, w.allowReload(&watcher.Event{PathName: fileName}))
assert.True(t, w.allowReload(&watcher.Event{PathName: normalizePath(t, fileName)}))
}
func shouldNotMatch(t *testing.T, p string, fileName string) {
func assertPatternNotMatch(t *testing.T, p, fileName string) {
t.Helper()
w := pattern{value: p}
require.NoError(t, w.parse())
w := newPattern(t, p)
assert.False(t, w.allowReload(&watcher.Event{PathName: fileName}))
assert.False(t, w.allowReload(&watcher.Event{PathName: normalizePath(t, fileName)}))
}

View File

@@ -3,6 +3,7 @@
package frankenphp
// #include <stdint.h>
// #include "frankenphp.h"
// #include <php.h>
import "C"
import (

View File

@@ -4,8 +4,8 @@ package frankenphp
// #cgo nocallback frankenphp_init_persistent_string
// #cgo noescape frankenphp_new_main_thread
// #cgo noescape frankenphp_init_persistent_string
// #include <php_variables.h>
// #include "frankenphp.h"
// #include <php_variables.h>
import "C"
import (
"log/slog"

View File

@@ -101,9 +101,9 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
numThreads := 10
numRequestsPerThread := 100
worker1Path := testDataPath + "/transition-worker-1.php"
worker1Path := filepath.Join(testDataPath, "transition-worker-1.php")
worker1Name := "worker-1"
worker2Path := testDataPath + "/transition-worker-2.php"
worker2Path := filepath.Join(testDataPath, "transition-worker-2.php")
worker2Name := "worker-2"
assert.NoError(t, Init(

View File

@@ -1,8 +1,5 @@
package frankenphp
//#include "frankenphp.h"
//#include <sys/resource.h>
import "C"
import (
"errors"
"log/slog"

View File

@@ -1,6 +1,7 @@
package frankenphp
import (
"path/filepath"
"testing"
"time"
@@ -33,7 +34,7 @@ func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
t.Cleanup(Shutdown)
workerName := "worker1"
workerPath := testDataPath + "/transition-worker-1.php"
workerPath := filepath.Join(testDataPath, "transition-worker-1.php")
assert.NoError(t, Init(
WithNumThreads(2),
WithMaxThreads(3),

View File

@@ -3,6 +3,6 @@
require_once __DIR__.'/_executor.php';
return function () {
echo "update 1: " . mercure_publish('foo', 'bar', true, 'myid', 'mytype', 10) . PHP_EOL;
echo "update 2: " . mercure_publish(['baz', 'bar']) . PHP_EOL;
echo "update 1: " . mercure_publish('foo', 'bar', true, 'myid', 'mytype', 10) . "\n";
echo "update 2: " . mercure_publish(['baz', 'bar']) . "\n";
};

View File

@@ -11,5 +11,5 @@ return function () {
$_SESSION['count'] = 0;
}
echo 'Count: '.$_SESSION['count'].PHP_EOL;
echo 'Count: '.$_SESSION['count']."\n";
};

View File

@@ -14,9 +14,9 @@ Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) {
return NULL;
}
void *__emalloc__(size_t size) { return emalloc(size); }
void *__emalloc__(size_t size) { return malloc(size); }
void __efree__(void *ptr) { efree(ptr); }
void __efree__(void *ptr) { free(ptr); }
void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,
bool persistent) {

View File

@@ -15,6 +15,8 @@ package frankenphp
#cgo noescape __zval_double__
#cgo noescape __zval_string__
#cgo noescape __zval_arr__
#cgo noescape __emalloc__
#cgo noescape __efree__
#include "types.h"
*/
import "C"

View File

@@ -1,11 +1,12 @@
#ifndef TYPES_H
#define TYPES_H
#include "frankenphp.h"
#include <Zend/zend.h>
#include <Zend/zend_API.h>
#include <Zend/zend_alloc.h>
#include <Zend/zend_hash.h>
#include <Zend/zend_types.h>
#include <stdlib.h>
zval *get_ht_packed_data(HashTable *, uint32_t index);
Bucket *get_ht_bucket_data(HashTable *, uint32_t index);

3
vcpkg.json Normal file
View File

@@ -0,0 +1,3 @@
{
"dependencies": ["brotli", "pthreads"]
}

View File

@@ -50,7 +50,7 @@ func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Re
// now we spam file updates and check if the request counter resets
for range limit {
updateTestFile("./testdata/files/test.txt", "updated", t)
updateTestFile(t, filepath.Join(".", "testdata", "files", "test.txt"), "updated")
time.Sleep(pollingTime * time.Millisecond)
body, _ := testGet("http://example.com/worker-with-counter.php", handler, t)
if body == "requests:1" {
@@ -61,15 +61,10 @@ func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Re
return false
}
func updateTestFile(fileName string, content string, t *testing.T) {
func updateTestFile(t *testing.T, fileName, content string) {
absFileName, err := filepath.Abs(fileName)
require.NoError(t, err)
dirName := filepath.Dir(absFileName)
if _, err = os.Stat(dirName); os.IsNotExist(err) {
err = os.MkdirAll(dirName, 0700)
}
require.NoError(t, err)
require.NoError(t, os.MkdirAll(filepath.Dir(absFileName), 0700))
require.NoError(t, os.WriteFile(absFileName, []byte(content), 0644))
}

View File

@@ -102,7 +102,7 @@ func newWorker(o workerOpt) (*worker, error) {
// Order is important!
// This order ensures that FrankenPHP started from inside a symlinked directory will properly resolve any paths.
// If it is started from outside a symlinked directory, it is resolved to the same path that we use in the Caddy module.
absFileName, err := filepath.EvalSymlinks(o.fileName)
absFileName, err := filepath.EvalSymlinks(filepath.FromSlash(o.fileName))
if err != nil {
return nil, fmt.Errorf("worker filename is invalid %q: %w", o.fileName, err)
}