From f2b2ca9e3f80303fe9b368f9c9937bec4aa0d94b Mon Sep 17 00:00:00 2001 From: macintoshplus <814683+macintoshplus@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:08:01 +0100 Subject: [PATCH 1/3] add config PIE and GitHub Action --- .github/FUNDING.yml | 13 ++++++++++ .github/workflows/ci.yml | 27 ++++++++++++++++++++ .github/workflows/windows.yml | 48 +++++++++++++++++++++++++++++++++++ .gitignore | 25 ++++++++++++++++++ composer.json | 17 +++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/windows.yml create mode 100644 .gitignore create mode 100644 composer.json diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ce72f0e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [macintoshplus] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +# patreon: # Replace with a single Patreon username +# open_collective: # Replace with a single Open Collective username +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +# liberapay: # Replace with a single Liberapay username +# issuehunt: # Replace with a single IssueHunt username +# otechie: # Replace with a single Otechie username +# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9607337 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI +on: + push: + branches: ['*'] +jobs: + test-linux: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php-rel: ['8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + ts-state: [ts, nts] + + steps: + - uses: actions/checkout@v5 + - name: Setup PHP + id: setup-php + uses: shivammathur/setup-php@v2 + with: + php-version: '${{ matrix.php-rel }}' + env: + phpts: '${{ matrix.ts-state }}' + + - name: build extension + run: phpize && ./configure --with-expect && make + - name: run tests + run: make test diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..56b2c80 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,48 @@ +name: Publish Windows Releases +on: + release: + types: [created] + push: + branches: ['*'] + +permissions: + contents: write + +jobs: + get-extension-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.extension-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Get the extension matrix + id: extension-matrix + uses: php/php-windows-builder/extension-matrix@v1 + build: + needs: get-extension-matrix + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + fail-fast: true + matrix: ${{fromJson(needs.get-extension-matrix.outputs.matrix)}} + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Build the extension + uses: php/php-windows-builder/extension@v1 + with: + php-version: ${{ matrix.php-version }} + arch: ${{ matrix.arch }} + ts: ${{ matrix.ts }} + args: '--with-expect' + release: + runs-on: ubuntu-latest + needs: build + if: ${{ github.event_name == 'release' }} + steps: + - name: Upload artifact to the release + uses: php/php-windows-builder/release@v1 + with: + release: ${{ github.event.release.tag_name }} +# token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33dcfda --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +/config.h +/config.h.in +/config.log +/config.nice +/config.status +/configure +/configure.ac +/expect.la +/expect.lo +/expect_fopen_wrapper.lo +/libtool +/Makefile +/Makefile.fragments +/Makefile.objects +/run-tests.php +/tests/*.diff +/tests/*.exp +/tests/*.log +/tests/*.php +/tests/*.out +/tests/*.sh +/.libs/ +/autom4te.cache/ +/build/ +/modules/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6c25f11 --- /dev/null +++ b/composer.json @@ -0,0 +1,17 @@ +{ + "name": "php-win-ext/expect", + "type": "php-ext", + "license": [ + "PHP-3.01" + ], + "authors": [ + { + "name": "Michael Spector", + "email": "michael@php.net" + } + ], + "require": { + "php": ">= 8.0.0" + }, + "description": "This extension allows to interact with processes through PTY, using expect library." +} \ No newline at end of file From 193125861226125b4a5efc05bf5e10acd1d60a0a Mon Sep 17 00:00:00 2001 From: macintoshplus <814683+macintoshplus@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:09:44 +0100 Subject: [PATCH 2/3] :bug: apply patch for PHP 8 Thanks @arekm :memo: add readme --- .github/windows.yml | 130 ++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 10 ++- .github/workflows/windows.yml | 48 ------------- README.md | 71 +++++++++++++++++++ composer.json | 4 ++ expect.c | 73 +++++++++++++++---- expect_fopen_wrapper.c | 10 ++- php_expect.h | 1 + 8 files changed, 285 insertions(+), 62 deletions(-) create mode 100644 .github/windows.yml delete mode 100644 .github/workflows/windows.yml create mode 100644 README.md diff --git a/.github/windows.yml b/.github/windows.yml new file mode 100644 index 0000000..5d859e0 --- /dev/null +++ b/.github/windows.yml @@ -0,0 +1,130 @@ +name: Publish Windows Releases +on: + workflow_dispatch: ~ + release: + types: [created] + push: + branches: ['*'] + +permissions: + contents: write + +jobs: + build-lib: + runs-on: windows-2022 + strategy: + fail-fast: true + matrix: +# arch: [x64, x86] + arch: [x64] + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Enable Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ matrix.arch }} + - name: Build the library + run: | + mkdir tcl + cd tcl + Invoke-WebRequest -Uri https://github.com/tcltk/tcl/releases/download/core-9-0-3/tcl9.0.3-src.tar.gz -outFile tcl.tar.gz + 7z x tcl.tar.gz -so | 7z x -aoa -si -ttar -o"." + cd tcl9.0.3\win + mkdir install + nmake -f Makefile.vc all INSTALLDIR=.\install + nmake -f Makefile.vc install INSTALLDIR=.\install + 7z a tcl.zip .\install + - name: Upload TCL library + uses: actions/upload-artifact@v5 + with: + name: tcl-${{ matrix.arch }} + path: tcl\tcl9.0.3\win\tcl.zip + if-no-files-found: error + + get-extension-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.extension-matrix.outputs.matrix }} + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Get the extension matrix + id: extension-matrix + uses: php/php-windows-builder/extension-matrix@v1 + with: + arch-list: x64 + php-version-list: "8.0" + ts-list: nts + + build: + needs: [get-extension-matrix, build-lib] + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + fail-fast: true + matrix: ${{fromJson(needs.get-extension-matrix.outputs.matrix)}} + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: prepare + run: | + mkdir ..\tcl + - name: Download native lib + uses: actions/download-artifact@v5 + with: + name: tcl-${{ matrix.arch }} + + - name: Extract native lib + run: | + dir + 7z x tcl.zip -o"..\tcl" + dir .. + dir ..\tcl + dir ..\tcl\install\include + dir ..\tcl\install\lib + + - name: Setup PHP SDK + id: setup-php-windows + uses: php/setup-php-sdk@v0.12 + with: + version: ${{ matrix.php-version }} + arch: ${{ matrix.arch }} + ts: ${{ matrix.ts }} + + - name: Enable Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ matrix.arch }} + toolset: ${{ steps.setup-php-windows.outputs.toolset }} + + - run: phpize + - run: ./configure --with-expect --with-extra-includes="$($Env:GITHUB_WORKSPACE)..\tcl\install\include" --with-extra-libs="$($Env:GITHUB_WORKSPACE)..\tcl\install\lib" --with-prefix=${{steps.setup-php-windows.outputs.prefix}} + - run: nmake + - run: nmake test TESTS=tests + + +# - name: Upload TCL library +# uses: actions/upload-artifact@v5 +# with: +# name: tcl-${{ matrix.php }}-${{ matrix.arch }} +# path: | +# win\install\ + +# - name: Build the extension +# uses: php/php-windows-builder/extension@v1 +# with: +# php-version: ${{ matrix.php-version }} +# arch: ${{ matrix.arch }} +# ts: ${{ matrix.ts }} +# args: '--with-expect' + release: + runs-on: ubuntu-latest + needs: build + if: ${{ github.event_name == 'release' }} + steps: + - name: Upload artifact to the release + uses: php/php-windows-builder/release@v1 + with: + release: ${{ github.event.release.tag_name }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9607337..e8fa5a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,13 @@ jobs: fail-fast: true matrix: php-rel: ['8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] +# php-rel: ['8.0'] ts-state: [ts, nts] steps: - uses: actions/checkout@v5 + - name: Install dependencies + run: sudo apt-get install -y tcl-dev tcl-expect-dev - name: Setup PHP id: setup-php uses: shivammathur/setup-php@v2 @@ -22,6 +25,11 @@ jobs: phpts: '${{ matrix.ts-state }}' - name: build extension - run: phpize && ./configure --with-expect && make + env: + CPPFLAGS: -I/usr/include/tcl + run: | + phpize + ./configure --with-expect + make - name: run tests run: make test diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index 56b2c80..0000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Publish Windows Releases -on: - release: - types: [created] - push: - branches: ['*'] - -permissions: - contents: write - -jobs: - get-extension-matrix: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.extension-matrix.outputs.matrix }} - steps: - - name: Checkout - uses: actions/checkout@v5 - - name: Get the extension matrix - id: extension-matrix - uses: php/php-windows-builder/extension-matrix@v1 - build: - needs: get-extension-matrix - runs-on: ${{ matrix.os }} - continue-on-error: false - strategy: - fail-fast: true - matrix: ${{fromJson(needs.get-extension-matrix.outputs.matrix)}} - steps: - - name: Checkout - uses: actions/checkout@v5 - - name: Build the extension - uses: php/php-windows-builder/extension@v1 - with: - php-version: ${{ matrix.php-version }} - arch: ${{ matrix.arch }} - ts: ${{ matrix.ts }} - args: '--with-expect' - release: - runs-on: ubuntu-latest - needs: build - if: ${{ github.event_name == 'release' }} - steps: - - name: Upload artifact to the release - uses: php/php-windows-builder/release@v1 - with: - release: ${{ github.event.release.tag_name }} -# token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d27570 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +PHP extension for expect library +================================ + +This extension allows to interact with processes through PTY, using expect library. + +This extension uses a Tcl/Expect library: https://www.tcl-lang.org/. + +Original author: Michael Spector + +> **Unable to build this extension for Windows because the TCL Expect is not available on Windows.** + + +Maintained branches: + +| Version | Status | +|---------|------------------------------| +| master | unmaintened :x: | +| v0.x | maintened :white_check_mark: | + +Maintained PHP Versions compatibility: + +| PHP Version | Status | +|-------------|------------------------| +| 5.x | no :x: | +| 7.x | no :x: | +| 8.0 | yes :white_check_mark: | +| 8.1 | yes :white_check_mark: | +| 8.2 | yes :white_check_mark: | +| 8.3 | yes :white_check_mark: | +| 8.4 | yes :white_check_mark: | +| 8.5 | yes :white_check_mark: | + +Installation system support: + +| Platform | Status | +|----------|------------------------| +| PECL | no :x: | +| PIE | yes :white_check_mark: | + + +To install the extension, install the library `tcl-dev tcl-expect-dev` first. + +Debian/Ubuntu/Mint: + +```shell +sudo apt-get install tcl-dev tcl-expect-dev +``` + +Alpine Linux: + +```shell +apk add tcl-dev expect-dev +``` + +Fedora: + +```shell +sudo dnf install tcl-devel expect-devel +``` + +Arch Linux: + +```shell +sudo pacman -S tcl expect +``` + +And use PIE (PHP Installer Extension) with a command like: + +```bash +pie install php-win-ext/expect +``` diff --git a/composer.json b/composer.json index 6c25f11..f7782b8 100644 --- a/composer.json +++ b/composer.json @@ -13,5 +13,9 @@ "require": { "php": ">= 8.0.0" }, + "php-ext": { + "extension-name": "expect", + "os-families-exclude": ["windows"] + }, "description": "This extension allows to interact with processes through PTY, using expect library." } \ No newline at end of file diff --git a/expect.c b/expect.c index 4e9b0f1..8cac3eb 100644 --- a/expect.c +++ b/expect.c @@ -53,7 +53,7 @@ zend_module_entry expect_module_entry = { expect_functions, PHP_MINIT(expect), PHP_MSHUTDOWN(expect), - NULL, + PHP_RINIT(expect), NULL, PHP_MINFO(expect), PHP_EXPECT_VERSION, @@ -151,8 +151,18 @@ static PHP_INI_MH(OnSetExpectLogUser) * */ static PHP_INI_MH(OnSetExpectLogFile) { + /* PHP streams cannot be opened during module startup because the resource + * list (EG(regular_list)) is not yet initialized. The logfile will be + * opened in RINIT once the request context is available. */ + if (stage == PHP_INI_STAGE_STARTUP || stage == PHP_INI_STAGE_SHUTDOWN) { + return SUCCESS; + } + if (EXPECT_G(logfile_stream)) { php_stream_close(EXPECT_G(logfile_stream)); + EXPECT_G(logfile_stream) = NULL; + exp_logfile = NULL; + exp_logfile_all = 0; } #if PHP_MAJOR_VERSION >= 7 if (ZSTR_LEN(new_value) > 0) { @@ -232,6 +242,28 @@ PHP_MSHUTDOWN_FUNCTION(expect) /* }}} */ +/* {{{ PHP_RINIT_FUNCTION + * Apply ini settings that require PHP streams, skipped during MINIT. */ +PHP_RINIT_FUNCTION(expect) +{ + if (!EXPECT_G(logfile_stream)) { + char *logfile = zend_ini_string("expect.logfile", sizeof("expect.logfile") - 1, 0); + if (logfile && strlen(logfile) > 0) { + php_stream *stream = php_stream_open_wrapper(logfile, "a", 0, NULL); + if (stream) { + stream->flags |= PHP_STREAM_FLAG_NO_SEEK; + if (php_stream_cast(stream, PHP_STREAM_AS_STDIO, (void **) &exp_logfile, REPORT_ERRORS) == SUCCESS) { + EXPECT_G(logfile_stream) = stream; + exp_logfile_all = 1; + } + } + } + } + return SUCCESS; +} +/* }}} */ + + /* {{{ PHP_MINFO_FUNCTION */ PHP_MINFO_FUNCTION(expect) { @@ -287,6 +319,10 @@ PHP_FUNCTION(expect_popen) } stream->flags |= PHP_STREAM_FLAG_NO_SEEK; + /* PTY reads may return EIO when the child exits; suppress notices (PHP 7.4+ d59aac58b3e7). */ +#ifdef PHP_STREAM_FLAG_SUPPRESS_ERRORS + stream->flags |= PHP_STREAM_FLAG_SUPPRESS_ERRORS; +#endif #if PHP_MAJOR_VERSION >= 7 ZVAL_LONG (&z_pid, exp_pid); @@ -309,13 +345,17 @@ PHP_FUNCTION(expect_expectl) struct exp_case *ecases, *ecases_ptr, matchedcase; #if PHP_MAJOR_VERSION >= 7 zval *z_stream, *z_cases, *z_match=NULL, *z_case, *z_value; + zend_ulong key; #else zval *z_stream, *z_cases, *z_match=NULL, **z_case, **z_value; + ulong key; #endif php_stream *stream; int fd, argc; - ulong key; - +#if PHP_MAJOR_VERSION >= 7 + HashPosition pos; +#endif + if (ZEND_NUM_ARGS() < 2 || ZEND_NUM_ARGS() > 3) { WRONG_PARAM_COUNT; } if (zend_parse_parameters (ZEND_NUM_ARGS() TSRMLS_CC, "ra|z/", &z_stream, &z_cases, &z_match) == FAILURE) { @@ -329,7 +369,7 @@ PHP_FUNCTION(expect_expectl) #endif #if PHP_MAJOR_VERSION >= 7 - if (!&(stream->wrapperdata)) { + if (Z_TYPE(stream->wrapperdata) != IS_LONG) { #else if (!stream->wrapperdata) { #endif @@ -345,16 +385,17 @@ PHP_FUNCTION(expect_expectl) ecases = (struct exp_case*) safe_emalloc (argc + 1, sizeof(struct exp_case), 0); ecases_ptr = ecases; - zend_hash_internal_pointer_reset (Z_ARRVAL_P(z_cases)); - #if PHP_MAJOR_VERSION >= 7 - while ((z_case = zend_hash_get_current_data (Z_ARRVAL_P(z_cases))) != NULL) + zend_hash_internal_pointer_reset_ex (Z_ARRVAL_P(z_cases), &pos); + + while ((z_case = zend_hash_get_current_data_ex (Z_ARRVAL_P(z_cases), &pos)) != NULL) { zval *z_pattern, *z_exp_type; - zend_hash_get_current_key(Z_ARRVAL_P(z_cases), NULL, &key); + zend_hash_get_current_key_ex(Z_ARRVAL_P(z_cases), NULL, &key, &pos); if (Z_TYPE_P(z_case) != IS_ARRAY) { #else + zend_hash_internal_pointer_reset (Z_ARRVAL_P(z_cases)); while (zend_hash_get_current_data (Z_ARRVAL_P(z_cases), (void **)&z_case) == SUCCESS) { zval **z_pattern, **z_exp_type; @@ -436,7 +477,11 @@ PHP_FUNCTION(expect_expectl) } ecases_ptr++; +#if PHP_MAJOR_VERSION >= 7 + zend_hash_move_forward_ex(Z_ARRVAL_P(z_cases), &pos); +#else zend_hash_move_forward(Z_ARRVAL_P(z_cases)); +#endif } ecases_ptr->pattern = NULL; ecases_ptr->re = NULL; @@ -452,11 +497,15 @@ PHP_FUNCTION(expect_expectl) if (z_match && exp_match && exp_match_len > 0) { char *tmp = (char *)emalloc (sizeof(char) * (exp_match_len + 1)); strlcpy (tmp, exp_match, exp_match_len + 1); -#if PHP_MAJOR_VERSION >= 7 +#if PHP_MAJOR_VERSION > 7 || (PHP_MAJOR_VERSION == 7 && PHP_MINOR_VERSION >= 4) z_match = zend_try_array_init(z_match); - if (!z_match) { - return; - } + if (!z_match) { + return; + } + add_index_string(z_match, 0, tmp); +#elif PHP_MAJOR_VERSION >= 7 + zval_dtor(z_match); + array_init(z_match); add_index_string(z_match, 0, tmp); #else zval_dtor (z_match); diff --git a/expect_fopen_wrapper.c b/expect_fopen_wrapper.c index ce8929f..998934a 100644 --- a/expect_fopen_wrapper.c +++ b/expect_fopen_wrapper.c @@ -26,6 +26,9 @@ #if PHP_MAJOR_VERSION >= 7 php_stream *php_expect_stream_open (php_stream_wrapper *wrapper, const char *command, const char *mode, int options, zend_string **opened_command, php_stream_context *context STREAMS_DC TSRMLS_DC) +#elif PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION >= 6 +php_stream *php_expect_stream_open (php_stream_wrapper *wrapper, const char *command, const char *mode, int options, + char **opened_command, php_stream_context *context STREAMS_DC TSRMLS_DC) #else php_stream *php_expect_stream_open (php_stream_wrapper *wrapper, char *command, char *mode, int options, char **opened_command, php_stream_context *context STREAMS_DC TSRMLS_DC) @@ -36,12 +39,17 @@ php_stream *php_expect_stream_open (php_stream_wrapper *wrapper, char *command, command += sizeof("expect://")-1; } -#if PHP_MAJOR_VERSION >= 7 +#if PHP_MAJOR_VERSION >= 7 || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION >= 6) if ((fp = exp_popen((char*)command)) != NULL) { #else if ((fp = exp_popen(command)) != NULL) { #endif php_stream* stream = php_stream_fopen_from_pipe (fp, mode); + stream->flags |= PHP_STREAM_FLAG_NO_SEEK; + /* PTY reads may return EIO when the child exits; suppress notices (PHP 7.4+ d59aac58b3e7). */ +#ifdef PHP_STREAM_FLAG_SUPPRESS_ERRORS + stream->flags |= PHP_STREAM_FLAG_SUPPRESS_ERRORS; +#endif #if PHP_MAJOR_VERSION >= 7 zval z_pid; ZVAL_LONG (&z_pid, exp_pid); diff --git a/php_expect.h b/php_expect.h index a8ce93d..856ff85 100644 --- a/php_expect.h +++ b/php_expect.h @@ -47,6 +47,7 @@ extern zend_module_entry expect_module_entry; PHP_MINIT_FUNCTION(expect); PHP_MSHUTDOWN_FUNCTION(expect); +PHP_RINIT_FUNCTION(expect); PHP_MINFO_FUNCTION(expect); PHP_FUNCTION(expect_popen); From d1c4cdc29160b2d3fcf1e7712cc33d794c1b6369 Mon Sep 17 00:00:00 2001 From: macintoshplus <814683+macintoshplus@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:56:58 +0100 Subject: [PATCH 3/3] bump version to 0.4.1 --- php_expect.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php_expect.h b/php_expect.h index 856ff85..ea42bec 100644 --- a/php_expect.h +++ b/php_expect.h @@ -37,7 +37,7 @@ extern zend_module_entry expect_module_entry; #define phpext_expect_ptr &expect_module_entry -#define PHP_EXPECT_VERSION "0.4.0" +#define PHP_EXPECT_VERSION "0.4.1" #ifdef PHP_WIN32 #define PHP_EXPECT_API __declspec(dllexport)