├── .github └── workflows │ └── test_n_pub.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── pyproject.toml ├── readme.md ├── requirements.txt ├── src └── namedpipe │ ├── __init__.py │ ├── _posix.py │ └── _win32.py └── tests ├── _test_ffmpeg.py ├── _test_ffmpeg_linux.py ├── _test_ffmpeg_read.py ├── _test_win32.py ├── requirements.txt ├── test_read.py └── test_write.py /.github/workflows/test_n_pub.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | tags: 8 | - "v*.*.*" 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | tests: 14 | name: Python ${{ matrix.python-version }} • ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | exclude: 21 | - os: macos-latest 22 | python-version: 3.9 23 | - os: windows-latest 24 | python-version: 3.9 25 | - os: macos-latest 26 | python-version: 3.10 27 | - os: windows-latest 28 | python-version: 3.10 29 | - os: macos-latest 30 | python-version: 3.11 31 | - os: windows-latest 32 | python-version: 3.11 33 | - os: macos-latest 34 | python-version: 3.12 35 | - os: windows-latest 36 | python-version: 3.12 37 | 38 | steps: 39 | - run: echo ${{github.ref}} 40 | 41 | - uses: actions/checkout@v4 42 | 43 | - name: Set up FFmpeg 44 | uses: FedericoCarboni/setup-ffmpeg@v3.1 45 | with: 46 | architecture: x64 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | - name: Setup Python ${{ matrix.python-version }} 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: ${{ matrix.python-version }} 53 | architecture: ${{ matrix.arch }} 54 | 55 | - name: Setup Python dependencies 56 | run: | 57 | python -m pip install -U pip 58 | pip install -U -r tests/requirements.txt pytest-github-actions-annotate-failures 59 | 60 | - name: Install namedpipe package 61 | run: pip install -q . 62 | 63 | - name: Run tests 64 | run: pytest -vv 65 | 66 | release: 67 | name: Distribution 68 | permissions: write-all 69 | runs-on: ubuntu-latest 70 | needs: tests 71 | if: startsWith(github.ref, 'refs/tags') 72 | steps: 73 | - uses: actions/checkout@v4 74 | 75 | - name: Setup Python 76 | uses: actions/setup-python@v5 77 | with: 78 | python-version: "3.x" # Version range or exact version of a Python version to use, using SemVer's version range syntax 79 | 80 | - name: Setup Python dependencies 81 | run: | 82 | python -m pip install -U pip setuptools 83 | pip install -U build 84 | 85 | - name: Build a binary wheel and a source tarball 86 | run: python -m build --sdist --wheel --outdir dist/ . 87 | 88 | - name: add python distribution files to release 89 | uses: softprops/action-gh-release@v1 90 | with: 91 | files: dist/* 92 | env: 93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 94 | 95 | - uses: actions/upload-artifact@v4 96 | with: 97 | path: ./dist 98 | 99 | pypi-publish: 100 | name: Upload release to PyPI 101 | needs: release 102 | runs-on: ubuntu-latest 103 | # environment: 104 | # name: pypi 105 | # url: https://pypi.org/p/ 106 | permissions: 107 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 108 | steps: 109 | # retrieve your distributions to artifact folder 110 | - uses: actions/download-artifact@v4 111 | 112 | - name: Publish package distributions to PyPI 113 | uses: pypa/gh-action-pypi-publish@release/v1 114 | with: 115 | packages-dir: artifact/ 116 | skip-existing: true 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | .vscode/* 163 | !.vscode/settings.json 164 | !.vscode/tasks.json 165 | !.vscode/launch.json 166 | !.vscode/extensions.json 167 | !.vscode/*.code-snippets 168 | 169 | # Local History for Visual Studio Code 170 | .history/ 171 | 172 | # Built Visual Studio Code Extensions 173 | *.vsix 174 | /.idea 175 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true 7 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.3.0] - 2025-04-10 10 | 11 | ### Changed 12 | 13 | - Dropped the dependency on `pywin32` and calling `kernel32` directly for Windows named pipes 14 | 15 | ## [0.2.5] - 2025-03-01 16 | 17 | ### Fixed 18 | 19 | - Fixed pipe and stream checks by testing to be not None 20 | 21 | ## [0.2.4] - 2025-02-21 22 | 23 | ### Fixed 24 | 25 | - Fixed broken pipe error handling in `win32`. It now correctly raises the `BrokenPipeError`. 26 | 27 | ## [0.2.3] - 2025-02-19 28 | 29 | ### Fixed 30 | 31 | - `__exit__` to re-raise the exception by returning `False` (reverted v0.2.2) 32 | 33 | ## [0.2.2] - 2025-02-19 34 | 35 | ### Fixed 36 | 37 | - `__exit__` to suppress any exception by returning `True` 38 | 39 | ## [0.2.1] - 2025-02-11 40 | 41 | ### Fixed 42 | 43 | - [Issue #3] fixed throwing exception when Windows raises ERROR_PIPE_CONNECTED error, which is an OK behavior 44 | 45 | ## [0.2.0] - 2025-02-04 46 | 47 | ### Added 48 | 49 | - Added `NPopen.readable()` and `NPopen.writable()` methods 50 | 51 | ## [0.1.1] - 2024-03-27 52 | 53 | ### Changed 54 | 55 | - Dropped support for py3.7 56 | - Removed unnecessary dependency `"typing_extensions;python_version<'3.8'"` 57 | 58 | ### Fixed 59 | 60 | - Reverted the use of the new Union type hint (|) unsupported in py3.8 to old `Union[]` syntax [issue#1;issue#2] 61 | 62 | ## [0.1.0] - 2022-11-22 63 | 64 | Initial release 65 | 66 | [unreleased]: https://github.com/python-ffmpegio/python-namedpipe/compare/v0.3.0...HEAD 67 | [0.3.0]: https://github.com/python-ffmpegio/python-namedpipe/compare/v0.2.5...v0.3.0 68 | [0.2.5]: https://github.com/python-ffmpegio/python-namedpipe/compare/v0.2.4...v0.2.4 69 | [0.2.4]: https://github.com/python-ffmpegio/python-namedpipe/compare/v0.2.3...v0.2.4 70 | [0.2.3]: https://github.com/python-ffmpegio/python-namedpipe/compare/v0.2.2...v0.2.3 71 | [0.2.2]: https://github.com/python-ffmpegio/python-namedpipe/compare/v0.2.1...v0.2.2 72 | [0.2.1]: https://github.com/python-ffmpegio/python-namedpipe/compare/v0.2.0...v0.2.1 73 | [0.2.0]: https://github.com/python-ffmpegio/python-namedpipe/compare/v0.1.1...v0.2.0 74 | [0.1.1]: https://github.com/python-ffmpegio/python-namedpipe/compare/v0.1.0...v0.1.1 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 61.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.pytest.ini_options] 6 | testpaths = ["tests"] 7 | 8 | [project] 9 | name = "namedpipe" 10 | description = "Cross-platform named pipe manager" 11 | readme = "README.md" 12 | keywords = ["utility"] 13 | license = { text = "GPL-2.0 License" } 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12", 21 | "Programming Language :: Python :: 3.13", 22 | ] 23 | dynamic = ["version"] 24 | requires-python = ">=3.9" 25 | dependencies = [] 26 | 27 | [project.urls] 28 | Repository = "https://github.com/python-ffmpegio/python-namedpipe" 29 | Issues = "https://github.com/python-ffmpegio/python-namedpipe/issues" 30 | Pull_Requests = "https://github.com/python-ffmpegio/python-namedpipe/pulls" 31 | 32 | [tool.setuptools.dynamic] 33 | version = { attr = "namedpipe.__version__" } 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # `namedpipe` Cross-platform named pipe manager for Python 2 | 3 | ![PyPI](https://img.shields.io/pypi/v/namedpipe) 4 | ![PyPI - Status]( https://img.shields.io/pypi/status/namedpipe) 5 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/namedpipe) 6 | ![GitHub License](https://img.shields.io/github/license/python-ffmpegio/python-namedpipe) 7 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/python-ffmpegio/python-namedpipe/test_n_pub.yml?branch=master) 8 | 9 | Python natively supports named pipes only in Posix systems via `os.mkfifo`. 10 | This package extends the support to Windows and defines a `NPopen` class as 11 | a cross-platform solution to manage named pipes. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | pip install namedpipe 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```python 22 | import subproces as sp 23 | from namedpipe import NPopen 24 | 25 | # 1. Use the context-managing `NPopen` class to open an (auto-)named pipe 26 | # - specify the mode argument with the standard notation (see built-in open()) 27 | with NPopen('r+') as pipe: # bidirectional (duplex) binary pipe 28 | # - for an inbound pipe, specify the read access mode 'rb' or 'rt' 29 | # - for an outbound pipe, specify the write access mode 'wb or 'wt' 30 | 31 | pipe.readable() # returns True if yields a readable stream 32 | pipe.writable() # returns True if yields a writable stream 33 | 34 | # 2. Get the pipe path via pipe.path or str(pipe) to start the client program 35 | sp.run(['my_client', pipe.path]) 36 | 37 | # auto-generated pipe paths (incremental integer pipe #): 38 | # - Posix: $TMPDIR/pipe[a-z0-9_]{8}/[0-9]+ (random pipe directory name) 39 | # - Windows: \\.\pipe\[0-9]+ 40 | 41 | # 3. Wait for the client to connect and create a stream 42 | # - blocks until connection is established 43 | # - returns an io-compatible stream 44 | stream = pipe.wait() 45 | 46 | # 4. Perform read/write operation with stream (or pipe.stream) as a file-like object 47 | b = stream.read(64) # read 64 bytes from the client 48 | b = pipe.stream.read(64) # same call but using the stream property 49 | 50 | in_bytes = bytearray(128) 51 | nread = stream.readinto(in_bytes) # read 128 bytes of data from client and place them in in_bytes 52 | 53 | b_rest = stream.readall() # read all bytes sent by the client, block till client closes the pipe 54 | 55 | stream.write(out_bytes) # send bytes in out_bytes to the client 56 | 57 | # 5. the stream is automatically closed and the pipe object is destroyed 58 | # when comes out of the context 59 | ``` 60 | 61 | ## API Reference 62 | 63 | TBD 64 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -r tests/requirements.txt -------------------------------------------------------------------------------- /src/namedpipe/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.0" 2 | 3 | from os import name as _os_name 4 | 5 | if _os_name == "nt": 6 | from ._win32 import NPopen 7 | else: 8 | from ._posix import NPopen 9 | 10 | NPopen.__doc__ = r"""Create a named pipe. 11 | 12 | On POSIX, the class uses py:func:`os.mkfifo()` to create the named pipe. On 13 | Windows, the class uses the Windows :py:func:`win32pipe.CreateNamedPipe()` 14 | function of the ``pywin32`` package. The arguments to NPopen are as follows. 15 | 16 | ``bufsize`` will be supplied as the corresponding argument to the open() function 17 | when creating the pipe file objects: 18 | 19 | * ``0`` means unbuffered (read and write are one system call and can return short) 20 | * ``1`` means line buffered (only usable in a text mode) 21 | * any other positive value means use a buffer of approximately that size 22 | * negative ``bufsize`` (the default) means the system default of 23 | ``io.DEFAULT_BUFFER_SIZE`` will be used. 24 | 25 | If ``encoding`` or ``errors`` are specified, or `mode` contains `t`, the file 26 | objects ``NPopen.stream`` will be opened in text mode with the specified 27 | ``encoding`` and as described below. 28 | 29 | ``encoding`` gives the name of the encoding that the stream will be decoded or 30 | encoded with. It defaults to ``locale.getencoding()``. ``encoding="locale"`` can 31 | be used to specify the current locale’s encoding explicitly. See `Text Encoding 32 | `_ in Python's 33 | Standard Library documentation for more information. 34 | 35 | ``errors`` is an optional string that specifies how encoding and decoding errors 36 | are to be handled. Pass ``'strict'`` to raise a ``ValueError`` exception if 37 | there is an encoding error (the default of ``None`` has the same effect), or 38 | pass ``'ignore'`` to ignore errors. (Note that ignoring encoding errors can lead 39 | to data loss.) ``'replace'`` causes a replacement marker (such as ``'?'``) to be 40 | inserted where there is malformed data. ``'backslashreplace'`` causes malformed 41 | data to be replaced by a backslashed escape sequence. When writing, 42 | ``'xmlcharrefreplace'`` (replace with the appropriate XML character reference) 43 | or ``'namereplace'`` (replace with ``\N{...}`` escape sequences) can be used. 44 | Any other error handling name that has been registered with 45 | ``codecs.register_error()`` is also valid. 46 | 47 | ``newline`` controls how line endings are handled. It can be ``None``, ``''``, 48 | ``'\n'``, ``'\r'``, and ``'\r\n'``. It works as follows: 49 | 50 | * When reading input from the stream, if ``newline`` is ``None``, universal 51 | newlines mode is enabled. Lines in the input can end in ``'\n'``, ``'\r'``, or 52 | ``'\r\n'``, and these are translated into ``'\n'`` before being returned to 53 | the caller. If ``newline`` is ``''``, universal newlines mode is enabled, but 54 | line endings are returned to the caller untranslated. If newline has any of 55 | the other legal values, input lines are only terminated by the given string, 56 | and the line ending is returned to the caller untranslated. 57 | * When writing output to the stream, if ``newline`` is ``None``, any ``'\n'`` 58 | characters written are translated to the system default line separator, 59 | ``os.linesep``. If ``newline`` is ``''`` or ``'\n'``, no translation takes 60 | place. If ``newline`` is any of the other legal values, any ``'\n'`` 61 | characters written are translated to the given string. 62 | 63 | By default, POSIX named pipe is created with a path signature ``$TMPDIR/pipe[a-z0-9_]{8}/[0-9]+`` 64 | while Windows named pipe is created with ``\\.\pipe\[0-9]+``. In other words, 65 | the named pipe has a numeric name. The pipe name can be customized by ``name`` 66 | argument. Given pipe name will replace the numeric pipe name but still placed in 67 | the same directory. In POSIX, ``name`` may be an absolute path to place the pipe 68 | outside of the ``$TMPDIR/pipe[a-z0-9_]{8}`` directory. 69 | 70 | ``NPopen`` is also a context manager and therefore supports the ``with`` 71 | statement. In this example, pipe and its stream are closed after the ``with`` 72 | statement’s suite is finished—even if an exception occurs: 73 | 74 | .. code-block:: python 75 | 76 | with NPopen('w') as pipe: 77 | 78 | proc = sp.Popen(['my_client', pipe.path]) 79 | 80 | stream = pipe.wait() 81 | 82 | stream.write('Spam and eggs!') 83 | 84 | """ 85 | 86 | NPopen.close.__doc__ = """Close named pipe 87 | 88 | Flush and close ``NPopen.stream`` and delete the pipe from the file system. This 89 | method has no effect if the file is already closed. Once the file is closed, any 90 | operation on the pipe (e.g. reading or writing) will raise a ``ValueError``. 91 | 92 | As a convenience, it is allowed to call this method more than once; only the 93 | first call, however, will have an effect. 94 | """ 95 | 96 | NPopen.wait.__doc__ = """Wait for client connection 97 | 98 | Blocks until the other end of the pipe is opened by a client. 99 | """ 100 | -------------------------------------------------------------------------------- /src/namedpipe/_posix.py: -------------------------------------------------------------------------------- 1 | import os, tempfile 2 | from os import path 3 | from typing import IO, Optional, Union 4 | try: 5 | from typing import Literal # type: ignore 6 | except ImportError: 7 | from typing_extensions import Literal 8 | 9 | 10 | class _FifoMan: 11 | 12 | __instance = None 13 | __inited = False 14 | 15 | def __new__(cls) -> "_FifoMan": 16 | if cls.__instance is None: 17 | cls.__instance = super().__new__(cls) 18 | return cls.__instance 19 | 20 | def __init__(self) -> None: 21 | if type(self).__inited: 22 | return 23 | type(self).__inited = True 24 | 25 | self.tempdir = None 26 | self.id = 0 27 | self.active = 0 # if zero, no tempdir 28 | 29 | def make(self, name): 30 | in_dir = not (name and path.isabs(name)) 31 | if in_dir: 32 | if not self.tempdir: 33 | self.tempdir = tempfile.mkdtemp(prefix="pipe", suffix="") 34 | name = path.join(self.tempdir, str(self.id)) 35 | try: 36 | os.mkfifo(name) 37 | except: 38 | if in_dir and not self.active: 39 | os.rmdir(self.tempdir) 40 | raise 41 | 42 | if in_dir: 43 | # if success, update the counter 44 | self.id += 1 45 | self.active += 1 46 | 47 | return name 48 | 49 | def unlink(self, name): 50 | os.unlink(name) 51 | if self.tempdir and name.startswith(self.tempdir): 52 | self.active -= 1 53 | if not self.active: 54 | os.rmdir(self.tempdir) 55 | self.tempdir = None 56 | 57 | 58 | class NPopen: 59 | def __init__( 60 | self, 61 | mode: Optional[str] = "r", 62 | bufsize: Optional[int] = -1, 63 | encoding: Optional[str] = None, 64 | errors: Optional[str] = None, 65 | newline: Optional[Literal["", "\n", "\r", "\r\n"]] = None, 66 | name: Optional[str] = None, 67 | ): 68 | # "open" named pipe 69 | self._path = _FifoMan().make(name) 70 | self.stream: Union[IO, None] = None # I/O stream of the pipe 71 | 72 | if 't' not in mode and 'b' not in mode: 73 | mode += 'b' # default to binary mode 74 | 75 | self._open_args = { 76 | "mode": mode, 77 | "buffering": bufsize, 78 | "encoding": encoding, 79 | "errors": errors, 80 | "newline": newline, 81 | } 82 | 83 | @property 84 | def path(self): 85 | return self._path 86 | 87 | def __str__(self): 88 | # return the path 89 | return self._path 90 | 91 | def close(self): 92 | # close named pipe 93 | if self.stream is not None: 94 | self.stream.close() 95 | self.stream = None 96 | if path.exists(self._path): 97 | _FifoMan().unlink(self._path) 98 | 99 | def wait(self): 100 | 101 | if self._path is None: 102 | raise RuntimeError("pipe has already been closed.") 103 | 104 | # wait for the pipe to open (the other end to be opened) and return fileobj to read/write 105 | self.stream = open(self._path, **self._open_args) 106 | return self.stream 107 | 108 | def __bool__(self): 109 | return self.stream is not None 110 | 111 | def __enter__(self): 112 | return self 113 | 114 | def __exit__(self, *_): 115 | self.close() 116 | return False 117 | 118 | def readable(self)->bool: 119 | """True if pipe's stream is readable""" 120 | return any(c in self._open_args['mode'] for c in 'r+') 121 | 122 | def writable(self)->bool: 123 | """True if pipe's stream is writable""" 124 | return any(c in self._open_args['mode'] for c in 'wxa+') 125 | -------------------------------------------------------------------------------- /src/namedpipe/_win32.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from ctypes import wintypes 3 | from os import path 4 | import io 5 | from typing import TypeVar, NewType, Literal, IO, Optional, Union 6 | 7 | WritableBuffer = TypeVar("WritableBuffer") 8 | PyHANDLE = NewType("PyHANDLE", int) 9 | 10 | # Define global constants 11 | PIPE_ACCESS_DUPLEX = 0x00000003 12 | PIPE_ACCESS_INBOUND = 0x00000001 13 | PIPE_ACCESS_OUTBOUND = 0x00000002 14 | PIPE_TYPE_BYTE = 0x00000000 15 | PIPE_READMODE_BYTE = 0x00000000 16 | PIPE_WAIT = 0x00000000 17 | PIPE_UNLIMITED_INSTANCES = 0xFFFFFFFF 18 | ERROR_PIPE_CONNECTED = 535 19 | ERROR_BROKEN_PIPE = 109 20 | ERROR_MORE_DATA = 234 21 | ERROR_IO_PENDING = 997 22 | FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 23 | INVALID_HANDLE_VALUE = -1 24 | 25 | id = 0 26 | 27 | def _wt(value: int) -> wintypes.DWORD: 28 | return wintypes.DWORD(value) 29 | 30 | def _name_pipe(): 31 | global id 32 | 33 | notok = True 34 | while notok: 35 | name = rf"\\.\pipe\{id}" 36 | id += 1 37 | notok = path.exists(name) 38 | 39 | return name 40 | 41 | 42 | def _win_error(code=None): 43 | if not code: 44 | code = ctypes.get_last_error() 45 | return ctypes.WinError(code) 46 | 47 | class NPopen: 48 | def __init__( 49 | self, 50 | mode: Optional[str] = "r", 51 | bufsize: Optional[int] = -1, 52 | encoding: Optional[str] = None, 53 | errors: Optional[str] = None, 54 | newline: Optional[Literal["", "\n", "\r", "\r\n"]] = None, 55 | name: Optional[str] = None, 56 | ): 57 | if bufsize is None: 58 | bufsize = -1 # Restore default 59 | if not isinstance(bufsize, int): 60 | raise TypeError("bufsize must be an integer") 61 | 62 | self.kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) 63 | self.stream: Union[IO, None] = None # I/O stream of the pipe 64 | self._path = _name_pipe() if name is None else rf"\\.\pipe\{name}" 65 | self._rd = any(c in mode for c in "r+") 66 | self._wr = any(c in mode for c in "wax+") 67 | 68 | if encoding or errors or newline: 69 | if "b" in mode: 70 | raise ValueError( 71 | "Cannot disambiguate when mode includes 't' and " 72 | "and encoding or errors or newline is supplied." 73 | ) 74 | txt_mode = True 75 | else: 76 | txt_mode = "t" in mode 77 | 78 | self._txt = ( 79 | { 80 | "encoding": encoding, 81 | "errors": errors, 82 | "newline": newline, 83 | "line_buffering": bufsize == 1, 84 | "write_through": self._wr, 85 | } 86 | if txt_mode 87 | else None 88 | ) 89 | 90 | self._bufsize = -1 if txt_mode and bufsize == 1 else bufsize 91 | if self._rd and self._wr: 92 | access = _wt(PIPE_ACCESS_DUPLEX) 93 | elif self._rd: 94 | access = _wt(PIPE_ACCESS_INBOUND) 95 | elif self._wr: 96 | access = _wt(PIPE_ACCESS_OUTBOUND) 97 | else: 98 | raise ValueError("Invalid mode") 99 | # TODO: assess options: FILE_FLAG_WRITE_THROUGH, FILE_FLAG_OVERLAPPED, FILE_FLAG_FIRST_PIPE_INSTANCE 100 | pipe_mode = _wt(PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT) 101 | 102 | # TODO: assess options: PIPE_WAIT, PIPE_NOWAIT, PIPE_ACCEPT_REMOTE_CLIENTS, PIPE_REJECT_REMOTE_CLIENTS 103 | 104 | max_instances = _wt(1) # PIPE_UNLIMITED_INSTANCES returns 'invalid params'. Pipes are point-to-point anyway 105 | buffer_size = _wt(0) 106 | timeout = _wt(0) 107 | 108 | # "open" named pipe 109 | h = self.kernel32.CreateNamedPipeW( 110 | self._path, 111 | access, 112 | pipe_mode, 113 | max_instances, 114 | buffer_size, 115 | buffer_size, 116 | timeout, 117 | None, 118 | ) 119 | if h == INVALID_HANDLE_VALUE: 120 | raise _win_error() 121 | self._pipe = h 122 | 123 | @property 124 | def path(self): 125 | """str: path of the pipe in the file system""" 126 | return self._path 127 | 128 | def __str__(self): 129 | return self._path 130 | 131 | def close(self): 132 | """Close the named pipe. 133 | A closed pipe cannot be used for further I/O operations. `close()` may 134 | be called more than once without error. 135 | """ 136 | if self.stream is not None: 137 | self.stream.close() 138 | self.stream = None 139 | if self._pipe is not None: 140 | self.kernel32.CloseHandle(self._pipe) 141 | self._pipe = None 142 | 143 | def wait(self): 144 | """Wait for the pipe to open (the other end to be opened) and return file object to read/write.""" 145 | if not self._pipe: 146 | raise RuntimeError("pipe has already been closed.") 147 | if not self.kernel32.ConnectNamedPipe(self._pipe, None): 148 | code = ctypes.get_last_error() 149 | if code != ERROR_PIPE_CONNECTED: # (ok, just indicating that the client has already connected)(Issue#3) 150 | raise _win_error(code) 151 | 152 | # create new io stream object 153 | stream = Win32RawIO(self._pipe, self._rd, self._wr) 154 | 155 | if self._bufsize: 156 | Wrapper = ( 157 | io.BufferedRandom 158 | if self._rd and self._wr 159 | else io.BufferedReader if self._rd else io.BufferedWriter 160 | ) 161 | stream = Wrapper( 162 | stream, self._bufsize if self._bufsize > 0 else io.DEFAULT_BUFFER_SIZE 163 | ) 164 | 165 | if self._txt is not None: 166 | stream = io.TextIOWrapper(stream, **self._txt) 167 | 168 | self.stream = stream 169 | return stream 170 | 171 | def __bool__(self): 172 | return self.stream is not None 173 | 174 | def __enter__(self): 175 | return self 176 | 177 | def __exit__(self, *_): 178 | self.close() 179 | return False 180 | 181 | def readable(self) -> bool: 182 | """True if pipe's stream is readable""" 183 | return self._rd 184 | 185 | def writable(self) -> bool: 186 | """True if pipe's stream is writable""" 187 | return self._wr 188 | 189 | 190 | class Win32RawIO(io.RawIOBase): 191 | """Raw I/O stream layer over open Windows pipe handle. 192 | 193 | `handle` is an open Windows ``HANDLE`` object (from ``ctype`` package) to 194 | be wrapped by this class. 195 | 196 | Specify the read and write modes by boolean flags: ``rd`` and ``wr``. 197 | """ 198 | 199 | def __init__(self, handle: PyHANDLE, rd: bool, wr: bool) -> None: 200 | super().__init__() 201 | self.kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) 202 | self.handle = handle # Underlying Windows handle. 203 | self._readable: bool = rd 204 | self._writable: bool = wr 205 | 206 | def readable(self) -> bool: 207 | """True if file was opened in a read mode.""" 208 | return self._readable 209 | 210 | def seekable(self) -> bool: 211 | """Always returns False (pipes are not seekable).""" 212 | return False 213 | 214 | def writable(self) -> bool: 215 | """True if file was opened in a write mode.""" 216 | return self._writable 217 | 218 | def close(self) -> None: 219 | """Close the pipe. 220 | 221 | A closed pipe cannot be used for further I/O operations. ``close()`` may 222 | be called more than once without error.""" 223 | if self.closed: 224 | return 225 | if self.handle is not None: 226 | self.kernel32.CloseHandle(self.handle) 227 | self.handle = None 228 | 229 | super().close() 230 | 231 | def readinto(self, b: WritableBuffer) -> Union[int, None]: 232 | """Read bytes into a pre-allocated, writable bytes-like object ``b`` and 233 | return the number of bytes read. For example, ``b`` might be a ``bytearray``.""" 234 | 235 | assert self.handle is not None # show type checkers we already checked 236 | assert self._readable 237 | 238 | size = len(b) 239 | nread = _wt(0) 240 | buf = (ctypes.c_char * size).from_buffer(b) 241 | 242 | success = self.kernel32.ReadFile(self.handle, buf, size, ctypes.byref(nread), None) 243 | if not success: 244 | code = ctypes.get_last_error() 245 | # ERROR_MORE_DATA - not big deal, will read next time 246 | # ERROR_IO_PENDING - should not happen, unless use OVERLAPPING, which we don't so far 247 | # ERROR_BROKEN_PIPE - pipe was closed from other end. While it is an error, test seemingly expects to receive 0 instead of exception 248 | if code not in (ERROR_MORE_DATA, ERROR_IO_PENDING, ERROR_BROKEN_PIPE): 249 | raise _win_error(code) 250 | 251 | return nread.value 252 | 253 | def write(self, b): 254 | """Write buffer ``b`` to file, return number of bytes written. 255 | 256 | Only makes one system call, so not all of the data may be written. 257 | The number of bytes actually written is returned.""" 258 | 259 | assert self.handle is not None # show type checkers we already checked 260 | assert self._writable 261 | size = len(b) 262 | nwritten = _wt(0) 263 | buf = (ctypes.c_char * size).from_buffer_copy(b) 264 | if not self.kernel32.WriteFile(self.handle, buf, _wt(size), ctypes.byref(nwritten), None): 265 | raise _win_error() 266 | 267 | return nwritten.value 268 | -------------------------------------------------------------------------------- /tests/_test_ffmpeg.py: -------------------------------------------------------------------------------- 1 | import time 2 | import win32pipe, win32file 3 | import numpy as np 4 | import subprocess as sp 5 | 6 | print("pipe server") 7 | count = 0 8 | pipe = win32pipe.CreateNamedPipe( 9 | r"\\.\pipe\Foo", 10 | win32pipe.PIPE_ACCESS_DUPLEX, 11 | win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_READMODE_BYTE | win32pipe.PIPE_WAIT, 12 | 1, 13 | 65536, 14 | 65536, 15 | 0, 16 | None, 17 | ) 18 | 19 | proc = sp.Popen( 20 | [ 21 | "ffmpeg", 22 | "-y", 23 | "-f", 24 | "rawvideo", 25 | "-pix_fmt", 26 | "rgb24", 27 | "-s", 28 | "480x320", 29 | "-r", 30 | "30", 31 | "-i", 32 | r"\\.\pipe\Foo", 33 | "-pix_fmt", 34 | "yuv420p", 35 | "output.mp4", 36 | ] 37 | ) 38 | 39 | 40 | try: 41 | print("waiting for client") 42 | win32pipe.ConnectNamedPipe(pipe, None) 43 | print("got client") 44 | 45 | for i in range(30): 46 | print(f"writing message {i}") 47 | F = np.random.randint(0, 255, [320, 480, 3], np.uint8) 48 | win32file.WriteFile(pipe, F) 49 | print("finished now") 50 | finally: 51 | win32file.CloseHandle(pipe) 52 | 53 | proc.wait() 54 | 55 | sp.run(["ffprobe", "output.mp4"]) 56 | -------------------------------------------------------------------------------- /tests/_test_ffmpeg_linux.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import subprocess as sp 4 | 5 | print("pipe server") 6 | count = 0 7 | pipe = os.mkfifo("Foo") 8 | 9 | proc = sp.Popen( 10 | [ 11 | "ffmpeg", 12 | "-y", 13 | "-f", 14 | "rawvideo", 15 | "-pix_fmt", 16 | "rgb24", 17 | "-s", 18 | "480x320", 19 | "-r", 20 | "30", 21 | "-i", 22 | "Foo", 23 | "-pix_fmt", 24 | "yuv420p", 25 | "output.mp4", 26 | ] 27 | ) 28 | 29 | with open("Foo", "wb") as f: 30 | 31 | for i in range(30): 32 | print(f"writing message {i}") 33 | F = np.random.randint(0, 255, [320, 480, 3], np.uint8) 34 | f.write(F) 35 | print("finished now") 36 | 37 | proc.wait() 38 | 39 | os.unlink('Foo') 40 | 41 | sp.run(["ffprobe", "output.mp4"]) 42 | -------------------------------------------------------------------------------- /tests/_test_ffmpeg_read.py: -------------------------------------------------------------------------------- 1 | import time 2 | import win32pipe, win32file 3 | import numpy as np 4 | import subprocess as sp 5 | 6 | print("pipe server") 7 | count = 0 8 | pipe = win32pipe.CreateNamedPipe( 9 | r"\\.\pipe\Foo", 10 | win32pipe.PIPE_ACCESS_DUPLEX, 11 | win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_READMODE_BYTE | win32pipe.PIPE_WAIT, 12 | 1, 13 | 65536, 14 | 65536, 15 | 0, 16 | None, 17 | ) 18 | 19 | proc = sp.Popen( 20 | [ 21 | "ffmpeg", 22 | "-y", 23 | "-f", "lavfi", 24 | "-i", "testsrc=s=320x240", 25 | "-f", "rawvideo", 26 | "-pix_fmt", "rgb24", 27 | r"\\.\pipe\Foo", 28 | ] 29 | ) 30 | 31 | 32 | try: 33 | print("waiting for client") 34 | win32pipe.ConnectNamedPipe(pipe, None) 35 | print("got client") 36 | 37 | nbytes = 320*240*3 38 | print(f'frame size: {nbytes} bytes') 39 | 40 | for i in range(30): 41 | print(f"reading frame {i}") 42 | hr,buf = win32file.ReadFile(pipe, nbytes) 43 | if hr: 44 | print('read error') 45 | else: 46 | print(f'read {len(buf)} bytes') 47 | print("finished now") 48 | finally: 49 | win32file.CloseHandle(pipe) 50 | 51 | proc.wait() 52 | 53 | sp.run(["ffprobe", "output.mp4"]) 54 | -------------------------------------------------------------------------------- /tests/_test_win32.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import win32pipe, win32file, pywintypes 4 | 5 | # https://stackoverflow.com/questions/48542644/python-and-windows-named-pipes 6 | def pipe_server(): 7 | print("pipe server") 8 | count = 0 9 | pipe = win32pipe.CreateNamedPipe( 10 | r'\\.\pipe\Foo', 11 | win32pipe.PIPE_ACCESS_DUPLEX, 12 | win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT, 13 | 1, 65536, 65536, 14 | 0, 15 | None) 16 | 17 | try: 18 | print("waiting for client") 19 | win32pipe.ConnectNamedPipe(pipe, None) 20 | print("got client") 21 | 22 | while count < 10: 23 | print(f"writing message {count}") 24 | # convert to bytes 25 | some_data = str.encode(f"{count}\n") 26 | win32file.WriteFile(pipe, some_data) 27 | time.sleep(1) 28 | count += 1 29 | 30 | print("finished now") 31 | finally: 32 | win32pipe.DisconnectNamedPipe(pipe) 33 | win32file.CloseHandle(pipe) 34 | 35 | 36 | def pipe_client(): 37 | print("pipe client") 38 | quit = False 39 | 40 | with open(r'\\.\pipe\Foo','rt') as f: 41 | while not f.closed: 42 | line = f.readline() 43 | print(line) 44 | # while not quit: 45 | # try: 46 | # handle = win32file.CreateFile( 47 | # r'\\.\pipe\Foo', 48 | # win32file.GENERIC_READ | win32file.GENERIC_WRITE, 49 | # 0, 50 | # None, 51 | # win32file.OPEN_EXISTING, 52 | # 0, 53 | # None 54 | # ) 55 | # res = win32pipe.SetNamedPipeHandleState(handle, win32pipe.PIPE_READMODE_MESSAGE, None, None) 56 | # if res == 0: 57 | # print(f"SetNamedPipeHandleState return code: {res}") 58 | # while True: 59 | # resp = win32file.ReadFile(handle, 64*1024) 60 | # print(f"message: {resp}") 61 | # except pywintypes.error as e: 62 | # if e.args[0] == 2: 63 | # print("no pipe, trying again in a sec") 64 | # time.sleep(1) 65 | # elif e.args[0] == 109: 66 | # print("broken pipe, bye bye") 67 | # quit = True 68 | 69 | 70 | if __name__ == '__main__': 71 | if len(sys.argv) < 2: 72 | print("need s or c as argument") 73 | elif sys.argv[1] == "s": 74 | pipe_server() 75 | elif sys.argv[1] == "c": 76 | pipe_client() 77 | else: 78 | print(f"no can do: {sys.argv[1]}") -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | numpy -------------------------------------------------------------------------------- /tests/test_read.py: -------------------------------------------------------------------------------- 1 | import subprocess as sp 2 | from namedpipe import NPopen 3 | 4 | 5 | def run_ffmpeg(pipe): 6 | sz = [320, 240] 7 | return ( 8 | sp.Popen( 9 | # fmt:off 10 | [ 11 | "ffmpeg", 12 | "-y", 13 | "-f", "lavfi", 14 | "-i", f"testsrc=s={sz[0]}x{sz[1]}:d=5", 15 | "-f", "rawvideo", 16 | "-pix_fmt", "rgb24", 17 | f'{pipe}', 18 | ] 19 | # fmt:on 20 | ), 21 | sz[0] * sz[1] * 3, 22 | ) 23 | 24 | 25 | def test_read_all(): 26 | with NPopen("r") as pipe: 27 | assert pipe.readable() 28 | assert not pipe.writable() 29 | proc, nbytes = run_ffmpeg(pipe) 30 | f = pipe.wait() 31 | while f.read(nbytes): 32 | pass 33 | proc.wait() 34 | 35 | 36 | def test_read_some(): 37 | with NPopen("r") as pipe: 38 | proc, nbytes = run_ffmpeg(pipe) 39 | f = pipe.wait() 40 | for i in range(30): 41 | f.read(nbytes) 42 | proc.kill() 43 | 44 | if __name__=='__main__': 45 | test_read_all() -------------------------------------------------------------------------------- /tests/test_write.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import subprocess as sp 3 | from namedpipe import NPopen 4 | from tempfile import TemporaryDirectory 5 | from os import path 6 | 7 | 8 | def run_ffmpeg(pipe, dstdir): 9 | outpath = path.join(dstdir, "output.mp4") 10 | sz = [320, 240, 3] 11 | return ( 12 | sp.Popen( 13 | # fmt:off 14 | [ 15 | "ffmpeg", 16 | "-y", 17 | "-f", "rawvideo", 18 | "-pix_fmt", "rgb24", 19 | "-s", f"{sz[0]}x{sz[1]}", 20 | "-r", "30", 21 | "-i", str(pipe), 22 | outpath, 23 | ] 24 | # fmt:on 25 | ), 26 | sz, 27 | outpath, 28 | ) 29 | 30 | 31 | def test_write(): 32 | print("pipe server") 33 | with TemporaryDirectory() as dstdir: 34 | with NPopen("w") as pipe: 35 | 36 | proc, shape, outpath = run_ffmpeg(pipe, dstdir) 37 | f = pipe.wait() 38 | for i in range(30): 39 | F = np.random.randint(0, 255, shape, np.uint8) 40 | f.write(F) 41 | 42 | proc.wait() 43 | 44 | sp.run(["ffprobe", outpath]) 45 | 46 | 47 | if __name__ == "__main__": 48 | test_write() 49 | --------------------------------------------------------------------------------