diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ef86d55 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,180 @@ +name: Release + +on: + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g., v0.1.0)' + required: true + type: string + +permissions: + contents: write + +jobs: + # --------------------------------------------------------------------------- + # Linux .so builds + # --------------------------------------------------------------------------- + build-linux: + name: Linux - PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mysqlnd + tools: phpize + ini-values: error_reporting=E_ALL + + - name: Build extension + working-directory: ext/mariadb_profiler + run: | + phpize + ./configure --enable-mariadb_profiler + make -j$(nproc) + + - name: Verify extension is loadable + working-directory: ext/mariadb_profiler + run: | + php -d "extension=$(pwd)/modules/mariadb_profiler.so" -m | grep mariadb_profiler + + - name: Package artifact + run: | + mkdir -p dist + cp ext/mariadb_profiler/modules/mariadb_profiler.so \ + "dist/mariadb_profiler-php${{ matrix.php }}-linux-x86_64.so" + + - uses: actions/upload-artifact@v4 + with: + name: mariadb_profiler-php${{ matrix.php }}-linux-x86_64 + path: dist/*.so + + # --------------------------------------------------------------------------- + # Windows .dll builds + # --------------------------------------------------------------------------- + build-windows: + name: Windows - PHP ${{ matrix.php }} ${{ matrix.ts }} ${{ matrix.arch }} + runs-on: windows-${{ matrix.os }} + defaults: + run: + shell: cmd + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + arch: [x64] + ts: [nts, ts] + os: ['2022'] + include: + # PHP 7.4-8.3 use VS16, PHP 8.4 uses VS17. + # windows-2022 runners have both toolsets. + - php: '7.4' + vs: vs16 + - php: '8.0' + vs: vs16 + - php: '8.1' + vs: vs16 + - php: '8.2' + vs: vs16 + - php: '8.3' + vs: vs16 + - php: '8.4' + vs: vs17 + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP SDK + id: setup + uses: php/setup-php-sdk@v0.12 + with: + version: ${{ matrix.php }} + arch: ${{ matrix.arch }} + ts: ${{ matrix.ts }} + + - name: Enable MSVC Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ matrix.arch }} + toolset: ${{ steps.setup.outputs.toolset }} + + - name: phpize + working-directory: ext\mariadb_profiler + run: phpize + + - name: configure + working-directory: ext\mariadb_profiler + run: configure --enable-mariadb_profiler --with-prefix=%INSTALL_DIR% + + - name: nmake + working-directory: ext\mariadb_profiler + run: nmake + + - name: Package artifact + shell: pwsh + run: | + $ts = "${{ matrix.ts }}" + $arch = "${{ matrix.arch }}" + $phpVer = "${{ matrix.php }}" + + # Find the built DLL + $dll = Get-ChildItem -Path ext\mariadb_profiler -Recurse -Filter "php_mariadb_profiler.dll" | + Select-Object -First 1 + if (-not $dll) { + Write-Error "php_mariadb_profiler.dll not found" + exit 1 + } + Write-Host "Found DLL: $($dll.FullName)" + + New-Item -ItemType Directory -Force -Path dist | Out-Null + $name = "php_mariadb_profiler-php${phpVer}-${ts}-${arch}" + Copy-Item $dll.FullName "dist\${name}.dll" + Write-Host "Packaged as: dist\${name}.dll" + + - uses: actions/upload-artifact@v4 + with: + name: php_mariadb_profiler-php${{ matrix.php }}-${{ matrix.ts }}-${{ matrix.arch }} + path: dist\*.dll + + # --------------------------------------------------------------------------- + # Create GitHub Release + # --------------------------------------------------------------------------- + release: + name: Create Release + needs: [build-linux, build-windows] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Collect release assets + run: | + mkdir -p release + find artifacts -type f \( -name "*.so" -o -name "*.dll" \) -exec cp {} release/ \; + echo "=== Release assets ===" + ls -lh release/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.tag }} + name: ${{ github.event.inputs.tag }} + draft: false + prerelease: false + generate_release_notes: true + files: release/* diff --git a/ext/mariadb_profiler/config.w32 b/ext/mariadb_profiler/config.w32 new file mode 100644 index 0000000..e1449b5 --- /dev/null +++ b/ext/mariadb_profiler/config.w32 @@ -0,0 +1,11 @@ +// config.w32 for extension mariadb_profiler + +ARG_ENABLE('mariadb_profiler', 'MariaDB Query Profiler support', 'no'); + +if (PHP_MARIADB_PROFILER != 'no') { + EXTENSION('mariadb_profiler', + 'mariadb_profiler.c profiler_mysqlnd_plugin.c profiler_job.c profiler_log.c profiler_tag.c profiler_trace.c', + PHP_MARIADB_PROFILER_SHARED, + '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1'); + ADD_EXTENSION_DEP('mariadb_profiler', 'mysqlnd', true); +} diff --git a/ext/mariadb_profiler/mariadb_profiler.c b/ext/mariadb_profiler/mariadb_profiler.c index 476f561..f173742 100644 --- a/ext/mariadb_profiler/mariadb_profiler.c +++ b/ext/mariadb_profiler/mariadb_profiler.c @@ -91,8 +91,8 @@ static int profiler_ensure_log_dir(TSRMLS_D) return FAILURE; } - /* Try to create directory (mode 0777, umask will apply) */ - if (mkdir(dir, 0777) == 0) { + /* Try to create directory (mode 0777, umask will apply on Unix) */ + if (PROFILER_MKDIR(dir, 0777) == 0) { return SUCCESS; } @@ -101,30 +101,67 @@ static int profiler_ensure_log_dir(TSRMLS_D) char tmp[4096]; char *p; size_t len; + char sep; len = snprintf(tmp, sizeof(tmp), "%s", dir); if (len >= sizeof(tmp)) { return FAILURE; } - /* Remove trailing slash */ - if (tmp[len - 1] == '/') { +#ifdef PHP_WIN32 + sep = '\\'; + /* Normalise forward slashes to backslashes on Windows */ + { + char *s; + for (s = tmp; *s; s++) { + if (*s == '/') *s = '\\'; + } + } +#else + sep = '/'; +#endif + + /* Remove trailing separator */ + if (tmp[len - 1] == sep) { tmp[len - 1] = '\0'; } - for (p = tmp + 1; *p; p++) { - if (*p == '/') { +#ifdef PHP_WIN32 + /* Skip drive prefix (e.g. "C:\") or UNC prefix (e.g. "\\server\share\") */ + p = tmp; + if (((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) + && p[1] == ':' && p[2] == sep) { + /* Drive letter path: start after "C:\" */ + p += 3; + } else if (p[0] == sep && p[1] == sep) { + /* UNC path: skip past "\\server\share\" */ + p += 2; + /* Skip server name */ + while (*p && *p != sep) p++; + if (*p == sep) p++; + /* Skip share name */ + while (*p && *p != sep) p++; + if (*p == sep) p++; + } else { + p++; + } +#else + p = tmp + 1; +#endif + + for (; *p; p++) { + if (*p == sep) { *p = '\0'; if (stat(tmp, &st) != 0) { - if (mkdir(tmp, 0777) != 0 && errno != EEXIST) { + if (PROFILER_MKDIR(tmp, 0777) != 0 && errno != EEXIST) { return FAILURE; } } - *p = '/'; + *p = sep; } } - if (mkdir(tmp, 0777) != 0 && errno != EEXIST) { + if (PROFILER_MKDIR(tmp, 0777) != 0 && errno != EEXIST) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "mariadb_profiler: failed to create log_dir '%s': %s", dir, strerror(errno)); diff --git a/ext/mariadb_profiler/php_mariadb_profiler_compat.h b/ext/mariadb_profiler/php_mariadb_profiler_compat.h index cbcbab8..35aa96d 100644 --- a/ext/mariadb_profiler/php_mariadb_profiler_compat.h +++ b/ext/mariadb_profiler/php_mariadb_profiler_compat.h @@ -191,4 +191,109 @@ typedef long zend_long; # define PROFILER_BOOL_T zend_bool #endif +/* + * ---- Platform I/O compatibility (Windows) ---- + * + * The extension uses POSIX APIs (flock, gettimeofday, localtime_r, open/read/close) + * that are not available on Windows. These macros and inline functions provide + * equivalent functionality using Win32 APIs. + * + * On Windows, php.h already includes and provides gettimeofday() + * via win32/time.h. We only need to handle the remaining POSIX-specific APIs. + */ +#ifdef PHP_WIN32 +# include +# include + +/* ssize_t is not defined in MSVC */ +typedef intptr_t profiler_ssize_t; + +/* POSIX I/O function mapping */ +# define profiler_open _open +# define profiler_read _read +# define profiler_close _close + +/* S_ISDIR may not be defined on all MSVC versions */ +# ifndef S_ISDIR +# define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +# endif + +/* mkdir: Windows _mkdir() takes only the path (no mode parameter) */ +# define PROFILER_MKDIR(path, mode) _mkdir(path) + +/* localtime_r: Windows has localtime_s with swapped argument order */ +static inline struct tm *profiler_localtime_r(const time_t *timep, struct tm *result) +{ + return (localtime_s(result, timep) == 0) ? result : NULL; +} +# define localtime_r profiler_localtime_r + +/* File locking: Windows uses LockFileEx/UnlockFileEx instead of flock() */ +# ifndef LOCK_SH +# define LOCK_SH 1 +# endif +# ifndef LOCK_EX +# define LOCK_EX 2 +# endif +# ifndef LOCK_NB +# define LOCK_NB 4 +# endif +# ifndef LOCK_UN +# define LOCK_UN 8 +# endif + +static inline int profiler_flock(int fd, int operation) +{ + HANDLE h = (HANDLE)_get_osfhandle(fd); + DWORD flags = 0; + OVERLAPPED ov = {0}; + + if (h == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } + + if (operation & LOCK_UN) { + if (UnlockFileEx(h, 0, MAXDWORD, MAXDWORD, &ov)) { + return 0; + } + _dosmaperr(GetLastError()); + return -1; + } + + if (operation & LOCK_EX) { + flags |= LOCKFILE_EXCLUSIVE_LOCK; + } + if (operation & LOCK_NB) { + flags |= LOCKFILE_FAIL_IMMEDIATELY; + } + if (LockFileEx(h, flags, 0, MAXDWORD, MAXDWORD, &ov)) { + return 0; + } + { + DWORD lasterr = GetLastError(); + if ((operation & LOCK_NB) && lasterr == ERROR_LOCK_VIOLATION) { + errno = EWOULDBLOCK; + } else { + _dosmaperr(lasterr); + } + } + return -1; +} +# define flock profiler_flock + +#else +/* Unix/Linux/macOS */ + +# include +typedef ssize_t profiler_ssize_t; + +# define profiler_open open +# define profiler_read read +# define profiler_close close + +# define PROFILER_MKDIR(path, mode) mkdir(path, mode) + +#endif /* PHP_WIN32 */ + #endif /* PHP_MARIADB_PROFILER_COMPAT_H */ diff --git a/ext/mariadb_profiler/profiler_job.c b/ext/mariadb_profiler/profiler_job.c index 4704792..c2cface 100644 --- a/ext/mariadb_profiler/profiler_job.c +++ b/ext/mariadb_profiler/profiler_job.c @@ -14,7 +14,9 @@ #include "php_mariadb_profiler.h" #include "profiler_job.h" -#include +#ifndef PHP_WIN32 +# include +#endif #include #include #include @@ -144,7 +146,7 @@ int profiler_job_refresh_active_jobs(void) int fd; struct stat st; char *buf = NULL; - ssize_t bytes_read; + profiler_ssize_t bytes_read; TSRMLS_FETCH(); /* Free previous state */ @@ -152,7 +154,7 @@ int profiler_job_refresh_active_jobs(void) jobs_path = profiler_job_get_jobs_path(); - fd = open(jobs_path, O_RDONLY); + fd = profiler_open(jobs_path, O_RDONLY); efree(jobs_path); if (fd < 0) { @@ -163,23 +165,23 @@ int profiler_job_refresh_active_jobs(void) /* Shared lock for reading */ if (flock(fd, LOCK_SH) != 0) { - close(fd); + profiler_close(fd); return FAILURE; } if (fstat(fd, &st) != 0 || st.st_size == 0) { flock(fd, LOCK_UN); - close(fd); + profiler_close(fd); PROFILER_G(last_job_check) = time(NULL); return SUCCESS; } buf = (char *)emalloc(st.st_size + 1); - bytes_read = read(fd, buf, st.st_size); + bytes_read = (profiler_ssize_t)profiler_read(fd, buf, st.st_size); buf[bytes_read > 0 ? bytes_read : 0] = '\0'; flock(fd, LOCK_UN); - close(fd); + profiler_close(fd); /* Parse the JSON to get active job keys */ profiler_job_parse_active_jobs(buf, &PROFILER_G(active_jobs), &PROFILER_G(active_job_count)); diff --git a/ext/mariadb_profiler/profiler_log.c b/ext/mariadb_profiler/profiler_log.c index bac853b..8f317d5 100644 --- a/ext/mariadb_profiler/profiler_log.c +++ b/ext/mariadb_profiler/profiler_log.c @@ -18,8 +18,10 @@ #include "profiler_tag.h" #include "profiler_trace.h" -#include -#include +#ifndef PHP_WIN32 +# include +# include +#endif #include /* {{{ profiler_log_escape_json_string