├── .git-blame-ignore-revs ├── .github └── workflows │ ├── ci.yaml │ ├── doc.yaml │ └── release.yaml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── codecov.yml ├── docs ├── docs │ ├── docs │ │ ├── blogposts.md │ │ ├── fs2 │ │ │ ├── custom-runners.md │ │ │ ├── customize.md │ │ │ ├── index.md │ │ │ ├── processgroups.md │ │ │ ├── redirection.md │ │ │ └── running.md │ │ ├── index.md │ │ ├── migration.md │ │ └── zstream │ │ │ ├── custom-runners.md │ │ │ ├── customize.md │ │ │ ├── index.md │ │ │ ├── processgroups.md │ │ │ ├── redirection.md │ │ │ └── running.md │ └── index.md └── src │ └── microsite │ ├── data │ └── menu.yml │ └── img │ ├── second-feature-icon.svg │ └── third-feature-icon.svg ├── examples └── externalpyproc │ ├── init.sh │ └── test.py ├── project ├── build.properties └── plugins.sbt ├── prox-core └── src │ └── main │ └── scala │ └── io │ └── github │ └── vigoo │ └── prox │ ├── common.scala │ ├── errors.scala │ ├── package.scala │ ├── path │ └── package.scala │ ├── process.scala │ ├── processgroup.scala │ ├── redirection.scala │ ├── runner.scala │ ├── runtime.scala │ └── syntax.scala ├── prox-fs2-3 └── src │ ├── main │ └── scala │ │ └── io │ │ └── github │ │ └── vigoo │ │ └── prox │ │ └── ProxFS2.scala │ └── test │ └── scala │ └── io │ └── github │ └── vigoo │ └── prox │ └── tests │ └── fs2 │ ├── InterpolatorSpecs.scala │ ├── ProcessGroupSpecs.scala │ ├── ProcessSpecs.scala │ └── ProxSpecHelpers.scala ├── prox-java9 └── src │ └── main │ └── scala │ └── io │ └── github │ └── vigoo │ └── prox │ └── java9 │ └── runner.scala ├── prox-zstream-2 └── src │ ├── main │ └── scala │ │ └── io │ │ └── github │ │ └── vigoo │ │ └── prox │ │ └── ProxZStream.scala │ └── test │ └── scala │ └── io │ └── github │ └── vigoo │ └── prox │ └── tests │ └── zstream │ ├── ProcessGroupSpecs.scala │ ├── ProcessSpecs.scala │ └── ProxSpecHelpers.scala └── scripts ├── decrypt_keys.sh └── publish_microsite.sh /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.8.1 2 | 9dd9b24328ef9e0200929265b623295bf8f7f6a3 3 | 4 | # Scala Steward: Reformat with scalafmt 3.8.6 5 | 12ea712573e8e3e8b26a9c3bb69ad0e921b8cfbb 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | build-test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | scala: ['2.12.18', '2.13.12', '3.3.1'] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Setup Scala 16 | uses: olafurpg/setup-scala@v10 17 | with: 18 | java-version: "adopt@1.11" 19 | - name: Coursier cache 20 | uses: coursier/cache-action@v5 21 | - name: Build and test 22 | if: ${{ matrix.scala == '3.1.0' }} 23 | run: sbt ++${{ matrix.scala }} clean test 24 | - name: Build and test 25 | if: ${{ matrix.scala != '3.1.0' }} 26 | run: sbt ++${{ matrix.scala }} clean coverage test coverageReport && bash <(curl -s https://codecov.io/bash) 27 | -------------------------------------------------------------------------------- /.github/workflows/doc.yaml: -------------------------------------------------------------------------------- 1 | name: Website 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | if: github.event_name != 'pull_request' 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - uses: olafurpg/setup-scala@v10 18 | with: 19 | java-version: "adopt@1.11" 20 | - uses: olafurpg/setup-gpg@v3 21 | - name: Setup GIT user 22 | uses: fregante/setup-git-user@v1 23 | - name: Setup Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: 2.6.6 27 | bundler-cache: true 28 | - name: Install Jekyll 29 | run: | 30 | gem install sass 31 | gem install activesupport -v 6.1.4.4 32 | gem install jekyll -v 4.0.0 33 | gem install nokogiri -v 1.13.10 34 | gem install jemoji -v 0.11.1 35 | gem install jekyll-sitemap -v 1.4.0 36 | - run: sbt docs/publishMicrosite 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: ['master'] 5 | release: 6 | types: 7 | - published 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Scala 17 | uses: olafurpg/setup-scala@v10 18 | with: 19 | java-version: "adopt@1.11" 20 | - uses: olafurpg/setup-gpg@v3 21 | - run: sbt ci-release 22 | env: 23 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 24 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 25 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 26 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | target/ 3 | lib_managed/ 4 | src_managed/ 5 | project/boot/ 6 | project/plugins/project/ 7 | .history 8 | .cache 9 | .lib/ 10 | .idea 11 | examples/externalpyproc/virtualenv 12 | .bloop/ 13 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.8.6 2 | runner.dialect = "scala213source3" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [prox](https://vigoo.github.io/prox) 2 | ![CI](https://github.com/vigoo/prox/workflows/CI/badge.svg) 3 | [![codecov](https://codecov.io/gh/vigoo/prox/branch/master/graph/badge.svg)](https://codecov.io/gh/vigoo/prox) 4 | [![Apache 2 License License](http://img.shields.io/badge/license-APACHE2-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 5 | [![Latest version](https://index.scala-lang.org/vigoo/prox/prox-core/latest.svg)](https://index.scala-lang.org/vigoo/prox/prox-core) 6 | ![Maven central](https://img.shields.io/maven-central/v/io.github.vigoo/prox-core_2.13.svg?style=flat-square) 7 | 8 | **prox** is a small library that helps you starting system processes and redirecting their input/output/error streams, 9 | either to files, [fs2](https://github.com/functional-streams-for-scala/fs2) streams or each other. 10 | 11 | > :warning: **Version 0.5 is a complete redesign of the library** 12 | 13 | See the [project's site](https://vigoo.github.io/prox) for documentation and examples. 14 | 15 | 16 | ---- 17 | 18 | YourKit supports open source projects with innovative and intelligent tools 19 | for monitoring and profiling Java and .NET applications. 20 | YourKit is the creator of YourKit Java Profiler, 21 | YourKit .NET Profiler, 22 | and YourKit YouMonitor. 23 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | val scala212 = "2.12.18" 2 | val scala213 = "2.13.12" 3 | val scala3 = "3.3.5" 4 | 5 | val zio2Version = "2.1.17" 6 | 7 | def scalacOptions212(jdk: Int) = Seq( 8 | "-Ypartial-unification", 9 | "-deprecation", 10 | "-Xsource:3", 11 | "-release", 12 | jdk.toString 13 | ) 14 | def scalacOptions213(jdk: Int) = 15 | Seq("-deprecation", "-Xsource:3", "-release", jdk.toString) 16 | def scalacOptions3(jdk: Int) = 17 | Seq("-deprecation", "-Ykind-projector", "-release", jdk.toString) 18 | 19 | import microsites.ConfigYml 20 | import xerial.sbt.Sonatype._ 21 | 22 | import scala.xml.{Node => XmlNode, NodeSeq => XmlNodeSeq, _} 23 | import scala.xml.transform.{RewriteRule, RuleTransformer} 24 | 25 | ThisBuild / dynverSonatypeSnapshots := true 26 | 27 | def commonSettings(jdk: Int) = Seq( 28 | organization := "io.github.vigoo", 29 | scalaVersion := scala213, 30 | crossScalaVersions := List(scala212, scala213, scala3), 31 | libraryDependencies ++= 32 | (CrossVersion.partialVersion(scalaVersion.value) match { 33 | case Some((3, _)) => Seq.empty 34 | case _ => 35 | Seq( 36 | compilerPlugin( 37 | "org.typelevel" % "kind-projector" % "0.13.3" cross CrossVersion.full 38 | ) 39 | ) 40 | }), 41 | libraryDependencies ++= Seq( 42 | "org.scala-lang.modules" %% "scala-collection-compat" % "2.13.0" 43 | ), 44 | Test / compile / coverageEnabled := true, 45 | Compile / compile / coverageEnabled := false, 46 | scalacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match { 47 | case Some((2, 12)) => scalacOptions212(jdk) 48 | case Some((2, 13)) => scalacOptions213(jdk) 49 | case Some((3, _)) => scalacOptions3(jdk) 50 | case _ => Nil 51 | }), 52 | 53 | // Publishing 54 | 55 | publishMavenStyle := true, 56 | licenses := Seq( 57 | "APL2" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt") 58 | ), 59 | sonatypeProjectHosting := Some( 60 | GitHubHosting("vigoo", "prox", "daniel.vigovszky@gmail.com") 61 | ), 62 | developers := List( 63 | Developer( 64 | id = "vigoo", 65 | name = "Daniel Vigovszky", 66 | email = "daniel.vigovszky@gmail.com", 67 | url = url("https://vigoo.github.io") 68 | ) 69 | ), 70 | sonatypeCredentialHost := "s01.oss.sonatype.org", 71 | sonatypeRepository := "https://s01.oss.sonatype.org/service/local", 72 | credentials ++= 73 | (for { 74 | username <- Option(System.getenv().get("SONATYPE_USERNAME")) 75 | password <- Option(System.getenv().get("SONATYPE_PASSWORD")) 76 | } yield Credentials( 77 | "Sonatype Nexus Repository Manager", 78 | "s01.oss.sonatype.org", 79 | username, 80 | password 81 | )).toSeq 82 | ) 83 | 84 | lazy val prox = project 85 | .in(file(".")) 86 | .settings(commonSettings(8)) 87 | .settings( 88 | name := "prox", 89 | organization := "io.github.vigoo", 90 | publish / skip := true 91 | ) 92 | .aggregate(proxCore, proxFS23, proxZStream2, proxJava9) 93 | 94 | lazy val proxCore = 95 | Project("prox-core", file("prox-core")).settings(commonSettings(8)) 96 | 97 | lazy val proxFS23 = Project("prox-fs2-3", file("prox-fs2-3")) 98 | .settings(commonSettings(8)) 99 | .settings( 100 | libraryDependencies ++= Seq( 101 | "co.fs2" %% "fs2-core" % "3.12.0", 102 | "co.fs2" %% "fs2-io" % "3.12.0", 103 | "dev.zio" %% "zio" % zio2Version % "test", 104 | "dev.zio" %% "zio-test" % zio2Version % "test", 105 | "dev.zio" %% "zio-test-sbt" % zio2Version % "test", 106 | "dev.zio" %% "zio-interop-cats" % "23.1.0.3" % "test" 107 | ), 108 | testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") 109 | ) 110 | .dependsOn(proxCore) 111 | 112 | lazy val proxZStream2 = Project("prox-zstream-2", file("prox-zstream-2")) 113 | .settings(commonSettings(8)) 114 | .settings( 115 | resolvers += 116 | "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots", 117 | libraryDependencies ++= Seq( 118 | "dev.zio" %% "zio" % zio2Version, 119 | "dev.zio" %% "zio-streams" % zio2Version, 120 | "dev.zio" %% "zio-prelude" % "1.0.0-RC28", 121 | "dev.zio" %% "zio-test" % zio2Version % "test", 122 | "dev.zio" %% "zio-test-sbt" % zio2Version % "test" 123 | ), 124 | testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") 125 | ) 126 | .dependsOn(proxCore) 127 | 128 | lazy val proxJava9 = Project("prox-java9", file("prox-java9")) 129 | .settings(commonSettings(9)) 130 | .dependsOn(proxCore) 131 | 132 | lazy val docs = project 133 | .enablePlugins( 134 | GhpagesPlugin, 135 | SiteScaladocPlugin, 136 | ScalaUnidocPlugin, 137 | MicrositesPlugin 138 | ) 139 | .settings(commonSettings(9)) 140 | .settings( 141 | addCompilerPlugin( 142 | "org.typelevel" %% s"kind-projector" % "0.13.2" cross CrossVersion.full 143 | ), 144 | publishArtifact := false, 145 | publish / skip := true, 146 | scalaVersion := scala213, 147 | name := "prox", 148 | description := "A Scala library for working with system processes", 149 | git.remoteRepo := "git@github.com:vigoo/prox.git", 150 | ScalaUnidoc / siteSubdirName := "api", 151 | addMappingsToSiteDir( 152 | ScalaUnidoc / packageDoc / mappings, 153 | ScalaUnidoc / siteSubdirName 154 | ), 155 | ScalaUnidoc / unidoc / unidocProjectFilter := inProjects( 156 | proxCore, 157 | proxFS23, 158 | proxZStream2, 159 | proxJava9 160 | ), 161 | micrositeUrl := "https://vigoo.github.io", 162 | micrositeBaseUrl := "/prox", 163 | micrositeHomepage := "https://vigoo.github.io/prox/", 164 | micrositeDocumentationUrl := "/prox/docs", 165 | micrositeAuthor := "Daniel Vigovszky", 166 | micrositeTwitterCreator := "@dvigovszky", 167 | micrositeGithubOwner := "vigoo", 168 | micrositeGithubRepo := "prox", 169 | micrositeGitterChannel := false, 170 | micrositeDataDirectory := baseDirectory.value / "src/microsite/data", 171 | micrositeStaticDirectory := baseDirectory.value / "src/microsite/static", 172 | micrositeImgDirectory := baseDirectory.value / "src/microsite/img", 173 | micrositeCssDirectory := baseDirectory.value / "src/microsite/styles", 174 | micrositeSassDirectory := baseDirectory.value / "src/microsite/partials", 175 | micrositeJsDirectory := baseDirectory.value / "src/microsite/scripts", 176 | micrositeTheme := "light", 177 | micrositeHighlightLanguages ++= Seq("scala", "sbt"), 178 | micrositeConfigYaml := ConfigYml( 179 | yamlCustomProperties = Map( 180 | "url" -> "https://vigoo.github.io", 181 | "plugins" -> List("jemoji", "jekyll-sitemap") 182 | ) 183 | ), 184 | micrositeAnalyticsToken := "UA-56320875-3", 185 | micrositePushSiteWith := GitHub4s, 186 | micrositeGithubToken := sys.env.get("GITHUB_TOKEN"), 187 | makeSite / includeFilter := "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.js" | "*.swf" | "*.txt" | "*.xml" | "*.svg", 188 | // Temporary fix to avoid including mdoc in the published POM 189 | 190 | // skip dependency elements with a scope 191 | pomPostProcess := { (node: XmlNode) => 192 | new RuleTransformer(new RewriteRule { 193 | override def transform(node: XmlNode): XmlNodeSeq = node match { 194 | case e: Elem 195 | if e.label == "dependency" && e.child.exists(child => 196 | child.label == "artifactId" && child.text.startsWith("mdoc_") 197 | ) => 198 | val organization = 199 | e.child.filter(_.label == "groupId").flatMap(_.text).mkString 200 | val artifact = 201 | e.child.filter(_.label == "artifactId").flatMap(_.text).mkString 202 | val version = 203 | e.child.filter(_.label == "version").flatMap(_.text).mkString 204 | Comment( 205 | s"dependency $organization#$artifact;$version has been omitted" 206 | ) 207 | case _ => node 208 | } 209 | }).transform(node).head 210 | } 211 | ) 212 | .dependsOn(proxCore, proxFS23, proxZStream2, proxJava9) 213 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 0 3 | round: down 4 | status: 5 | default_rules: 6 | threshold: 2% 7 | -------------------------------------------------------------------------------- /docs/docs/docs/blogposts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Blog posts 4 | --- 5 | # Blog posts 6 | 7 | The following series of blog posts are based on the development of `prox`: 8 | 9 | - [Part 1 - type level programming](https://vigoo.github.io/posts/2019-02-10-prox-1-types.html) 10 | - [Part 2 - Akka Streams with Cats Effect](https://vigoo.github.io/posts/2019-03-07-prox-2-io-akkastreams.html) 11 | - [Part 3 - Effect abstraction and ZIO](https://vigoo.github.io/posts/2019-08-13-prox-3-zio.html) 12 | - [Part 4 - Simplified redesign](https://vigoo.github.io/posts/2020-08-03-prox-4-simplify.html) 13 | -------------------------------------------------------------------------------- /docs/docs/docs/fs2/custom-runners.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Custom runners 4 | --- 5 | 6 | # Customizing the runner 7 | 8 | ```scala mdoc:invisible 9 | import cats.effect._ 10 | import cats.Traverse 11 | import scala.concurrent.ExecutionContext 12 | import io.github.vigoo.prox._ 13 | 14 | val prox = ProxFS2[IO] 15 | import prox._ 16 | ``` 17 | 18 | The _runner_ is responsible for stating the native processes and wiring all the redirections together. The default 19 | implementation is called `JVMProcessRunner`. 20 | 21 | There are use cases when providing a custom runner makes sense. One such use case could be to launch external processes 22 | within a docker container in case of running on a development machine (for example from tests), while running them directly 23 | in production, when the whole service is running within the container. 24 | 25 | We can implement this scenario by using `JVMProcessRunner` in production and a custom `DockerizedProcessRunner` in tests, 26 | where we define the latter as follows: 27 | 28 | ```scala mdoc 29 | import java.nio.file.Path 30 | import java.util.UUID 31 | 32 | case class DockerImage(name: String) 33 | 34 | case class DockerContainer(name: String) 35 | 36 | case class DockerProcessInfo[DockerProcessInfo](container: DockerContainer, dockerProcessInfo: DockerProcessInfo) 37 | 38 | class DockerizedProcessRunner[Info](processRunner: ProcessRunner[Info], 39 | mountedDirectory: Path, 40 | workingDirectory: Path, 41 | image: DockerImage) 42 | extends ProcessRunner[DockerProcessInfo[Info]] { 43 | 44 | override def startProcess[O, E](process: Process[O, E]): IO[RunningProcess[O, E, DockerProcessInfo[Info]]] = { 45 | for { 46 | container <- generateContainerName 47 | runningProcess <- processRunner 48 | .startProcess(wrapInDocker(process, container)) 49 | } yield runningProcess.mapInfo(info => DockerProcessInfo(container, info)) 50 | } 51 | 52 | override def startProcessGroup[O, E](processGroup: ProcessGroup[O, E]): IO[RunningProcessGroup[O, E, DockerProcessInfo[Info]]] = { 53 | Traverse[Vector].sequence(processGroup.originalProcesses.toVector.map(key => generateContainerName.map(c => key -> c))).flatMap { keyAndNames => 54 | val nameMap = keyAndNames.toMap 55 | val names = keyAndNames.map(_._2) 56 | val modifiedProcessGroup = processGroup.map(new ProcessGroup.Mapper[O, E] { 57 | def mapFirst[P <: Process[fs2.Stream[IO, Byte], E]](process: P): P = wrapInDocker(process, names.head).asInstanceOf[P] 58 | def mapInnerWithIdx[P <: Process.UnboundIProcess[fs2.Stream[IO, Byte], E]](process: P, idx: Int): P = 59 | wrapInDocker(process, names(idx)).asInstanceOf[P] 60 | def mapLast[P <: Process.UnboundIProcess[O, E]](process: P): P = wrapInDocker(process, names.last).asInstanceOf[P] 61 | }) 62 | processRunner.startProcessGroup(modifiedProcessGroup) 63 | .map(_.mapInfo { case (key, info) => DockerProcessInfo(nameMap(key), info) }) 64 | } 65 | } 66 | 67 | private def generateContainerName: IO[DockerContainer] = 68 | IO(DockerContainer(UUID.randomUUID().toString)) 69 | 70 | private def wrapInDocker[O, E](process: Process[O, E], container: DockerContainer): Process[O, E] = { 71 | val envVars = process.environmentVariables.flatMap { case (key, value) => List("-e", s"$key=$value") }.toList 72 | process.withCommand("docker").withArguments( 73 | "run" :: 74 | "--name" :: container.name :: 75 | "-v" :: mountedDirectory.toString :: 76 | "-w" :: workingDirectory.toString :: 77 | envVars ::: 78 | List(image.name, process.command) ::: 79 | process.arguments 80 | ) 81 | } 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/docs/docs/fs2/customize.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Customizing environment 4 | --- 5 | 6 | # Customizing the environment 7 | 8 | ```scala mdoc:invisible 9 | import cats.effect._ 10 | import scala.concurrent.ExecutionContext 11 | import io.github.vigoo.prox._ 12 | 13 | val prox = ProxFS2[IO] 14 | import prox._ 15 | ``` 16 | 17 | The type returned by the `Process` constructor also implements the `ProcessConfiguration` trait, 18 | adding three methods that can be used to customize the working environment of the process to be started: 19 | 20 | ### Working directory 21 | 22 | The `in` method can be used to customize the working directory: 23 | 24 | ```scala mdoc 25 | import io.github.vigoo.prox.path._ 26 | 27 | val dir = home / "tmp" 28 | val proc1 = Process("ls") in dir 29 | ``` 30 | 31 | Not that `dir` has the type `java.nio.file.Path`, and the `home / tmp` syntax is just a thin 32 | syntax extension to produce such values. 33 | 34 | ### Adding environment variables 35 | 36 | The `with` method can be used to add environment variables to the process in the following 37 | way: 38 | 39 | ```scala mdoc 40 | val proc2 = Process("echo", List("$TEST")) `with` ("TEST" -> "Hello world") 41 | ``` 42 | 43 | ### Removing environment variables 44 | 45 | The subprocess inherits the parent process environment, so it may be necessary to 46 | _remove_ some already defined environment variables with the `without` method: 47 | 48 | ```scala mdoc 49 | val proc3 = Process("echo" , List("$PATH")) `without` "PATH" 50 | ``` 51 | 52 | ### Writing reusable functions 53 | 54 | Because these methods are part of the `ProcessConfiguration` _capability_, writing reusable functions require us to define 55 | a polymorphic function that requires this capability: 56 | 57 | ```scala mdoc 58 | import java.nio.file.Path 59 | 60 | def withHome[P <: ProcessLike with ProcessLikeConfiguration](home: Path, proc: P): P#Self = 61 | proc `with` ("HOME" -> home.toString) 62 | ``` 63 | 64 | Then we can use it on any kind of process or process group (read about [redirection](redirection) to understand 65 | why there are multiple concrete process types): 66 | 67 | ```scala mdoc 68 | val proc4 = Process("echo", List("$HOME")) 69 | val proc5 = withHome(home, proc4) 70 | 71 | val group1 = Process("grep", List("ERROR")) | Process("sort") 72 | val group2 = withHome(home, group1) 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/docs/docs/fs2/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Getting started 4 | --- 5 | 6 | # Getting started with prox 7 | 8 | First add one of the `prox` interfaces as a dependency: 9 | 10 | ```sbt 11 | libraryDependencies += "io.github.vigoo" %% "prox-fs2" % "0.7.3" 12 | ``` 13 | 14 | or for Cats Effect 3.x / FS2 3.x: 15 | 16 | ```sbt 17 | libraryDependencies += "io.github.vigoo" %% "prox-fs2-3" % "0.7.3" 18 | ``` 19 | 20 | and, assuming that we have a long living `Blocker` thread pool defined already, we can create 21 | the `Prox` module: 22 | 23 | ```scala mdoc:invisible 24 | import cats.effect._ 25 | import scala.concurrent.ExecutionContext 26 | import io.github.vigoo.prox._ 27 | ``` 28 | 29 | ```scala mdoc 30 | val prox = ProxFS2[IO] 31 | import prox._ 32 | ``` 33 | 34 | We require `F` to implement the `Concurrent` type class, and for that we have to have an implicit 35 | _context shifter_ in scope (this should be already available in an application using cats-effect). 36 | 37 | ### Defining a process to run 38 | In prox a process to be executed is defined by a pure value which implements the `Process[O, E]` trait. 39 | The type parameters have the following meaning: 40 | 41 | - `O` is the type of the output value after the system process has finished running 42 | - `E` is the type of the error output value after the system process has finished running 43 | 44 | To create a simple process to be executed use the `Process` constructor: 45 | 46 | ```scala mdoc 47 | val proc1 = Process("ls", List("-hal")) 48 | ``` 49 | 50 | or we can use the _string interpolator_: 51 | 52 | ```scala mdoc 53 | val proc2 = proc"ls -hal" 54 | ``` 55 | 56 | Then we can 57 | - [customize the process execution](customize) by for example setting environment variables and working directory 58 | - and [redirect the input, output and error](redirection) channels of the process 59 | - [pipe two or more processes together](processgroups) 60 | 61 | still staying on purely specification level. 62 | 63 | ### Running the process 64 | 65 | Once we have our process specification ready, we can _start_ the process with one of the 66 | IO functions on process. 67 | 68 | But for this we first have to have a `ProcessRunner` implementation in scope. The default 69 | one is called `JVMProcessRunner` and it can be created in the following way: 70 | 71 | ```scala mdoc:silent 72 | implicit val runner: ProcessRunner[JVMProcessInfo] = new JVMProcessRunner 73 | ``` 74 | 75 | Read the [custom process runners](custom-runners) page for an example of using a customized runner. 76 | 77 | With the runner in place we can use [several methods to start the process](running). 78 | The simplest one is called `run` and it blocks the active thread until the process finishes 79 | running: 80 | 81 | ```scala mdoc 82 | proc1.run() 83 | ``` 84 | 85 | The result of this IO action is a `ProcessResult[O, E]`, with the ability to observe the 86 | _exit code_ and the redirected output and error values. In our first example both `O` and 87 | `E` were `Unit` because the default is to redirect output and error to the _standard output_ and 88 | _standard error_ streams. 89 | -------------------------------------------------------------------------------- /docs/docs/docs/fs2/processgroups.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Process groups 4 | --- 5 | 6 | # Connecting processes together via pipes 7 | ```scala mdoc:invisible 8 | import cats.effect._ 9 | import scala.concurrent.ExecutionContext 10 | import io.github.vigoo.prox._ 11 | 12 | val prox = ProxFS2[IO] 13 | import prox._ 14 | ``` 15 | 16 | Connecting one process to another means that the standard output of the first process 17 | gets redirected to the standard input of the second process. This is implemented using 18 | the redirection capabilities described [on the redirection page](redirection). The result 19 | of connecting one process to another is called a _process group_ and it implements the 20 | trait `ProcessGroup[O, E]`. 21 | 22 | To create a process group, either: 23 | - Use the `|` or `via` methods between two **unbounded** processes 24 | - Use the `|` or `via` methods between an **unbounded** process group and an **unbounded** process 25 | 26 | It is important that the process group construction must always happen before any redirection, 27 | the type system enforces this by requiring the involved processes to be `UnboundedProcess`. 28 | 29 | > :bulb: `Process.UnboundedProcess` is a type alias for a process with all the redirection capabilities 30 | 31 | Let's see an example of simply piping: 32 | 33 | ```scala mdoc:silent 34 | val group1 = Process("grep", List("ERROR")) | Process("sort") 35 | val group2 = group1 | Process("uniq", List("-c")) 36 | ``` 37 | 38 | A custom pipe (when using `via`) can be anything of the type `Pipe[F, Byte, Byte]`. The 39 | following not very useful example capitalizes each word coming through: 40 | 41 | ```scala mdoc:silent 42 | val customPipe: fs2.Pipe[IO, Byte, Byte] = 43 | (s: fs2.Stream[IO, Byte]) => s 44 | .through(fs2.text.utf8.decode) // decode UTF-8 45 | .through(fs2.text.lines) // split to lines 46 | .map(_.split(' ').toVector) // split lines to words 47 | .map(v => v.map(_.capitalize).mkString(" ")) 48 | .intersperse("\n") // remerge lines 49 | .through(fs2.text.utf8.encode) // encode as UTF-8 50 | 51 | val group3 = Process("echo", List("hello world")).via(customPipe).to(Process("wc", List("-w"))) 52 | ``` -------------------------------------------------------------------------------- /docs/docs/docs/fs2/redirection.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Redirection 4 | --- 5 | 6 | # Redirecting input, output and error 7 | 8 | ```scala mdoc:invisible 9 | import cats.effect._ 10 | import scala.concurrent.ExecutionContext 11 | import io.github.vigoo.prox._ 12 | 13 | val prox = ProxFS2[IO] 14 | import prox._ 15 | ``` 16 | 17 | Similarly to [customization](customize), redirection is also implemented with _capability traits_. 18 | The `ProcessIO` type returned by the `Process` constructor implements all the three redirection capability 19 | traits: 20 | 21 | - `RedirectableInput` marks that the standard input of the process is not bound yet 22 | - `RedirectableOutput` marks that the standard output of the process is not bound yet 23 | - `RedirectableError` marks that the standard error output of the process is not bound yet 24 | 25 | Each of the three channels can be **only redirected once**. The result type of each redirection method no longer 26 | implements the given capability. 27 | 28 | Let's see an example of this (redirection methods are described below on this page): 29 | 30 | ```scala mdoc 31 | import cats.implicits._ 32 | 33 | val proc1 = Process("echo", List("Hello world")) 34 | val proc2 = proc1 ># fs2.text.utf8.decode 35 | ``` 36 | 37 | It is no longer possible to redirect the output of `proc2`: 38 | 39 | ```scala mdoc:fail 40 | val proc3 = proc2 >? fs2.text.utf8.decode[IO].andThen(fs2.text.lines) 41 | ``` 42 | 43 | Many redirection methods have an _operator_ version but all of them have alphanumberic 44 | variants as well. 45 | 46 | ### Input redirection 47 | Input redirection is enabled by the `RedirectableInput` trait. The following operations 48 | are supported: 49 | 50 | | operator | alternative | parameter type | what it does | 51 | |----------|--------------|----------------------|---------------| 52 | | `<` | `fromFile` | `java.nio.file.Path` | Natively attach a source file to STDIN | 53 | | `<` | `fromStream` | `Stream[F, Byte]` | Attach an _fs2 byte stream_ to STDIN | 54 | | `!<` | `fromStream` | `Stream[F, Byte]` | Attach an _fs2 byte stream_ to STDIN and **flush** after each chunk | 55 | 56 | ### Output redirection 57 | Output redirection is enabled by the `RedirectableOutput` trait. 58 | The following operations are supported: 59 | 60 | | operator | alternative | parameter type | result type | what it does | 61 | |----------|----------------|--------------------------------|-------------| --------------| 62 | | `>` | `toFile` | `java.nio.file.Path` | `Unit` | Natively attach STDOUT to a file | 63 | | `>>` | `appendToFile` | `java.nio.file.Path` | `Unit` | Natively attach STDOUT to a file in append mode | 64 | | `>` | `toSink` | `Pipe[F, Byte, Unit]` | `Unit` | Drains the STDOUT through the given pipe | 65 | | `>#` | `toFoldMonoid` | `[O: Monoid](Pipe[F, Byte, O]` | `O` | Sends STDOUT through the pipe and folds the result using its _monoid_ instance 66 | | `>?` | `toVector` | `Pipe[F, Byte, O]` | `Vector[O]` | Sends STDOUT through the pipe and collects the results | 67 | | | `drainOutput` | `Pipe[F, Byte, O]` | `Unit` | Drains the STDOUT through the given pipe | 68 | | | `foldOutput` | `Pipe[F, Byte, O], R, (R, O) => R` | `R` | Sends STDOUT through the pipe and folds the result using a custom fold function | 69 | 70 | ### Error redirection 71 | Error redirection is enabled by the `RedirectableError` trait. 72 | The following operations are supported: 73 | 74 | | operator | alternative | parameter type | result type | what it does | 75 | |-----------|---------------------|--------------------------------|-------------| --------------| 76 | | `!>` | `errorToFile` | `java.nio.file.Path` | `Unit` | Natively attach STDERR to a file | 77 | | `!>>` | `appendErrorToFile` | `java.nio.file.Path` | `Unit` | Natively attach STDERR to a file in append mode | 78 | | `!>` | `errorToSink` | `Pipe[F, Byte, Unit]` | `Unit` | Drains the STDERR through the given pipe | 79 | | `!>#` | `errorToFoldMonoid` | `[O: Monoid](Pipe[F, Byte, O]` | `O` | Sends STDERR through the pipe and folds the result using its _monoid_ instance 80 | | `!>?` | `errorToVector` | `Pipe[F, Byte, O]` | `Vector[O]` | Sends STDERR through the pipe and collects the results | 81 | | | `drainError` | `Pipe[F, Byte, O]` | `Unit` | Drains the STDERR through the given pipe | 82 | | | `foldError` | `Pipe[F, Byte, O], R, (R, O) => R` | `R` | Sends STDERR through the pipe and folds the result using a custom fold function | 83 | 84 | ### Redirection for process groups 85 | [Process groups](processgroups) are two or more processes attached together through pipes. 86 | This connection is internally implemented using the above described redirection capabilities. 87 | This means that all but the first process has their _inputs_ bound, and all but the last one has 88 | their _outputs_ bound. Redirection of input and output for a _process group_ is thus a well defined 89 | operation meaning redirection of input of the _first_ process and redirection of output of the _last process_. 90 | 91 | For this reason the class created via _process piping_ implements the `RedirectableInput` and 92 | `RedirectableOutput` traits described above. 93 | 94 | For the sake of simplicity the library does not support anymore the fully customizable 95 | per-process error redirection for process groups, but a reduced but still quite expressive 96 | version described by the `RedirectableErrors` trait. 97 | 98 | The methods in this trait define error redirection for **all process in the group at once**: 99 | 100 | | operator | alternative | parameter type | result type | what it does | 101 | |-----------|----------------------|--------------------------------|-------------| --------------| 102 | | `!>` | `errorsToSink` | `Pipe[F, Byte, Unit]` | `Unit` | Drains the STDERR through the given pipe | 103 | | `!>#` | `errorsToFoldMonoid` | `[O: Monoid](Pipe[F, Byte, O]` | `O` | Sends STDERR through the pipe and folds the result using its _monoid_ instance 104 | | `!>?` | `errorsToVector` | `Pipe[F, Byte, O]` | `Vector[O]` | Sends STDERR through the pipe and collects the results | 105 | | | `drainErrors` | `Pipe[F, Byte, O]` | `Unit` | Drains the STDERR through the given pipe | 106 | | | `foldErrors` | `Pipe[F, Byte, O], R, (R, O) => R` | `R` | Sends STDERR through the pipe and folds the result using a custom fold function | 107 | 108 | Redirection to file is not possible through this interface as only a single path could be 109 | provided. 110 | The result of these redirections is accessible through the `ProcessGroupResult` interface as 111 | it is described in the [running processes section](running). 112 | 113 | By using the `RedirectableErrors.customizedPerProcess` interface (having the type 114 | `RedirectableErrors.CustomizedPerProcess`) it is possible to customize the redirection 115 | targets per process while keeping their types uniform: 116 | 117 | | operator | alternative | parameter type | result type | what it does | 118 | |-----------|----------------------|-----------------------------------------------|-------------| --------------| 119 | | | `errorsToFile` | `Process => java.nio.file.Path` | `Unit` | Natively attach STDERR to a file | 120 | | | `appendErrorsToFile` | `Process => java.nio.file.Path` | `Unit` | Natively attach STDERR to a file in append mode | 121 | | | `errorsToSink` | `Process => Pipe[F, Byte, Unit]` | `Unit` | Drains the STDERR through the given pipe | 122 | | | `errorsToFoldMonoid` | `Process => [O: Monoid](Pipe[F, Byte, O]` | `O` | Sends STDERR through the pipe and folds the result using its _monoid_ instance 123 | | | `errorsToVector` | `Process => Pipe[F, Byte, O]` | `Vector[O]` | Sends STDERR through the pipe and collects the results | 124 | | | `drainErrors` | `Process => Pipe[F, Byte, O]` | `Unit` | Drains the STDERR through the given pipe | 125 | | | `foldErrors` | `Process => Pipe[F, Byte, O], R, (R, O) => R` | `R` | Sends STDERR through the pipe and folds the result using a custom fold function | 126 | 127 | Let's see an example of how this works! 128 | 129 | First we define a queue where we want to send _error lines_ from all the involved 130 | processes, then we define the two processes separately, connect them with a pipe and 131 | customize the error redirection where we prefix the parsed lines based on which 132 | process they came from: 133 | 134 | 135 | ```scala mdoc:silent 136 | import cats.effect.std.Queue 137 | 138 | for { 139 | errors <- Queue.unbounded[IO, String] 140 | parseLines = fs2.text.utf8.decode[IO].andThen(fs2.text.lines) 141 | 142 | p1 = Process("proc1") 143 | p2 = Process("proc2") 144 | group = (p1 | p2).customizedPerProcess.errorsToSink { 145 | case p if p == p1 => parseLines.andThen(_.map(s => "P1: " + s)).andThen(_.evalMap(errors.offer)) 146 | case p if p == p2 => parseLines.andThen(_.map(s => "P2: " + s)).andThen(_.evalMap(errors.offer)) 147 | } 148 | } yield () 149 | ``` 150 | 151 | ### Creating reusable functions 152 | The `Process` object contains several useful _type aliases_ for writing functions that work with any process by 153 | only specifying what redirection channels we want _unbounded_. 154 | 155 | The `UnboundProcess` represents a process which is fully unbound, no redirection has been done yet. It is 156 | defined as follows: 157 | 158 | ```scala 159 | type UnboundProcess = Process[Unit, Unit] 160 | with RedirectableInput[UnboundOEProcess] 161 | with RedirectableOutput[UnboundIEProcess[*]] 162 | with RedirectableError[UnboundIOProcess[*]] 163 | ``` 164 | 165 | where `UnboundIOProcess[E]` for example represents a process which has its _error output_ already bound. 166 | 167 | These type aliases can be used to define functions performing redirection on arbitrary processes, for example: 168 | 169 | ```scala mdoc 170 | def logErrors[P <: Process.UnboundEProcess[_]](proc: P) = { 171 | val target = fs2.text.utf8.decode[IO].andThen(fs2.text.lines).andThen(_.evalMap(line => IO(println(line)))) 172 | proc !> target 173 | } 174 | 175 | val proc4 = logErrors(Process("something")) 176 | ``` -------------------------------------------------------------------------------- /docs/docs/docs/fs2/running.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Running processes 4 | --- 5 | 6 | # Running processes and process groups 7 | ```scala mdoc:invisible 8 | import io.github.vigoo.prox.zstream._ 9 | ``` 10 | 11 | There are three methods for running a _process_: 12 | 13 | - The `run` method is the simplest one, it starts the process and then blocks the current fiber until it terminates 14 | - The `start` method starts the process and returns a fiber packed into a resource. The fiber finishes when the process terminates. Canceling the fiber terminates the process. 15 | - The `startProcess` method returns a `RunningProcess[O, E]` interface that allows advanced some operations 16 | 17 | Similarly for a _process group_, there is a `run`, a `start` and a `startProcessGroup` method but with different result types. 18 | 19 | Let's see some examples! 20 | 21 | ```scala mdoc:silent 22 | implicit val runner: ProcessRunner[JVMProcessInfo] = new JVMProcessRunner 23 | 24 | val process = Process("echo", List("hello")) 25 | 26 | val result1 = process.run() 27 | val result2 = process.start().flatMap { fiber => 28 | fiber.join 29 | } 30 | 31 | val result3 = 32 | for { 33 | runningProcess <- process.startProcess() 34 | _ <- runningProcess.kill() 35 | } yield () 36 | ``` 37 | 38 | Both `RunningProcess` and `RunningProcessGroup` has the following methods: 39 | - `waitForExit()` waits until the process terminates 40 | - `terminate()` sends `SIGTERM` to the process 41 | - `kill()` sends `SIGKILL` to the process 42 | 43 | In addition `RunningProcess` also defines an `isAlive` check. 44 | 45 | ### Process execution result 46 | The result of a process is represented by `ProcessResult[O, E]` defined as follows: 47 | 48 | ```scala 49 | trait ProcessResult[+O, +E] { 50 | val exitCode: ExitCode 51 | val output: O 52 | val error: E 53 | } 54 | ``` 55 | 56 | The type and value of `output` and `error` depends on what [redirection was defined](redirection) on the process. 57 | 58 | ### Process group execution result 59 | The result of a process group is represented by `ProcessGroupResult[O, E]`: 60 | 61 | ```scala 62 | trait ProcessGroupResult[+O, +E] { 63 | val exitCodes: Map[Process[Unit, Unit], ExitCode] 64 | val output: O 65 | val errors: Map[Process[Unit, Unit], E] 66 | } 67 | ``` 68 | 69 | The keys of the maps are the original _process_ values used in the piping operations. -------------------------------------------------------------------------------- /docs/docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Getting started 4 | --- 5 | 6 | Prox has two different interfaces: 7 | - [Cats Effect with FS2](fs2/index) 8 | - [ZIO with ZStream](zstream/index) 9 | 10 | -------------------------------------------------------------------------------- /docs/docs/docs/migration.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Migration 4 | --- 5 | # Migration 6 | 7 | ### from 0.1.x to 0.2 8 | 9 | - The `start` method on processes now requires a `blockingExecutionContext` argument 10 | - `Ignore` has been renamed to `Drain` 11 | - `Log` has been renamed to `ToVector` 12 | 13 | ### from 0.2 to 0.4 14 | 15 | - `Process` now takes the effect type as parameter, so in case of cats-effect, `Process(...)` becomes `Process[IO](...)` 16 | - The `start` method on processes now gets a `Blocker` instead of an execution context 17 | 18 | ### from 0.4 to 0.5 19 | 20 | 0.5 is a complete rewrite of the original library, and the API changed a lot, especially 21 | if the process types were used in code to pass around / wrap them. Please refer to the other 22 | sections of the documentation to learn how to reimplement them. For simple use cases where 23 | constructing and running the processes directly the main differences are: 24 | 25 | - Different operators / methods for different source and target types, see [the page about redirection](redirection) 26 | - The need of an implicit [process runner](running) in scope 27 | - New ways to start and wait for the process, see [the page about runnning processes](running) 28 | 29 | ### from 0.5 to 0.6 30 | 31 | 0.6 introduces the native ZIO/ZStream version of the library. For existing code the following differences apply: 32 | 33 | - Instead of `prox`, the artifact is now called `prox-fs2` 34 | - Instead of _global imports_, the FS2 prox module now has to be constructed with the `FS2` constructor and the API is imported from that 35 | - Because the `FS2` module captures the `F[_]` and the `Blocker`, they are no longer needed to pass on to the API functions and types 36 | -------------------------------------------------------------------------------- /docs/docs/docs/zstream/custom-runners.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Custom runners 4 | --- 5 | 6 | # Customizing the runner 7 | 8 | ```scala mdoc:invisible 9 | import zio._ 10 | import zio.stream._ 11 | import io.github.vigoo.prox._ 12 | import io.github.vigoo.prox.zstream._ 13 | ``` 14 | 15 | The _runner_ is responsible for stating the native processes and wiring all the redirections together. The default 16 | implementation is called `JVMProcessRunner`. 17 | 18 | There are use cases when providing a custom runner makes sense. One such use case could be to launch external processes 19 | within a docker container in case of running on a development machine (for example from tests), while running them directly 20 | in production, when the whole service is running within the container. 21 | 22 | We can implement this scenario by using `JVMProcessRunner` in production and a custom `DockerizedProcessRunner` in tests, 23 | where we define the latter as follows: 24 | 25 | ```scala mdoc 26 | import java.nio.file.Path 27 | import java.util.UUID 28 | 29 | case class DockerImage(name: String) 30 | 31 | case class DockerContainer(name: String) 32 | 33 | case class DockerProcessInfo[DockerProcessInfo](container: DockerContainer, dockerProcessInfo: DockerProcessInfo) 34 | 35 | class DockerizedProcessRunner[Info](processRunner: ProcessRunner[Info], 36 | mountedDirectory: Path, 37 | workingDirectory: Path, 38 | image: DockerImage) 39 | extends ProcessRunner[DockerProcessInfo[Info]] { 40 | 41 | override def startProcess[O, E](process: Process[O, E]): ZIO[Any, ProxError, RunningProcess[O, E, DockerProcessInfo[Info]]] = { 42 | for { 43 | container <- generateContainerName 44 | runningProcess <- processRunner 45 | .startProcess(wrapInDocker(process, container)) 46 | } yield runningProcess.mapInfo(info => DockerProcessInfo(container, info)) 47 | } 48 | 49 | override def startProcessGroup[O, E](processGroup: ProcessGroup[O, E]): ZIO[Any, ProxError, RunningProcessGroup[O, E, DockerProcessInfo[Info]]] = { 50 | ZIO.foreach(processGroup.originalProcesses.toVector)(key => generateContainerName.map(c => key -> c)).flatMap { keyAndNames => 51 | val nameMap = keyAndNames.toMap 52 | val names = keyAndNames.map(_._2) 53 | val modifiedProcessGroup = processGroup.map(new ProcessGroup.Mapper[O, E] { 54 | def mapFirst[P <: Process[ZStream[Any, ProxError, Byte], E]](process: P): P = wrapInDocker(process, names.head).asInstanceOf[P] 55 | def mapInnerWithIdx[P <: Process.UnboundIProcess[ZStream[Any, ProxError, Byte], E]](process: P, idx: Int): P = 56 | wrapInDocker(process, names(idx)).asInstanceOf[P] 57 | def mapLast[P <: Process.UnboundIProcess[O, E]](process: P): P = wrapInDocker(process, names.last).asInstanceOf[P] 58 | }) 59 | processRunner.startProcessGroup(modifiedProcessGroup) 60 | .map(_.mapInfo { case (key, info) => DockerProcessInfo(nameMap(key), info) }) 61 | } 62 | } 63 | 64 | private def generateContainerName: ZIO[Any, ProxError, DockerContainer] = 65 | ZIO.attempt(DockerContainer(UUID.randomUUID().toString)).mapError(UnknownProxError) 66 | 67 | private def wrapInDocker[O, E](process: Process[O, E], container: DockerContainer): Process[O, E] = { 68 | val envVars = process.environmentVariables.flatMap { case (key, value) => List("-e", s"$key=$value") }.toList 69 | process.withCommand("docker").withArguments( 70 | "run" :: 71 | "--name" :: container.name :: 72 | "-v" :: mountedDirectory.toString :: 73 | "-w" :: workingDirectory.toString :: 74 | envVars ::: 75 | List(image.name, process.command) ::: 76 | process.arguments 77 | ) 78 | } 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/docs/docs/zstream/customize.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Customizing environment 4 | --- 5 | 6 | # Customizing the environment 7 | 8 | ```scala mdoc:invisible 9 | import io.github.vigoo.prox.zstream._ 10 | import io.github.vigoo.prox._ 11 | ``` 12 | 13 | The type returned by the `Process` constructor also implements the `ProcessConfiguration` trait, 14 | adding three methods that can be used to customize the working environment of the process to be started: 15 | 16 | ### Working directory 17 | 18 | The `in` method can be used to customize the working directory: 19 | 20 | ```scala mdoc 21 | import io.github.vigoo.prox.path._ 22 | 23 | val dir = home / "tmp" 24 | val proc1 = Process("ls") in dir 25 | ``` 26 | 27 | Not that `dir` has the type `java.nio.file.Path`, and the `home / tmp` syntax is just a thin 28 | syntax extension to produce such values. 29 | 30 | ### Adding environment variables 31 | 32 | The `with` method can be used to add environment variables to the process in the following 33 | way: 34 | 35 | ```scala mdoc 36 | val proc2 = Process("echo", List("$TEST")) `with` ("TEST" -> "Hello world") 37 | ``` 38 | 39 | ### Removing environment variables 40 | 41 | The subprocess inherits the parent process environment, so it may be necessary to 42 | _remove_ some already defined environment variables with the `without` method: 43 | 44 | ```scala mdoc 45 | val proc3 = Process("echo" , List("$PATH")) `without` "PATH" 46 | ``` 47 | 48 | ### Writing reusable functions 49 | 50 | Because these methods are part of the `ProcessConfiguration` _capability_, writing reusable functions require us to define 51 | a polymorphic function that requires this capability: 52 | 53 | ```scala mdoc 54 | import java.nio.file.Path 55 | 56 | def withHome[P <: ProcessLike with ProcessLikeConfiguration](home: Path, proc: P): P#Self = 57 | proc `with` ("HOME" -> home.toString) 58 | ``` 59 | 60 | Then we can use it on any kind of process or process group (read about [redirection](redirection) to understand 61 | why there are multiple concrete process types): 62 | 63 | ```scala mdoc 64 | val proc4 = Process("echo", List("$HOME")) 65 | val proc5 = withHome(home, proc4) 66 | 67 | val group1 = Process("grep", List("ERROR")) | Process("sort") 68 | val group2 = withHome(home, group1) 69 | ``` -------------------------------------------------------------------------------- /docs/docs/docs/zstream/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Getting started 4 | --- 5 | 6 | # Getting started with prox 7 | 8 | First add one of the `prox` interfaces as a dependency: 9 | 10 | ```sbt 11 | libraryDependencies += "io.github.vigoo" %% "prox-zstream" % "0.7.3" 12 | ``` 13 | 14 | and import the ZIO specific API from: 15 | 16 | ```scala mdoc 17 | import io.github.vigoo.prox._ 18 | import io.github.vigoo.prox.zstream._ 19 | ``` 20 | 21 | There is also an experimental version for ZIO 2, based on it's snapshot releases: 22 | 23 | ```sbt 24 | libraryDependencies += "io.github.vigoo" %% "prox-zstream-2" % "0.7.3" 25 | ``` 26 | 27 | The code snippets in the documentation are based on the ZIO 1 version. 28 | 29 | ### Defining a process to run 30 | In prox a process to be executed is defined by a pure value which implements the `Process[O, E]` trait. 31 | The type parameters have the following meaning: 32 | 33 | - `O` is the type of the output value after the system process has finished running 34 | - `E` is the type of the error output value after the system process has finished running 35 | 36 | To create a simple process to be executed use the `Process` constructor: 37 | 38 | ```scala mdoc 39 | val proc1 = Process("ls", List("-hal")) 40 | ``` 41 | 42 | or we can use the _string interpolator_: 43 | 44 | ```scala mdoc 45 | val proc2 = proc"ls -hal" 46 | ``` 47 | 48 | Then we can 49 | - [customize the process execution](customize) by for example setting environment variables and working directory 50 | - and [redirect the input, output and error](redirection) channels of the process 51 | - [pipe two or more processes together](processgroups) 52 | 53 | still staying on purely specification level. 54 | 55 | ### Running the process 56 | 57 | Once we have our process specification ready, we can _start_ the process with one of the 58 | IO functions on process. 59 | 60 | But for this we first have to have a `ProcessRunner` implementation in scope. The default 61 | one is called `JVMProcessRunner` and it can be created in the following way: 62 | 63 | ```scala mdoc:silent 64 | implicit val runner: ProcessRunner[JVMProcessInfo] = new JVMProcessRunner 65 | ``` 66 | 67 | Read the [custom process runners](custom-runners) page for an example of using a customized runner. 68 | 69 | With the runner in place we can use [several methods to start the process](running). 70 | The simplest one is called `run` and it blocks the active thread until the process finishes 71 | running: 72 | 73 | ```scala mdoc 74 | proc1.run() 75 | ``` 76 | 77 | The result of this IO action is a `ProcessResult[O, E]`, with the ability to observe the 78 | _exit code_ and the redirected output and error values. In our first example both `O` and 79 | `E` were `Unit` because the default is to redirect output and error to the _standard output_ and 80 | _standard error_ streams. 81 | -------------------------------------------------------------------------------- /docs/docs/docs/zstream/processgroups.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Process groups 4 | --- 5 | 6 | # Connecting processes together via pipes 7 | ```scala mdoc:invisible 8 | import zio._ 9 | import zio.stream._ 10 | import zio.prelude._ 11 | import io.github.vigoo.prox._ 12 | import io.github.vigoo.prox.zstream._ 13 | import java.nio.charset.StandardCharsets 14 | ``` 15 | 16 | Connecting one process to another means that the standard output of the first process 17 | gets redirected to the standard input of the second process. This is implemented using 18 | the redirection capabilities described [on the redirection page](redirection). The result 19 | of connecting one process to another is called a _process group_ and it implements the 20 | trait `ProcessGroup[O, E]`. 21 | 22 | To create a process group, either: 23 | - Use the `|` or `via` methods between two **unbounded** processes 24 | - Use the `|` or `via` methods between an **unbounded** process group and an **unbounded** process 25 | 26 | It is important that the process group construction must always happen before any redirection, 27 | the type system enforces this by requiring the involved processes to be `UnboundedProcess`. 28 | 29 | > :bulb: `Process.UnboundedProcess` is a type alias for a process with all the redirection capabilities 30 | 31 | Let's see an example of simply pipeing: 32 | 33 | ```scala mdoc:silent 34 | val group1 = Process("grep", List("ERROR")) | Process("sort") 35 | val group2 = group1 | Process("uniq", List("-c")) 36 | ``` 37 | 38 | A custom pipe (when using `via`) can be anything of the type `ZStream[any, ProxError, Byte] => ZStream[any, ProxError, Byte])`. 39 | The following not very useful example capitalizes each word coming through: 40 | 41 | ```scala mdoc:silent 42 | val customPipe: ProxPipe[Byte, Byte] = 43 | (s: ZStream[Any, ProxError, Byte]) => s 44 | .via(ZPipeline.utf8Decode.mapError(UnknownProxError.apply)) // decode UTF-8 45 | .via(ZPipeline.splitLines) // split to lines 46 | .map(_.split(' ').toVector) // split lines to words 47 | .map(v => v.map(_.capitalize).mkString(" ")) 48 | .intersperse("\n") // remerge lines 49 | .flatMap(str => ZStream.fromIterable(str.getBytes(StandardCharsets.UTF_8))) // reencode 50 | 51 | val group3 = Process("echo", List("hello world")).via(customPipe).to(Process("wc", List("-w"))) 52 | ``` -------------------------------------------------------------------------------- /docs/docs/docs/zstream/redirection.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Redirection 4 | --- 5 | 6 | # Redirecting input, output and error 7 | 8 | ```scala mdoc:invisible 9 | import io.github.vigoo.prox._ 10 | import io.github.vigoo.prox.zstream._ 11 | ``` 12 | 13 | Similarly to [customization](customize), redirection is also implemented with _capability traits_. 14 | The `ProcessIO` type returned by the `Process` constructor implements all the three redirection capability 15 | traits: 16 | 17 | - `RedirectableInput` marks that the standard input of the process is not bound yet 18 | - `RedirectableOutput` marks that the standard output of the process is not bound yet 19 | - `RedirectableError` marks that the standard error output of the process is not bound yet 20 | 21 | Each of the three channels can be **only redirected once**. The result type of each redirection method no longer 22 | implements the given capability. 23 | 24 | Let's see an example of this (redirection methods are described below on this page): 25 | 26 | ```scala mdoc 27 | import zio._ 28 | import zio.stream._ 29 | import zio.prelude._ 30 | 31 | val proc1 = Process("echo", List("Hello world")) 32 | val proc2 = proc1 ># ZPipeline.utf8Decode 33 | ``` 34 | 35 | It is no longer possible to redirect the output of `proc2`: 36 | 37 | ```scala mdoc:fail 38 | val proc3 = proc2 >? (ZPipeline.utf8Decode >>> ZPipeline.splitLines) 39 | ``` 40 | 41 | Many redirection methods have an _operator_ version but all of them have alphanumberic 42 | variants as well. 43 | 44 | ### Input redirection 45 | Input redirection is enabled by the `RedirectableInput` trait. The following operations 46 | are supported: 47 | 48 | | operator | alternative | parameter type | what it does | 49 | |----------|--------------|---------------------------------|---------------| 50 | | `<` | `fromFile` | `java.nio.file.Path` | Natively attach a source file to STDIN | 51 | | `<` | `fromStream` | `ZStream[Any, ProxError, Byte]` | Attach a _ZIO byte stream_ to STDIN | 52 | | `!<` | `fromStream` | `ZStream[Any, ProxError, Byte]` | Attach a _ZIO byte stream_ to STDIN and **flush** after each chunk | 53 | 54 | ### Output redirection 55 | Output redirection is enabled by the `RedirectableOutput` trait. 56 | The following operations are supported: 57 | 58 | | operator | alternative | parameter type | result type | what it does | 59 | |----------|----------------|--------------------------------------------------------------------------------|-------------| --------------| 60 | | `>` | `toFile` | `java.nio.file.Path` | `Unit` | Natively attach STDOUT to a file | 61 | | `>>` | `appendToFile` | `java.nio.file.Path` | `Unit` | Natively attach STDOUT to a file in append mode | 62 | | `>` | `toSink` | `TransformAndSink[Byte, _]` | `Unit` | Drains the STDOUT through the given sink | 63 | | `>#` | `toFoldMonoid` | `[O: Identity](ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `O` | Sends STDOUT through the stream and folds the result using its _monoid_ instance 64 | | `>?` | `toVector` | `ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `Vector[O]` | Sends STDOUT through the stream and collects the results | 65 | | | `drainOutput` | `ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `Unit` | Drains the STDOUT through the given stream | 66 | | | `foldOutput` | `ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O]), R, (R, O) => R` | `R` | Sends STDOUT through the stream and folds the result using a custom fold function | 67 | 68 | All the variants that accept a _stream transformation_ (`ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])`) are also usable by directly passing 69 | a `ZPipeline`. 70 | 71 | `TransformAndSink` encapsulates a _stream transformation_ and a _unit sink_. It is possible to use a sink directly if transformation is not needed. 72 | 73 | ```scala 74 | case class TransformAndSink[A, B](transform: ZStream[Any, ProxError, A] => ZStream[Any, ProxError, B], 75 | sink: ZSink[Any, ProxError, B, Any, Unit]) 76 | ``` 77 | 78 | ### Error redirection 79 | Error redirection is enabled by the `RedirectableError` trait. 80 | The following operations are supported: 81 | 82 | | operator | alternative | parameter type | result type | what it does | 83 | |-----------|---------------------|--------------------------------------------------------------------------------|-------------| --------------| 84 | | `!>` | `errorToFile` | `java.nio.file.Path` | `Unit` | Natively attach STDERR to a file | 85 | | `!>>` | `appendErrorToFile` | `java.nio.file.Path` | `Unit` | Natively attach STDERR to a file in append mode | 86 | | `!>` | `errorToSink` | `TransformAndSink[Byte, _]` | `Unit` | Drains the STDERR through the given sink | 87 | | `!>#` | `errorToFoldMonoid` | `[O: Monoid](ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `O` | Sends STDERR through the pipe and folds the result using its _monoid_ instance 88 | | `!>?` | `errorToVector` | `ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `Vector[O]` | Sends STDERR through the pipe and collects the results | 89 | | | `drainError` | `ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `Unit` | Drains the STDERR through the given pipe | 90 | | | `foldError` | `ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O]), R, (R, O) => R` | `R` | Sends STDERR through the pipe and folds the result using a custom fold function | 91 | 92 | ### Redirection for process groups 93 | [Process groups](processgroups) are two or more processes attached together through pipes. 94 | This connection is internally implemented using the above described redirection capabilities. 95 | This means that all but the first process has their _inputs_ bound, and all but the last one has 96 | their _outputs_ bound. Redirection of input and output for a _process group_ is thus a well defined 97 | operation meaning redirection of input of the _first_ process and redirection of output of the _last process_. 98 | 99 | For this reason the class created via _process piping_ implements the `RedirectableInput` and 100 | `RedirectableOutput` traits described above. 101 | 102 | For the sake of simplicity the library does not support anymore the fully customizable 103 | per-process error redirection for process groups, but a reduced but still quite expressive 104 | version described by the `RedirectableErrors` trait. 105 | 106 | The methods in this trait define error redirection for **all process in the group at once**: 107 | 108 | | operator | alternative | parameter type | result type | what it does | 109 | |-----------|----------------------|--------------------------------------------------------------------------------|-------------| --------------| 110 | | `!>` | `errorsToSink` | `TransformAndSink[Byte, _]` | `Unit` | Drains the STDERR through the given sink | 111 | | `!>#` | `errorsToFoldMonoid` | `[O: Monoid](ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `O` | Sends STDERR through the stream and folds the result using its _monoid_ instance 112 | | `!>?` | `errorsToVector` | `ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `Vector[O]` | Sends STDERR through the stream and collects the results | 113 | | | `drainErrors` | `ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `Unit` | Drains the STDERR through the given stream | 114 | | | `foldErrors` | `ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O]), R, (R, O) => R` | `R` | Sends STDERR through the stream and folds the result using a custom fold function | 115 | 116 | Redirection to file is not possible through this interface as only a single path could be 117 | provided. 118 | The result of these redirections is accessible through the `ProcessGroupResult` interface as 119 | it is described in the [running processes section](running). 120 | 121 | By using the `RedirectableErrors.customizedPerProcess` interface (having the type 122 | `RedirectableErrors.CustomizedPerProcess`) it is possible to customize the redirection 123 | targets per process while keeping their types uniform: 124 | 125 | | operator | alternative | parameter type | result type | what it does | 126 | |-----------|----------------------|-------------------------------------------------------------------------------------------|-------------| --------------| 127 | | | `errorsToFile` | `Process => java.nio.file.Path` | `Unit` | Natively attach STDERR to a file | 128 | | | `appendErrorsToFile` | `Process => java.nio.file.Path` | `Unit` | Natively attach STDERR to a file in append mode | 129 | | | `errorsToSink` | `Process => TransformAndSink[Byte, _]` | `Unit` | Drains the STDERR through the given sink | 130 | | | `errorsToFoldMonoid` | `Process => [O: Monoid](ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `O` | Sends STDERR through the stream and folds the result using its _monoid_ instance 131 | | | `errorsToVector` | `Process => ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `Vector[O]` | Sends STDERR through the stream and collects the results | 132 | | | `drainErrors` | `Process => ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O])` | `Unit` | Drains the STDERR through the given stream | 133 | | | `foldErrors` | `Process => ZStream[Any, ProxError, Byte] => ZStream[Any, ProxError, O]), R, (R, O) => R` | `R` | Sends STDERR through the stream and folds the result using a custom fold function | 134 | 135 | Let's see an example of how this works! 136 | 137 | First we define a queue where we want to send _error lines_ from all the involved 138 | processes, then we define the two processes separately, connect them with a pipe and 139 | customize the error redirection where we prefix the parsed lines based on which 140 | process they came from: 141 | 142 | 143 | ```scala mdoc:silent 144 | 145 | for { 146 | errors <- Queue.unbounded[String] 147 | parseLines = (s: ZStream[Any, ProxError, Byte]) => s.via(ZPipeline.utf8Decode.mapError(UnknownProxError.apply) >>> ZPipeline.splitLines) 148 | 149 | p1 = Process("proc1") 150 | p2 = Process("proc2") 151 | group = (p1 | p2).customizedPerProcess.errorsToSink { 152 | case p if p == p1 => TransformAndSink(parseLines.andThen(_.map(s => "P1: " + s)), ZSink.foreach(errors.offer)) 153 | case p if p == p2 => TransformAndSink(parseLines.andThen(_.map(s => "P2: " + s)), ZSink.foreach(errors.offer)) 154 | } 155 | } yield () 156 | ``` 157 | 158 | ### Creating reusable functions 159 | The `Process` object contains several useful _type aliases_ for writing functions that work with any process by 160 | only specifying what redirection channels we want _unbounded_. 161 | 162 | The `UnboundProcess` represents a process which is fully unbound, no redirection has been done yet. It is 163 | defined as follows: 164 | 165 | ```scala 166 | type UnboundProcess = Process[Unit, Unit] 167 | with RedirectableInput[UnboundOEProcess] 168 | with RedirectableOutput[UnboundIEProcess[*]] 169 | with RedirectableError[UnboundIOProcess[*]] 170 | ``` 171 | 172 | where `UnboundIOProcess[E]` for example represents a process which has its _error output_ already bound. 173 | 174 | These type aliases can be used to define functions performing redirection on arbitrary processes, for example: 175 | 176 | ```scala mdoc 177 | def logErrors[P <: Process.UnboundEProcess[_]](proc: P) = { 178 | val target = TransformAndSink( 179 | ZPipeline.utf8Decode.mapError(UnknownProxError.apply) >>> ZPipeline.splitLines, 180 | ZSink.foreach((line: String) => ZIO.debug(line))) 181 | proc !> target 182 | } 183 | 184 | val proc4 = logErrors(Process("something")) 185 | ``` -------------------------------------------------------------------------------- /docs/docs/docs/zstream/running.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Running processes 4 | --- 5 | 6 | # Running processes and process groups 7 | ```scala mdoc:invisible 8 | import zio._ 9 | import io.github.vigoo.prox._ 10 | import io.github.vigoo.prox.zstream._ 11 | ``` 12 | 13 | There are three methods for running a _process_: 14 | 15 | - The `run` method is the simplest one, it starts the process and then blocks the current fiber until it terminates 16 | - The `start` method starts the process and returns a fiber packed into a resource. The fiber finishes when the process terminates. Canceling the fiber terminates the process. 17 | - The `startProcess` method returns a `RunningProcess[O, E]` interface that allows advanced some operations 18 | 19 | Similarly for a _process group_, there is a `run`, a `start` and a `startProcessGroup` method but with different result types. 20 | 21 | Let's see some examples! 22 | 23 | ```scala mdoc:silent 24 | implicit val runner: ProcessRunner[JVMProcessInfo] = new JVMProcessRunner 25 | 26 | val process = Process("echo", List("hello")) 27 | 28 | val result1 = process.run() 29 | val result2 = ZIO.scoped { 30 | process.start().flatMap { fiber => 31 | fiber.join 32 | } 33 | } 34 | 35 | val result3 = 36 | for { 37 | runningProcess <- process.startProcess() 38 | _ <- runningProcess.kill() 39 | } yield () 40 | ``` 41 | 42 | Both `RunningProcess` and `RunningProcessGroup` has the following methods: 43 | - `waitForExit()` waits until the process terminates 44 | - `terminate()` sends `SIGTERM` to the process 45 | - `kill()` sends `SIGKILL` to the process 46 | 47 | In addition `RunningProcess` also defines an `isAlive` check. 48 | 49 | ### Process execution result 50 | The result of a process is represented by `ProcessResult[O, E]` defined as follows: 51 | 52 | ```scala 53 | trait ProcessResult[+O, +E] { 54 | val exitCode: ExitCode 55 | val output: O 56 | val error: E 57 | } 58 | ``` 59 | 60 | The type and value of `output` and `error` depends on what [redirection was defined](redirection) on the process. 61 | 62 | ### Process group execution result 63 | The result of a process group is represented by `ProcessGroupResult[O, E]`: 64 | 65 | ```scala 66 | trait ProcessGroupResult[+O, +E] { 67 | val exitCodes: Map[Process[Unit, Unit], ExitCode] 68 | val output: O 69 | val errors: Map[Process[Unit, Unit], E] 70 | } 71 | ``` 72 | 73 | The keys of the maps are the original _process_ values used in the piping operations. -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: homeFeatures 3 | title: "prox: Home" 4 | features: 5 | - first: ["Type safe", "Define the execution of one or more system processes in a type safe way"] 6 | - second: ["Purely functional", "Compose the process execution as an IO effect"] 7 | - third: ["Streaming", "Redirect input, output and error to/from functional streams"] 8 | --- 9 | 10 | Prox is a Scala library for running system processes, plugging them to each other and redirecting them to streams. 11 | -------------------------------------------------------------------------------- /docs/src/microsite/data/menu.yml: -------------------------------------------------------------------------------- 1 | options: 2 | - title: Home 3 | url: index 4 | - title: Cats Effect / FS2 5 | url: docs/fs2/index.html 6 | menu_section: fs2 7 | 8 | nested_options: 9 | - title: Getting started 10 | url: docs/fs2/index.html 11 | - title: Customizing environment 12 | url: docs/fs2/customize.html 13 | - title: Redirection 14 | url: docs/fs2/redirection.html 15 | - title: Process groups 16 | url: docs/fs2/processgroups.html 17 | - title: Running processes 18 | url: docs/fs2/running.html 19 | - title: Custom runners 20 | url: docs/fs2/custom-runners.html 21 | 22 | - title: ZIO / ZStream 23 | url: docs/zstream/index.html 24 | menu_section: zstream 25 | 26 | nested_options: 27 | - title: Getting started 28 | url: docs/zstream/index.html 29 | - title: Customizing environment 30 | url: docs/zstream/customize.html 31 | - title: Redirection 32 | url: docs/zstream/redirection.html 33 | - title: Process groups 34 | url: docs/zstream/processgroups.html 35 | - title: Running processes 36 | url: docs/zstream/running.html 37 | - title: Custom runners 38 | url: docs/zstream/custom-runners.html 39 | 40 | - title: Migration 41 | url: docs/migration.html 42 | menu_section: migration 43 | - title: Blog posts 44 | url: docs/blogposts.html 45 | menu_section: blogposts 46 | - title: API reference 47 | url: api/io/github/vigoo/prox/ 48 | menu_section: apireference -------------------------------------------------------------------------------- /docs/src/microsite/img/second-feature-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/src/microsite/img/third-feature-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 70 | 72 | 73 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | 95 | 96 | 98 | 99 | 100 | 101 | 102 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /examples/externalpyproc/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | IFS=$'\n\t' 5 | 6 | rm -rf virtualenv 7 | virtualenv --setuptools --no-site-packages -p python2.7 virtualenv 8 | -------------------------------------------------------------------------------- /examples/externalpyproc/test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def run(): 4 | stop = False 5 | 6 | while not stop: 7 | line = sys.stdin.readline().strip() 8 | 9 | if len(line) == 0: 10 | stop = True 11 | else: 12 | print line + "!?!?" 13 | sys.stdout.flush() 14 | 15 | run() 16 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.9.9 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2") 2 | addSbtPlugin("com.47deg" % "sbt-microsites" % "1.4.4") 3 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.7.0") 4 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.6.1") 5 | addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") 6 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 7 | 8 | ThisBuild / libraryDependencySchemes ++= Seq( 9 | "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always 10 | ) 11 | -------------------------------------------------------------------------------- /prox-core/src/main/scala/io/github/vigoo/prox/common.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox 2 | 3 | import java.nio.file.Path 4 | 5 | trait CommonModule { 6 | this: Prox => 7 | 8 | trait ProcessLikeConfiguration { 9 | val workingDirectory: Option[Path] 10 | val environmentVariables: Map[String, String] 11 | val removedEnvironmentVariables: Set[String] 12 | 13 | type Self <: ProcessLikeConfiguration 14 | 15 | protected def applyConfiguration( 16 | workingDirectory: Option[Path], 17 | environmentVariables: Map[String, String], 18 | removedEnvironmentVariables: Set[String] 19 | ): Self 20 | 21 | /** Changes the working directory of the process 22 | * 23 | * @param workingDirectory 24 | * the working directory 25 | * @return 26 | * a new process with the working directory set 27 | */ 28 | def in(workingDirectory: Path): Self = 29 | applyConfiguration( 30 | workingDirectory = Some(workingDirectory), 31 | environmentVariables, 32 | removedEnvironmentVariables 33 | ) 34 | 35 | /** Use the inherited working directory of the process instead of an 36 | * explicit one 37 | * 38 | * @return 39 | * a new process with the working directory cleared 40 | */ 41 | def inInheritedWorkingDirectory(): Self = 42 | applyConfiguration( 43 | workingDirectory = None, 44 | environmentVariables, 45 | removedEnvironmentVariables 46 | ) 47 | 48 | /** Adds an environment variable to the process 49 | * 50 | * @param nameValuePair 51 | * A pair of name and value 52 | * @return 53 | * a new process with the working directory set 54 | */ 55 | def `with`(nameValuePair: (String, String)): Self = 56 | applyConfiguration( 57 | workingDirectory, 58 | environmentVariables = environmentVariables + nameValuePair, 59 | removedEnvironmentVariables 60 | ) 61 | 62 | /** Removes an environment variable from the process 63 | * 64 | * Usable to remove variables inherited from the parent process. 65 | * 66 | * @param name 67 | * Name of the environment variable 68 | * @return 69 | * a new process with the working directory set 70 | */ 71 | def without(name: String): Self = 72 | applyConfiguration( 73 | workingDirectory, 74 | environmentVariables, 75 | removedEnvironmentVariables = removedEnvironmentVariables + name 76 | ) 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /prox-core/src/main/scala/io/github/vigoo/prox/errors.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox 2 | 3 | sealed trait ProxError { 4 | def toThrowable: Throwable 5 | } 6 | 7 | final case class FailedToReadProcessOutputException(reason: Throwable) 8 | extends Exception(s"Failed to read process output", reason) 9 | final case class FailedToReadProcessOutput(reason: Throwable) 10 | extends ProxError { 11 | override def toThrowable: Throwable = FailedToReadProcessOutputException( 12 | reason 13 | ) 14 | } 15 | 16 | final case class FailedToWriteProcessInputException(reason: Throwable) 17 | extends Exception(s"Failed to write process input", reason) 18 | final case class FailedToWriteProcessInput(reason: Throwable) 19 | extends ProxError { 20 | override def toThrowable: Throwable = FailedToWriteProcessInputException( 21 | reason 22 | ) 23 | } 24 | 25 | final case class UnknownProxErrorException(reason: Throwable) 26 | extends Exception(s"Unknown prox failure", reason) 27 | final case class UnknownProxError(reason: Throwable) extends ProxError { 28 | override def toThrowable: Throwable = UnknownProxErrorException(reason) 29 | } 30 | 31 | final case class MultipleProxErrorsException(value: List[ProxError]) 32 | extends Exception(s"Multiple prox failures: ${value.mkString(", ")}") 33 | final case class MultipleProxErrors(errors: List[ProxError]) extends ProxError { 34 | override def toThrowable: Throwable = MultipleProxErrorsException(errors) 35 | } 36 | 37 | final case class FailedToQueryStateException(reason: Throwable) 38 | extends Exception(s"Failed to query state of process", reason) 39 | final case class FailedToQueryState(reason: Throwable) extends ProxError { 40 | override def toThrowable: Throwable = FailedToQueryStateException(reason) 41 | } 42 | 43 | final case class FailedToDestroyException(reason: Throwable) 44 | extends Exception(s"Failed to destroy process", reason) 45 | final case class FailedToDestroy(reason: Throwable) extends ProxError { 46 | override def toThrowable: Throwable = FailedToDestroyException(reason) 47 | } 48 | 49 | final case class FailedToWaitForExitException(reason: Throwable) 50 | extends Exception(s"Failed to wait for process to exit", reason) 51 | final case class FailedToWaitForExit(reason: Throwable) extends ProxError { 52 | override def toThrowable: Throwable = FailedToWaitForExitException(reason) 53 | } 54 | 55 | final case class FailedToStartProcessException(reason: Throwable) 56 | extends Exception(s"Failed to start process", reason) 57 | final case class FailedToStartProcess(reason: Throwable) extends ProxError { 58 | override def toThrowable: Throwable = FailedToStartProcessException(reason) 59 | } 60 | -------------------------------------------------------------------------------- /prox-core/src/main/scala/io/github/vigoo/prox/package.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo 2 | 3 | /** Provides classes to work with system processes in a type safe way. 4 | * 5 | * Refer to the user guide for more information. 6 | * 7 | * A process to be executed is represented by the [[Process]] trait. Once it 8 | * has finished running the results are in [[ProcessResult]]. We call a group 9 | * of processes attached together a process group, represented by 10 | * [[ProcessGroup]], its result is described by [[ProcessGroupResult]]. 11 | * 12 | * Redirection of input, output and error is enabled by the 13 | * [[RedirectableInput]], [[RedirectableOutput]] and [[RedirectableError]] 14 | * trait for single processes, and the [[RedirectableErrors]] trait for process 15 | * groups. 16 | * 17 | * Processes and process groups are executed by a [[ProcessRunner]], the 18 | * default implementation is called [[JVMProcessRunner]]. 19 | * 20 | * @author 21 | * Daniel Vigovszky 22 | */ 23 | package object prox { 24 | trait Prox 25 | extends ProxRuntime 26 | with CommonModule 27 | with ProcessModule 28 | with ProcessGroupModule 29 | with RedirectionModule 30 | with ProcessRunnerModule 31 | with SyntaxModule 32 | 33 | /** Common base trait for processes and process groups, used in constraints in 34 | * the redirection traits 35 | */ 36 | trait ProcessLike 37 | } 38 | -------------------------------------------------------------------------------- /prox-core/src/main/scala/io/github/vigoo/prox/path/package.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox 2 | 3 | import java.nio.file.{Path, Paths} 4 | 5 | /** Small helper package to work with Java NIO paths */ 6 | package object path { 7 | 8 | /** The home directory */ 9 | val home: Path = Paths.get(java.lang.System.getProperty("user.home")) 10 | 11 | /** The root directory */ 12 | val root: Path = Paths.get("/") 13 | 14 | /** Extension methods for [[java.nio.file.Path]] */ 15 | implicit class PathOps(value: Path) { 16 | 17 | /** Resolves a child of the given path 18 | * 19 | * @param child 20 | * The child's name 21 | * @return 22 | * Returns the path to the given child 23 | */ 24 | def /(child: String): Path = value.resolve(child) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /prox-core/src/main/scala/io/github/vigoo/prox/processgroup.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox 2 | 3 | import java.nio.file.Path 4 | 5 | trait ProcessGroupModule { 6 | this: Prox => 7 | 8 | /** Result of an executed process group 9 | * 10 | * @tparam O 11 | * Output type 12 | * @tparam E 13 | * Error output type 14 | */ 15 | trait ProcessGroupResult[+O, +E] { 16 | 17 | /** Per-process exit codes. The key is the original process passed to the 18 | * piping operator 19 | */ 20 | val exitCodes: Map[Process[Unit, Unit], ProxExitCode] 21 | 22 | /** Output of the last process in the group */ 23 | val output: O 24 | 25 | /** Per-process error outputs. The key is the original process passed to the 26 | * piping operator 27 | */ 28 | val errors: Map[Process[Unit, Unit], E] 29 | } 30 | 31 | /** Default implementation of [[ProcessGroupResult]] */ 32 | case class SimpleProcessGroupResult[+O, +E]( 33 | override val exitCodes: Map[Process[Unit, Unit], ProxExitCode], 34 | override val output: O, 35 | override val errors: Map[Process[Unit, Unit], E] 36 | ) extends ProcessGroupResult[O, E] 37 | 38 | /** Representation of a running process group 39 | * 40 | * @tparam O 41 | * Output type 42 | * @tparam E 43 | * Error output type 44 | * @tparam Info 45 | * Runner-specific per-process information type 46 | */ 47 | trait RunningProcessGroup[O, E, +Info] { 48 | val runningOutput: ProxFiber[O] 49 | 50 | /** Runner-specific information about each running process */ 51 | val info: Map[Process[Unit, Unit], Info] 52 | 53 | /** Forcibly terminates all processes in the group. Blocks until it is done. 54 | */ 55 | def kill(): ProxIO[ProcessGroupResult[O, E]] 56 | 57 | /** Terminates all processes in the group. Blocks until it is done. */ 58 | def terminate(): ProxIO[ProcessGroupResult[O, E]] 59 | 60 | /** Blocks until the processes finish running */ 61 | def waitForExit(): ProxIO[ProcessGroupResult[O, E]] 62 | 63 | def mapInfo[I2]( 64 | f: (Process[Unit, Unit], Info) => I2 65 | ): RunningProcessGroup[O, E, I2] = 66 | new RunningProcessGroup[O, E, I2] { 67 | override val runningOutput: ProxFiber[O] = 68 | RunningProcessGroup.this.runningOutput 69 | override val info: Map[Process[Unit, Unit], I2] = 70 | RunningProcessGroup.this.info.map { case (key, value) => 71 | key -> f(key, value) 72 | } 73 | 74 | override def kill(): ProxIO[ProcessGroupResult[O, E]] = 75 | RunningProcessGroup.this.kill() 76 | 77 | override def terminate(): ProxIO[ProcessGroupResult[O, E]] = 78 | RunningProcessGroup.this.terminate() 79 | 80 | override def waitForExit(): ProxIO[ProcessGroupResult[O, E]] = 81 | RunningProcessGroup.this.waitForExit() 82 | } 83 | } 84 | 85 | /** Process group is two or more processes attached to each other 86 | * 87 | * This implements a pipeline of processes. The input of the first process 88 | * and the output of the last process is redirectable with the 89 | * [[RedirectableInput]] and [[RedirectableOutput]] traits. The processes are 90 | * attached to each other's input/output streams, the pipe between them is 91 | * customizable. 92 | * 93 | * The error streams are also redirectable with the [[RedirectableErrors]] 94 | * trait. 95 | * 96 | * @tparam O 97 | * Output type 98 | * @tparam E 99 | * Error output type 100 | */ 101 | trait ProcessGroup[O, E] 102 | extends ProcessLike 103 | with ProcessGroupConfiguration[O, E] { 104 | val firstProcess: Process[ProxStream[Byte], E] 105 | val innerProcesses: List[Process.UnboundIProcess[ProxStream[Byte], E]] 106 | val lastProcess: Process.UnboundIProcess[O, E] 107 | 108 | val originalProcesses: List[Process[Unit, Unit]] 109 | 110 | /** Starts the process group asynchronously and returns the 111 | * [[RunningProcessGroup]] interface for it 112 | * 113 | * This is the most advanced way to start process groups. See [[start]] and 114 | * [[run]] as alternatives. 115 | * 116 | * @param runner 117 | * The process runner to be used 118 | * @tparam Info 119 | * The runner-specific information about the started processes 120 | * @return 121 | * interface for handling the running process group 122 | */ 123 | def startProcessGroup[Info]()(implicit 124 | runner: ProcessRunner[Info] 125 | ): ProxIO[RunningProcessGroup[O, E, Info]] = 126 | runner.startProcessGroup(this) 127 | 128 | /** Starts the process group asynchronously and returns a closeable fiber 129 | * representing it 130 | * 131 | * Joining the fiber waits for the processes to be terminated. Canceling 132 | * the fiber terminates the processesnormally (with SIGTERM). 133 | * 134 | * @param runner 135 | * The process runner to be used 136 | * @return 137 | * a managed fiber representing the running processes 138 | */ 139 | def start[Info]()(implicit 140 | runner: ProcessRunner[Info] 141 | ): ProxResource[ProxFiber[ProcessGroupResult[O, E]]] = 142 | runner.start(this) 143 | 144 | /** Starts the process group asynchronously and blocks the execution until 145 | * it is finished 146 | * 147 | * @param runner 148 | * The process runner to be used 149 | * @return 150 | * the result of the finished processes 151 | */ 152 | def run[Info]()(implicit 153 | runner: ProcessRunner[Info] 154 | ): ProxIO[ProcessGroupResult[O, E]] = 155 | start().use(_.join) 156 | 157 | /** Applies the given mapper to each process in the group 158 | * 159 | * @param f 160 | * process mapper 161 | * @return 162 | * a new process group with all the processes altered by the mapper 163 | */ 164 | def map(f: ProcessGroup.Mapper[O, E]): Self 165 | } 166 | 167 | trait ProcessGroupConfiguration[O, E] extends ProcessLikeConfiguration { 168 | this: ProcessGroup[O, E] => 169 | 170 | override type Self <: ProcessGroup[O, E] 171 | 172 | private val allProcesses = (firstProcess :: innerProcesses) :+ lastProcess 173 | 174 | override val workingDirectory: Option[Path] = { 175 | val allWorkingDirectories = allProcesses.map(_.workingDirectory).toSet 176 | if (allWorkingDirectories.size == 1) { 177 | allWorkingDirectories.head 178 | } else { 179 | None 180 | } 181 | } 182 | 183 | override val environmentVariables: Map[String, String] = { 184 | allProcesses.map(_.environmentVariables.toSet).reduce(_ intersect _).toMap 185 | } 186 | 187 | override val removedEnvironmentVariables: Set[String] = { 188 | allProcesses.map(_.removedEnvironmentVariables).reduce(_ intersect _) 189 | } 190 | 191 | override protected def applyConfiguration( 192 | workingDirectory: Option[Path], 193 | environmentVariables: Map[String, String], 194 | removedEnvironmentVariables: Set[String] 195 | ): Self = 196 | map(new ProcessGroup.Mapper[O, E] { 197 | override def mapFirst[P <: Process[ProxStream[Byte], E]]( 198 | process: P 199 | ): P = 200 | ConfigApplication[P]( 201 | process, 202 | workingDirectory, 203 | environmentVariables, 204 | removedEnvironmentVariables 205 | ) 206 | 207 | override def mapInnerWithIdx[ 208 | P <: Process.UnboundIProcess[ProxStream[Byte], E] 209 | ](process: P, idx: Int): P = 210 | ConfigApplication[P]( 211 | process, 212 | workingDirectory, 213 | environmentVariables, 214 | removedEnvironmentVariables 215 | ) 216 | 217 | override def mapLast[P <: Process.UnboundIProcess[O, E]]( 218 | process: P 219 | ): P = 220 | ConfigApplication[P]( 221 | process, 222 | workingDirectory, 223 | environmentVariables, 224 | removedEnvironmentVariables 225 | ) 226 | }) 227 | 228 | class ConfigApplication[P <: ProcessLikeConfiguration] { 229 | // NOTE: Unfortunately we have no proof that P#Self == P so we cast 230 | 231 | private def applyWorkingDirectory( 232 | workingDirectory: Option[Path] 233 | )(process: P): P = 234 | workingDirectory match { 235 | case Some(path) => (process in path).asInstanceOf[P] 236 | case None => process.inInheritedWorkingDirectory().asInstanceOf[P] 237 | } 238 | 239 | private def addEnvironmentVariables( 240 | environmentVariables: Seq[(String, String)] 241 | )(process: P): P = 242 | environmentVariables.foldLeft(process) { case (proc, pair) => 243 | (proc `with` pair).asInstanceOf[P] 244 | } 245 | 246 | private def removeEnvironmentVariables( 247 | environmentVariables: Seq[String] 248 | )(process: P): P = 249 | environmentVariables.foldLeft(process) { case (proc, name) => 250 | (proc without name).asInstanceOf[P] 251 | } 252 | 253 | def apply( 254 | process: P, 255 | workingDirectory: Option[Path], 256 | environmentVariables: Map[String, String], 257 | removedEnvironmentVariables: Set[String] 258 | ): P = 259 | (applyWorkingDirectory(workingDirectory) _ compose 260 | addEnvironmentVariables(environmentVariables.toSeq) compose 261 | removeEnvironmentVariables(removedEnvironmentVariables.toSeq))( 262 | process 263 | ) 264 | } 265 | 266 | object ConfigApplication { 267 | def apply[P <: ProcessLikeConfiguration]: ConfigApplication[P] = 268 | new ConfigApplication[P] 269 | } 270 | 271 | } 272 | 273 | object ProcessGroup { 274 | 275 | /** Mapper functions for altering a process group */ 276 | trait Mapper[O, E] { 277 | def mapFirst[P <: Process[ProxStream[Byte], E]](process: P): P 278 | 279 | def mapInnerWithIdx[P <: Process.UnboundIProcess[ProxStream[Byte], E]]( 280 | process: P, 281 | idx: Int 282 | ): P 283 | 284 | def mapLast[P <: Process.UnboundIProcess[O, E]](process: P): P 285 | } 286 | 287 | /** Process group with bound input, output and error streams */ 288 | case class ProcessGroupImplIOE[O, E]( 289 | override val firstProcess: Process[ProxStream[Byte], E], 290 | override val innerProcesses: List[ 291 | Process.UnboundIProcess[ProxStream[Byte], E] 292 | ], 293 | override val lastProcess: Process.UnboundIProcess[O, E], 294 | override val originalProcesses: List[Process[Unit, Unit]] 295 | ) extends ProcessGroup[O, E] { 296 | 297 | override type Self = ProcessGroupImplIOE[O, E] 298 | 299 | def map(f: ProcessGroup.Mapper[O, E]): ProcessGroupImplIOE[O, E] = { 300 | copy( 301 | firstProcess = f.mapFirst(this.firstProcess), 302 | innerProcesses = this.innerProcesses.zipWithIndex.map { 303 | case (p, idx) => f.mapInnerWithIdx(p, idx + 1) 304 | }, 305 | lastProcess = f.mapLast(this.lastProcess), 306 | originalProcesses 307 | ) 308 | } 309 | } 310 | 311 | /** Process group with bound input and output streams */ 312 | case class ProcessGroupImplIO[O]( 313 | override val firstProcess: Process.UnboundEProcess[ProxStream[Byte]], 314 | override val innerProcesses: List[ 315 | Process.UnboundIEProcess[ProxStream[Byte]] 316 | ], 317 | override val lastProcess: Process.UnboundIEProcess[O], 318 | override val originalProcesses: List[Process[Unit, Unit]] 319 | ) extends ProcessGroup[O, Unit] 320 | with RedirectableErrors[ProcessGroupImplIOE[O, *]] { 321 | 322 | override type Self = ProcessGroupImplIO[O] 323 | 324 | def map(f: ProcessGroup.Mapper[O, Unit]): ProcessGroupImplIO[O] = { 325 | copy( 326 | firstProcess = f.mapFirst(this.firstProcess), 327 | innerProcesses = this.innerProcesses.zipWithIndex.map { 328 | case (p, idx) => f.mapInnerWithIdx(p, idx + 1) 329 | }, 330 | lastProcess = f.mapLast(this.lastProcess), 331 | originalProcesses 332 | ) 333 | } 334 | 335 | override def connectErrors[ 336 | R <: GroupErrorRedirection, 337 | OR <: OutputRedirection, 338 | E 339 | ](target: R)(implicit 340 | groupErrorRedirectionType: GroupErrorRedirectionType.Aux[R, OR, E], 341 | outputRedirectionType: OutputRedirectionType.Aux[OR, E] 342 | ): ProcessGroupImplIOE[O, E] = { 343 | val origs = originalProcesses.reverse.toVector 344 | ProcessGroupImplIOE( 345 | firstProcess.connectError( 346 | groupErrorRedirectionType 347 | .toOutputRedirectionType(target, origs.head) 348 | ), 349 | innerProcesses.zipWithIndex.map { case (p, idx) => 350 | p.connectError( 351 | groupErrorRedirectionType 352 | .toOutputRedirectionType(target, origs(idx + 1)) 353 | ) 354 | }, 355 | lastProcess.connectError( 356 | groupErrorRedirectionType 357 | .toOutputRedirectionType(target, origs.last) 358 | ), 359 | originalProcesses 360 | ) 361 | } 362 | } 363 | 364 | /** Process group with bound input and error streams */ 365 | case class ProcessGroupImplIE[E]( 366 | override val firstProcess: Process[ProxStream[Byte], E], 367 | override val innerProcesses: List[ 368 | Process.UnboundIProcess[ProxStream[Byte], E] 369 | ], 370 | override val lastProcess: Process.UnboundIOProcess[E], 371 | override val originalProcesses: List[Process[Unit, Unit]] 372 | ) extends ProcessGroup[Unit, E] 373 | with RedirectableOutput[ProcessGroupImplIOE[*, E]] { 374 | 375 | override type Self = ProcessGroupImplIE[E] 376 | 377 | def map(f: ProcessGroup.Mapper[Unit, E]): ProcessGroupImplIE[E] = { 378 | copy( 379 | firstProcess = f.mapFirst(this.firstProcess), 380 | innerProcesses = this.innerProcesses.zipWithIndex.map { 381 | case (p, idx) => f.mapInnerWithIdx(p, idx + 1) 382 | }, 383 | lastProcess = f.mapLast(this.lastProcess), 384 | originalProcesses 385 | ) 386 | } 387 | 388 | override def connectOutput[R <: OutputRedirection, RO](target: R)(implicit 389 | outputRedirectionType: OutputRedirectionType.Aux[R, RO] 390 | ): ProcessGroupImplIOE[RO, E] = { 391 | ProcessGroupImplIOE( 392 | firstProcess, 393 | innerProcesses, 394 | lastProcess.connectOutput(target), 395 | originalProcesses 396 | ) 397 | } 398 | } 399 | 400 | /** Process group with bound output and error streams */ 401 | case class ProcessGroupImplOE[O, E]( 402 | override val firstProcess: Process.UnboundIProcess[ProxStream[Byte], E], 403 | override val innerProcesses: List[ 404 | Process.UnboundIProcess[ProxStream[Byte], E] 405 | ], 406 | override val lastProcess: Process.UnboundIProcess[O, E], 407 | override val originalProcesses: List[Process[Unit, Unit]] 408 | ) extends ProcessGroup[O, E] 409 | with RedirectableInput[ProcessGroupImplIOE[O, E]] { 410 | 411 | override type Self = ProcessGroupImplOE[O, E] 412 | 413 | def map(f: ProcessGroup.Mapper[O, E]): ProcessGroupImplOE[O, E] = { 414 | copy( 415 | firstProcess = f.mapFirst(this.firstProcess), 416 | innerProcesses = this.innerProcesses.zipWithIndex.map { 417 | case (p, idx) => f.mapInnerWithIdx(p, idx + 1) 418 | }, 419 | lastProcess = f.mapLast(this.lastProcess), 420 | originalProcesses 421 | ) 422 | } 423 | 424 | override def connectInput( 425 | source: InputRedirection 426 | ): ProcessGroupImplIOE[O, E] = { 427 | ProcessGroupImplIOE( 428 | firstProcess.connectInput(source), 429 | innerProcesses, 430 | lastProcess, 431 | originalProcesses 432 | ) 433 | } 434 | } 435 | 436 | /** Process group with bound input stream */ 437 | case class ProcessGroupImplI( 438 | override val firstProcess: Process.UnboundEProcess[ProxStream[Byte]], 439 | override val innerProcesses: List[ 440 | Process.UnboundIEProcess[ProxStream[Byte]] 441 | ], 442 | override val lastProcess: Process.UnboundProcess, 443 | override val originalProcesses: List[Process[Unit, Unit]] 444 | ) extends ProcessGroup[Unit, Unit] 445 | with RedirectableOutput[ProcessGroupImplIO[*]] 446 | with RedirectableErrors[ProcessGroupImplIE[*]] { 447 | 448 | override type Self = ProcessGroupImplI 449 | 450 | def map(f: ProcessGroup.Mapper[Unit, Unit]): ProcessGroupImplI = { 451 | copy( 452 | firstProcess = f.mapFirst(this.firstProcess), 453 | innerProcesses = this.innerProcesses.zipWithIndex.map { 454 | case (p, idx) => f.mapInnerWithIdx(p, idx + 1) 455 | }, 456 | lastProcess = f.mapLast(this.lastProcess), 457 | originalProcesses 458 | ) 459 | } 460 | 461 | override def connectOutput[R <: OutputRedirection, RO](target: R)(implicit 462 | outputRedirectionType: OutputRedirectionType.Aux[R, RO] 463 | ): ProcessGroupImplIO[RO] = { 464 | ProcessGroupImplIO( 465 | firstProcess, 466 | innerProcesses, 467 | lastProcess.connectOutput(target), 468 | originalProcesses 469 | ) 470 | } 471 | 472 | override def connectErrors[ 473 | R <: GroupErrorRedirection, 474 | OR <: OutputRedirection, 475 | E 476 | ](target: R)(implicit 477 | groupErrorRedirectionType: GroupErrorRedirectionType.Aux[R, OR, E], 478 | outputRedirectionType: OutputRedirectionType.Aux[OR, E] 479 | ): ProcessGroupImplIE[E] = { 480 | val origs = originalProcesses.reverse.toVector 481 | ProcessGroupImplIE( 482 | firstProcess.connectError( 483 | groupErrorRedirectionType 484 | .toOutputRedirectionType(target, origs.head) 485 | ), 486 | innerProcesses.zipWithIndex.map { case (p, idx) => 487 | p.connectError( 488 | groupErrorRedirectionType 489 | .toOutputRedirectionType(target, origs(idx + 1)) 490 | ) 491 | }, 492 | lastProcess.connectError( 493 | groupErrorRedirectionType 494 | .toOutputRedirectionType(target, origs.last) 495 | ), 496 | originalProcesses 497 | ) 498 | } 499 | } 500 | 501 | /** Process group with bound output stream */ 502 | case class ProcessGroupImplO[O]( 503 | override val firstProcess: Process.UnboundIEProcess[ProxStream[Byte]], 504 | override val innerProcesses: List[ 505 | Process.UnboundIEProcess[ProxStream[Byte]] 506 | ], 507 | override val lastProcess: Process.UnboundIEProcess[O], 508 | override val originalProcesses: List[Process[Unit, Unit]] 509 | ) extends ProcessGroup[O, Unit] 510 | with RedirectableInput[ProcessGroupImplIO[O]] 511 | with RedirectableErrors[ProcessGroupImplOE[O, *]] { 512 | 513 | override type Self = ProcessGroupImplO[O] 514 | 515 | def map(f: ProcessGroup.Mapper[O, Unit]): ProcessGroupImplO[O] = { 516 | copy( 517 | firstProcess = f.mapFirst(this.firstProcess), 518 | innerProcesses = this.innerProcesses.zipWithIndex.map { 519 | case (p, idx) => f.mapInnerWithIdx(p, idx + 1) 520 | }, 521 | lastProcess = f.mapLast(this.lastProcess), 522 | originalProcesses 523 | ) 524 | } 525 | 526 | override def connectInput( 527 | source: InputRedirection 528 | ): ProcessGroupImplIO[O] = { 529 | ProcessGroupImplIO( 530 | firstProcess.connectInput(source), 531 | innerProcesses, 532 | lastProcess, 533 | originalProcesses 534 | ) 535 | } 536 | 537 | override def connectErrors[ 538 | R <: GroupErrorRedirection, 539 | OR <: OutputRedirection, 540 | E 541 | ](target: R)(implicit 542 | groupErrorRedirectionType: GroupErrorRedirectionType.Aux[R, OR, E], 543 | outputRedirectionType: OutputRedirectionType.Aux[OR, E] 544 | ): ProcessGroupImplOE[O, E] = { 545 | val origs = originalProcesses.reverse.toVector 546 | ProcessGroupImplOE( 547 | firstProcess.connectError( 548 | groupErrorRedirectionType 549 | .toOutputRedirectionType(target, origs.head) 550 | ), 551 | innerProcesses.zipWithIndex.map { case (p, idx) => 552 | p.connectError( 553 | groupErrorRedirectionType 554 | .toOutputRedirectionType(target, origs(idx + 1)) 555 | ) 556 | }, 557 | lastProcess.connectError( 558 | groupErrorRedirectionType 559 | .toOutputRedirectionType(target, origs.last) 560 | ), 561 | originalProcesses 562 | ) 563 | } 564 | } 565 | 566 | /** Process group with bound error stream */ 567 | case class ProcessGroupImplE[E]( 568 | override val firstProcess: Process.UnboundIProcess[ProxStream[Byte], E], 569 | override val innerProcesses: List[ 570 | Process.UnboundIProcess[ProxStream[Byte], E] 571 | ], 572 | override val lastProcess: Process.UnboundIOProcess[E], 573 | override val originalProcesses: List[Process[Unit, Unit]] 574 | ) extends ProcessGroup[Unit, E] 575 | with RedirectableOutput[ProcessGroupImplOE[*, E]] 576 | with RedirectableInput[ProcessGroupImplIE[E]] { 577 | 578 | override type Self = ProcessGroupImplE[E] 579 | 580 | def map(f: ProcessGroup.Mapper[Unit, E]): ProcessGroupImplE[E] = { 581 | copy( 582 | firstProcess = f.mapFirst(this.firstProcess), 583 | innerProcesses = this.innerProcesses.zipWithIndex.map { 584 | case (p, idx) => f.mapInnerWithIdx(p, idx + 1) 585 | }, 586 | lastProcess = f.mapLast(this.lastProcess), 587 | originalProcesses 588 | ) 589 | } 590 | 591 | override def connectOutput[R <: OutputRedirection, RO](target: R)(implicit 592 | outputRedirectionType: OutputRedirectionType.Aux[R, RO] 593 | ): ProcessGroupImplOE[RO, E] = { 594 | ProcessGroupImplOE( 595 | firstProcess, 596 | innerProcesses, 597 | lastProcess.connectOutput(target), 598 | originalProcesses 599 | ) 600 | } 601 | 602 | override def connectInput( 603 | source: InputRedirection 604 | ): ProcessGroupImplIE[E] = { 605 | ProcessGroupImplIE( 606 | firstProcess.connectInput(source), 607 | innerProcesses, 608 | lastProcess, 609 | originalProcesses 610 | ) 611 | } 612 | } 613 | 614 | /** Process group with unbound input, output and error streams */ 615 | case class ProcessGroupImpl( 616 | override val firstProcess: Process.UnboundIEProcess[ProxStream[Byte]], 617 | override val innerProcesses: List[ 618 | Process.UnboundIEProcess[ProxStream[Byte]] 619 | ], 620 | override val lastProcess: Process.UnboundProcess, 621 | override val originalProcesses: List[Process[Unit, Unit]] 622 | ) extends ProcessGroup[Unit, Unit] 623 | with RedirectableOutput[ProcessGroupImplO[*]] 624 | with RedirectableInput[ProcessGroupImplI] 625 | with RedirectableErrors[ProcessGroupImplE[*]] { 626 | 627 | override type Self = ProcessGroupImpl 628 | 629 | def pipeInto( 630 | other: Process.UnboundProcess, 631 | channel: ProxPipe[Byte, Byte] 632 | ): ProcessGroupImpl = { 633 | val pl1 = lastProcess.connectOutput( 634 | OutputStreamThroughPipe( 635 | channel, 636 | (stream: ProxStream[Byte]) => pure(stream) 637 | ) 638 | ) 639 | 640 | copy( 641 | innerProcesses = pl1 :: innerProcesses, 642 | lastProcess = other, 643 | originalProcesses = other :: originalProcesses 644 | ) 645 | } 646 | 647 | def |(other: Process.UnboundProcess): ProcessGroupImpl = 648 | pipeInto(other, identityPipe) 649 | 650 | def via( 651 | channel: ProxPipe[Byte, Byte] 652 | ): PipeBuilderSyntax[ProcessGroupImpl] = 653 | new PipeBuilderSyntax( 654 | new PipeBuilder[ProcessGroupImpl] { 655 | override def build( 656 | other: Process.UnboundProcess, 657 | channel: ProxPipe[Byte, Byte] 658 | ): ProcessGroupImpl = 659 | ProcessGroupImpl.this.pipeInto(other, channel) 660 | }, 661 | channel 662 | ) 663 | 664 | def map(f: ProcessGroup.Mapper[Unit, Unit]): ProcessGroupImpl = { 665 | copy( 666 | firstProcess = f.mapFirst(this.firstProcess), 667 | innerProcesses = this.innerProcesses.zipWithIndex.map { 668 | case (p, idx) => f.mapInnerWithIdx(p, idx + 1) 669 | }, 670 | lastProcess = f.mapLast(this.lastProcess), 671 | originalProcesses 672 | ) 673 | } 674 | 675 | override def connectInput(source: InputRedirection): ProcessGroupImplI = 676 | ProcessGroupImplI( 677 | firstProcess.connectInput(source), 678 | innerProcesses, 679 | lastProcess, 680 | originalProcesses 681 | ) 682 | 683 | override def connectErrors[ 684 | R <: GroupErrorRedirection, 685 | OR <: OutputRedirection, 686 | E 687 | ](target: R)(implicit 688 | groupErrorRedirectionType: GroupErrorRedirectionType.Aux[R, OR, E], 689 | outputRedirectionType: OutputRedirectionType.Aux[OR, E] 690 | ): ProcessGroupImplE[E] = { 691 | val origs = originalProcesses.reverse.toVector 692 | ProcessGroupImplE( 693 | firstProcess.connectError( 694 | groupErrorRedirectionType 695 | .toOutputRedirectionType(target, origs.head) 696 | ), 697 | innerProcesses.zipWithIndex.map { case (p, idx) => 698 | p.connectError( 699 | groupErrorRedirectionType 700 | .toOutputRedirectionType(target, origs(idx + 1)) 701 | ) 702 | }, 703 | lastProcess.connectError( 704 | groupErrorRedirectionType 705 | .toOutputRedirectionType(target, origs.last) 706 | ), 707 | originalProcesses 708 | ) 709 | } 710 | 711 | override def connectOutput[R <: OutputRedirection, RO](target: R)(implicit 712 | outputRedirectionType: OutputRedirectionType.Aux[R, RO] 713 | ): ProcessGroupImplO[RO] = { 714 | ProcessGroupImplO( 715 | firstProcess, 716 | innerProcesses, 717 | lastProcess.connectOutput(target), 718 | originalProcesses 719 | ) 720 | } 721 | } 722 | 723 | } 724 | 725 | } 726 | -------------------------------------------------------------------------------- /prox-core/src/main/scala/io/github/vigoo/prox/runner.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox 2 | 3 | import java.lang.{Process as JvmProcess} 4 | import scala.jdk.CollectionConverters.* 5 | 6 | trait ProcessRunnerModule { 7 | this: Prox => 8 | 9 | /** Interface for running processes and process groups 10 | * 11 | * The default implementation is [[JVMProcessRunner]] 12 | * 13 | * @tparam Info 14 | * The type of information provided for a started process 15 | */ 16 | trait ProcessRunner[Info] { 17 | 18 | /** Starts the process asynchronously and returns the [[RunningProcess]] 19 | * interface for it 20 | * 21 | * @param process 22 | * The process to be started 23 | * @tparam O 24 | * Output type 25 | * @tparam E 26 | * Error output type 27 | * @return 28 | * interface for handling the running process 29 | */ 30 | def startProcess[O, E]( 31 | process: Process[O, E] 32 | ): ProxIO[RunningProcess[O, E, Info]] 33 | 34 | /** Starts the process asynchronously and returns a managed fiber 35 | * representing it 36 | * 37 | * Joining the fiber means waiting until the process gets terminated. 38 | * Cancelling the fiber terminates the process. 39 | * 40 | * @param process 41 | * The process to be started 42 | * @tparam O 43 | * Output type 44 | * @tparam E 45 | * Error output type 46 | * @return 47 | * interface for handling the running process 48 | */ 49 | def start[O, E]( 50 | process: Process[O, E] 51 | ): ProxResource[ProxFiber[ProcessResult[O, E]]] = { 52 | val run = startFiber(bracket(startProcess(process)) { runningProcess => 53 | runningProcess.waitForExit() 54 | } { 55 | case (_, Completed) => 56 | unit 57 | case (_, Failed(reason)) => 58 | raiseError(reason.toSingleError) 59 | case (runningProcess, Canceled) => 60 | runningProcess.terminate().map(_ => ()) 61 | }) 62 | 63 | makeResource(run, _.cancel) 64 | } 65 | 66 | /** Starts a process group asynchronously and returns an interface for them 67 | * 68 | * @param processGroup 69 | * The process group to start 70 | * @tparam O 71 | * Output type 72 | * @tparam E 73 | * Error output type 74 | * @return 75 | * interface for handling the running process group 76 | */ 77 | def startProcessGroup[O, E]( 78 | processGroup: ProcessGroup[O, E] 79 | ): ProxIO[RunningProcessGroup[O, E, Info]] 80 | 81 | /** Starts the process group asynchronously and returns a managed fiber 82 | * representing it 83 | * 84 | * Joining the fiber means waiting until the process gets terminated. 85 | * Cancelling the fiber terminates the process. 86 | * 87 | * @param processGroup 88 | * The process group to be started 89 | * @tparam O 90 | * Output type 91 | * @tparam E 92 | * Error output type 93 | * @return 94 | * interface for handling the running process 95 | */ 96 | def start[O, E]( 97 | processGroup: ProcessGroup[O, E] 98 | ): ProxResource[ProxFiber[ProcessGroupResult[O, E]]] = { 99 | val run = 100 | startFiber( 101 | bracket(startProcessGroup(processGroup)) { runningProcess => 102 | runningProcess.waitForExit() 103 | } { 104 | case (_, Completed) => 105 | unit 106 | case (_, Failed(reason)) => 107 | raiseError(reason.toSingleError) 108 | case (runningProcess, Canceled) => 109 | runningProcess.terminate().map(_ => ()) 110 | } 111 | ) 112 | 113 | makeResource(run, _.cancel) 114 | } 115 | } 116 | 117 | class JVMProcessInfo() 118 | 119 | /** Default implementation of [[RunningProcess]] using the Java process API */ 120 | class JVMRunningProcess[O, E, +Info <: JVMProcessInfo]( 121 | val nativeProcess: JvmProcess, 122 | override val runningInput: ProxFiber[Unit], 123 | override val runningOutput: ProxFiber[O], 124 | override val runningError: ProxFiber[E], 125 | override val info: Info 126 | ) extends RunningProcess[O, E, Info] { 127 | 128 | def isAlive: ProxIO[Boolean] = 129 | effect(nativeProcess.isAlive, FailedToQueryState.apply) 130 | 131 | def kill(): ProxIO[ProcessResult[O, E]] = 132 | effect(nativeProcess.destroyForcibly(), FailedToDestroy.apply).flatMap( 133 | _ => waitForExit() 134 | ) 135 | 136 | def terminate(): ProxIO[ProcessResult[O, E]] = 137 | effect(nativeProcess.destroy(), FailedToDestroy.apply).flatMap(_ => 138 | waitForExit() 139 | ) 140 | 141 | def waitForExit(): ProxIO[ProcessResult[O, E]] = { 142 | for { 143 | exitCode <- blockingEffect( 144 | nativeProcess.waitFor(), 145 | FailedToWaitForExit.apply 146 | ) 147 | _ <- runningInput.join 148 | output <- runningOutput.join 149 | error <- runningError.join 150 | } yield SimpleProcessResult(exitCodeFromInt(exitCode), output, error) 151 | } 152 | } 153 | 154 | /** Default implementation of [[RunningProcessGroup]] using the Java process 155 | * API 156 | */ 157 | class JVMRunningProcessGroup[O, E, +Info <: JVMProcessInfo]( 158 | runningProcesses: Map[Process[Unit, Unit], RunningProcess[_, E, Info]], 159 | override val runningOutput: ProxFiber[O] 160 | ) extends RunningProcessGroup[O, E, Info] { 161 | 162 | override val info: Map[Process[Unit, Unit], Info] = 163 | runningProcesses.map { case (key, value) => (key, value.info) } 164 | 165 | def kill(): ProxIO[ProcessGroupResult[O, E]] = 166 | traverse(runningProcesses.values.toList)(_.kill().map(_ => ())).flatMap( 167 | _ => waitForExit() 168 | ) 169 | 170 | def terminate(): ProxIO[ProcessGroupResult[O, E]] = 171 | traverse(runningProcesses.values.toList)(_.terminate().map(_ => ())) 172 | .flatMap(_ => waitForExit()) 173 | 174 | def waitForExit(): ProxIO[ProcessGroupResult[O, E]] = 175 | for { 176 | results <- traverse(runningProcesses.toList) { case (spec, rp) => 177 | rp.waitForExit().map((result: ProcessResult[_, E]) => spec -> result) 178 | } 179 | lastOutput <- runningOutput.join 180 | exitCodes = results.map { case (proc, result) => 181 | proc -> result.exitCode 182 | }.toMap 183 | errors = results.map { case (proc, result) => 184 | proc -> result.error 185 | }.toMap 186 | } yield SimpleProcessGroupResult(exitCodes, lastOutput, errors) 187 | } 188 | 189 | /** Default implementation of [[ProcessRunner]] using the Java process API */ 190 | abstract class JVMProcessRunnerBase[Info <: JVMProcessInfo] 191 | extends ProcessRunner[Info] { 192 | 193 | import JVMProcessRunnerBase._ 194 | 195 | override def startProcess[O, E]( 196 | process: Process[O, E] 197 | ): ProxIO[RunningProcess[O, E, Info]] = { 198 | val builder = withEnvironmentVariables( 199 | process, 200 | withWorkingDirectory( 201 | process, 202 | new ProcessBuilder((process.command :: process.arguments).asJava) 203 | ) 204 | ) 205 | 206 | builder.redirectOutput( 207 | ouptutRedirectionToNative(process.outputRedirection) 208 | ) 209 | builder.redirectError(ouptutRedirectionToNative(process.errorRedirection)) 210 | builder.redirectInput(inputRedirectionToNative(process.inputRedirection)) 211 | 212 | for { 213 | nativeProcess <- effect(builder.start(), FailedToStartProcess.apply) 214 | processInfo <- getProcessInfo(nativeProcess) 215 | nativeOutputStream <- effect( 216 | nativeProcess.getInputStream, 217 | UnknownProxError.apply 218 | ) 219 | nativeErrorStream <- effect( 220 | nativeProcess.getErrorStream, 221 | UnknownProxError.apply 222 | ) 223 | 224 | inputStream = runInputStream(process, nativeProcess) 225 | runningInput <- startFiber(inputStream) 226 | runningOutput <- startFiber(process.runOutputStream(nativeOutputStream)) 227 | runningError <- startFiber(process.runErrorStream(nativeErrorStream)) 228 | } yield new JVMRunningProcess( 229 | nativeProcess, 230 | runningInput, 231 | runningOutput, 232 | runningError, 233 | processInfo 234 | ) 235 | } 236 | 237 | protected def getProcessInfo(process: JvmProcess): ProxIO[Info] 238 | 239 | private def connectAndStartProcesses[E]( 240 | firstProcess: Process[ProxStream[Byte], E] 241 | with RedirectableInput[Process[ProxStream[Byte], E]], 242 | previousOutput: ProxStream[Byte], 243 | remainingProcesses: List[ 244 | Process[ProxStream[Byte], E] 245 | with RedirectableInput[Process[ProxStream[Byte], E]] 246 | ], 247 | startedProcesses: List[RunningProcess[_, E, Info]] 248 | ): ProxIO[(List[RunningProcess[_, E, Info]], ProxStream[Byte])] = { 249 | startProcess( 250 | firstProcess.connectInput( 251 | InputStream(previousOutput, flushChunks = false) 252 | ) 253 | ).flatMap { first => 254 | first.runningOutput.join.flatMap { firstOutput => 255 | val updatedStartedProcesses = first :: startedProcesses 256 | remainingProcesses match { 257 | case nextProcess :: rest => 258 | connectAndStartProcesses( 259 | nextProcess, 260 | firstOutput, 261 | rest, 262 | updatedStartedProcesses 263 | ) 264 | case Nil => 265 | pure((updatedStartedProcesses.reverse, firstOutput)) 266 | } 267 | } 268 | } 269 | } 270 | 271 | override def startProcessGroup[O, E]( 272 | processGroup: ProcessGroup[O, E] 273 | ): ProxIO[RunningProcessGroup[O, E, Info]] = 274 | for { 275 | first <- startProcess(processGroup.firstProcess) 276 | firstOutput <- first.runningOutput.join 277 | innerResult <- 278 | if (processGroup.innerProcesses.isEmpty) { 279 | pure((List.empty, firstOutput)) 280 | } else { 281 | val inner = processGroup.innerProcesses.reverse 282 | connectAndStartProcesses( 283 | inner.head, 284 | firstOutput, 285 | inner.tail, 286 | List.empty 287 | ) 288 | } 289 | (inner, lastInput) = innerResult 290 | last <- startProcess( 291 | processGroup.lastProcess.connectInput( 292 | InputStream(lastInput, flushChunks = false) 293 | ) 294 | ) 295 | runningProcesses = processGroup.originalProcesses.reverse 296 | .zip((first :: inner) :+ last) 297 | .toMap 298 | } yield new JVMRunningProcessGroup[O, E, Info]( 299 | runningProcesses, 300 | last.runningOutput 301 | ) 302 | 303 | private def runInputStream[O, E]( 304 | process: Process[O, E], 305 | nativeProcess: JvmProcess 306 | ): ProxIO[Unit] = { 307 | process.inputRedirection match { 308 | case StdIn() => unit 309 | case InputFile(_) => unit 310 | case InputStream(stream, flushChunks) => 311 | drainToJavaOutputStream( 312 | stream, 313 | nativeProcess.getOutputStream, 314 | flushChunks 315 | ) 316 | } 317 | } 318 | } 319 | 320 | object JVMProcessRunnerBase { 321 | def withWorkingDirectory[O, E]( 322 | process: Process[O, E], 323 | builder: ProcessBuilder 324 | ): ProcessBuilder = 325 | process.workingDirectory match { 326 | case Some(directory) => builder.directory(directory.toFile) 327 | case None => builder 328 | } 329 | 330 | def withEnvironmentVariables[O, E]( 331 | process: Process[O, E], 332 | builder: ProcessBuilder 333 | ): ProcessBuilder = { 334 | process.environmentVariables.foreach { case (name, value) => 335 | builder.environment().put(name, value) 336 | } 337 | process.removedEnvironmentVariables.foreach { name => 338 | builder.environment().remove(name) 339 | } 340 | builder 341 | } 342 | 343 | def ouptutRedirectionToNative( 344 | outputRedirection: OutputRedirection 345 | ): ProcessBuilder.Redirect = { 346 | outputRedirection match { 347 | case StdOut() => ProcessBuilder.Redirect.INHERIT 348 | case OutputFile(path, false) => ProcessBuilder.Redirect.to(path.toFile) 349 | case OutputFile(path, true) => 350 | ProcessBuilder.Redirect.appendTo(path.toFile) 351 | case OutputStreamThroughPipe(_, _, _) => ProcessBuilder.Redirect.PIPE 352 | case OutputStreamToSink(_, _) => ProcessBuilder.Redirect.PIPE 353 | } 354 | } 355 | 356 | def inputRedirectionToNative( 357 | inputRedirection: InputRedirection 358 | ): ProcessBuilder.Redirect = { 359 | inputRedirection match { 360 | case StdIn() => ProcessBuilder.Redirect.INHERIT 361 | case InputFile(path) => ProcessBuilder.Redirect.from(path.toFile) 362 | case InputStream(_, _) => ProcessBuilder.Redirect.PIPE 363 | } 364 | } 365 | } 366 | 367 | class JVMProcessRunner() extends JVMProcessRunnerBase[JVMProcessInfo] { 368 | 369 | override protected def getProcessInfo( 370 | process: JvmProcess 371 | ): ProxIO[JVMProcessInfo] = 372 | effect(new JVMProcessInfo(), UnknownProxError.apply) 373 | } 374 | 375 | } 376 | -------------------------------------------------------------------------------- /prox-core/src/main/scala/io/github/vigoo/prox/runtime.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox 2 | 3 | private[prox] sealed trait IOResult 4 | private[prox] case object Completed extends IOResult 5 | private[prox] final case class Failed(errors: List[ProxError]) extends IOResult 6 | private[prox] case object Canceled extends IOResult 7 | 8 | trait ProxRuntime { 9 | // NOTE: the Prox prefix was added to avoid collision with the host environment's names (when importing cats.effect._ or zio._) 10 | type ProxExitCode 11 | type ProxFiber[_] 12 | type ProxIO[_] 13 | type ProxResource[_] 14 | 15 | type ProxSink[_] 16 | type ProxPipe[_, _] 17 | type ProxStream[_] 18 | 19 | type ProxMonoid[_] 20 | 21 | protected def exitCodeFromInt(value: Int): ProxExitCode 22 | 23 | protected def unit: ProxIO[Unit] 24 | protected def pure[A](value: A): ProxIO[A] 25 | protected def effect[A](f: => A, wrapError: Throwable => ProxError): ProxIO[A] 26 | protected def blockingEffect[A]( 27 | f: => A, 28 | wrapError: Throwable => ProxError 29 | ): ProxIO[A] 30 | protected def raiseError(error: ProxError): ProxIO[Unit] 31 | protected def ioMap[A, B](io: ProxIO[A], f: A => B): ProxIO[B] 32 | protected def ioFlatMap[A, B](io: ProxIO[A], f: A => ProxIO[B]): ProxIO[B] 33 | protected def traverse[A, B](list: List[A])( 34 | f: A => ProxIO[B] 35 | ): ProxIO[List[B]] 36 | 37 | protected def identityPipe[A]: ProxPipe[A, A] 38 | 39 | protected def bracket[A, B](acquire: ProxIO[A])(use: A => ProxIO[B])( 40 | fin: (A, IOResult) => ProxIO[Unit] 41 | ): ProxIO[B] 42 | 43 | protected def makeResource[A]( 44 | acquire: ProxIO[A], 45 | release: A => ProxIO[Unit] 46 | ): ProxResource[A] 47 | protected def useResource[A, B]( 48 | r: ProxResource[A], 49 | f: A => ProxIO[B] 50 | ): ProxIO[B] 51 | 52 | protected def joinFiber[A](f: ProxFiber[A]): ProxIO[A] 53 | protected def cancelFiber[A](f: ProxFiber[A]): ProxIO[Unit] 54 | 55 | protected def drainStream[A](s: ProxStream[A]): ProxIO[Unit] 56 | protected def streamToVector[A](s: ProxStream[A]): ProxIO[Vector[A]] 57 | protected def foldStream[A, B]( 58 | s: ProxStream[A], 59 | init: B, 60 | f: (B, A) => B 61 | ): ProxIO[B] 62 | protected def foldMonoidStream[A: ProxMonoid](s: ProxStream[A]): ProxIO[A] 63 | protected def streamThrough[A, B]( 64 | s: ProxStream[A], 65 | pipe: ProxPipe[A, B] 66 | ): ProxStream[B] 67 | protected def runStreamTo[A]( 68 | s: ProxStream[A], 69 | sink: ProxSink[A] 70 | ): ProxIO[Unit] 71 | 72 | protected def fromJavaInputStream( 73 | input: java.io.InputStream, 74 | chunkSize: Int 75 | ): ProxStream[Byte] 76 | protected def drainToJavaOutputStream( 77 | stream: ProxStream[Byte], 78 | output: java.io.OutputStream, 79 | flushChunks: Boolean 80 | ): ProxIO[Unit] 81 | 82 | protected def startFiber[A](f: ProxIO[A]): ProxIO[ProxFiber[A]] 83 | 84 | protected implicit class IOOps[A](io: ProxIO[A]) { 85 | def map[B](f: A => B): ProxIO[B] = ioMap(io, f) 86 | def flatMap[B](f: A => ProxIO[B]): ProxIO[B] = ioFlatMap(io, f) 87 | } 88 | 89 | protected implicit class ResourceOps[A](r: ProxResource[A]) { 90 | def use[B](f: A => ProxIO[B]): ProxIO[B] = useResource(r, f) 91 | } 92 | 93 | protected implicit class FiberOps[A](f: ProxFiber[A]) { 94 | def cancel: ProxIO[Unit] = cancelFiber(f) 95 | def join: ProxIO[A] = joinFiber(f) 96 | } 97 | 98 | protected implicit class StreamOps[A](s: ProxStream[A]) { 99 | def drain: ProxIO[Unit] = drainStream(s) 100 | def toVector: ProxIO[Vector[A]] = streamToVector(s) 101 | def fold[B](init: B, f: (B, A) => B): ProxIO[B] = foldStream(s, init, f) 102 | def through[B](pipe: ProxPipe[A, B]): ProxStream[B] = streamThrough(s, pipe) 103 | def run(sink: ProxSink[A]): ProxIO[Unit] = runStreamTo(s, sink) 104 | } 105 | 106 | protected implicit class MonoidStreamOps[A: ProxMonoid](s: ProxStream[A]) { 107 | def foldMonoid: ProxIO[A] = foldMonoidStream(s) 108 | } 109 | 110 | protected implicit class ListProxErrorOps(list: List[ProxError]) { 111 | def toSingleError: ProxError = 112 | list match { 113 | case Nil => 114 | UnknownProxError(new IllegalArgumentException("Error list is empty")) 115 | case List(single) => single 116 | case _ => MultipleProxErrors(list) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /prox-core/src/main/scala/io/github/vigoo/prox/syntax.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox 2 | 3 | trait SyntaxModule { 4 | this: Prox => 5 | 6 | /** Extension methods for unbound processes enabling the creation of process 7 | * groups 8 | */ 9 | implicit class ProcessPiping(process: Process.UnboundProcess) { 10 | 11 | /** Attaches the output of this process to an other process' input 12 | * 13 | * Use the [[|]] or the [[via]] methods instead for more readability. 14 | * 15 | * @param other 16 | * The other process 17 | * @param channel 18 | * Pipe between the two processes 19 | * @return 20 | * Returns a [[ProcessGroup]] 21 | */ 22 | def pipeInto( 23 | other: Process.UnboundProcess, 24 | channel: ProxPipe[Byte, Byte] 25 | ): ProcessGroup.ProcessGroupImpl = { 26 | 27 | val p1 = process.connectOutput( 28 | OutputStreamThroughPipe( 29 | channel, 30 | (stream: ProxStream[Byte]) => pure(stream) 31 | ) 32 | ) 33 | 34 | ProcessGroup.ProcessGroupImpl( 35 | p1, 36 | List.empty, 37 | other, 38 | List(other, process) 39 | ) 40 | } 41 | 42 | /** Attaches the output of this process to an other process' input 43 | * 44 | * @param other 45 | * The other process 46 | * @return 47 | * Returns a [[ProcessGroup]] 48 | */ 49 | def |(other: Process.UnboundProcess): ProcessGroup.ProcessGroupImpl = 50 | pipeInto(other, identityPipe) 51 | 52 | /** Attaches the output of this process to an other process' input with a 53 | * custom channel 54 | * 55 | * There is a syntax helper step to allow the following syntax: 56 | * {{{ 57 | * val processGroup = process1.via(channel).to(process2) 58 | * }}} 59 | * 60 | * @param channel 61 | * Pipe between the two processes 62 | * @return 63 | * Returns a syntax helper trait that has a [[PipeBuilderSyntax.to]] 64 | * method to finish the construction 65 | */ 66 | def via( 67 | channel: ProxPipe[Byte, Byte] 68 | ): PipeBuilderSyntax[ProcessGroup.ProcessGroupImpl] = 69 | new PipeBuilderSyntax( 70 | new PipeBuilder[ProcessGroup.ProcessGroupImpl] { 71 | override def build( 72 | other: Process.UnboundProcess, 73 | channel: ProxPipe[Byte, Byte] 74 | ): ProcessGroup.ProcessGroupImpl = 75 | process.pipeInto(other, channel) 76 | }, 77 | channel 78 | ) 79 | } 80 | 81 | trait PipeBuilder[P] { 82 | def build(other: Process.UnboundProcess, channel: ProxPipe[Byte, Byte]): P 83 | } 84 | 85 | class PipeBuilderSyntax[P]( 86 | builder: PipeBuilder[P], 87 | channel: ProxPipe[Byte, Byte] 88 | ) { 89 | def to(other: Process.UnboundProcess): P = 90 | builder.build(other, channel) 91 | 92 | } 93 | 94 | /** String interpolator for an alternative of [[Process.apply]] 95 | * 96 | * {{{ 97 | * val process = proc"ls -hal $dir" 98 | * }}} 99 | */ 100 | implicit class ProcessStringContextIO(ctx: StringContext) { 101 | 102 | def proc(args: Any*): Process.ProcessImpl = { 103 | val staticParts = ctx.parts.map(Left.apply) 104 | val injectedParts = args.map(Right.apply) 105 | val parts = 106 | staticParts.zipAll(injectedParts, Left(""), Right("")).flatMap { 107 | case (a, b) => List(a, b) 108 | } 109 | val words = parts 110 | .flatMap { 111 | case Left(value) => value.trim.split(' ') 112 | case Right(value) => List(value.toString) 113 | } 114 | .filter(_.nonEmpty) 115 | .toList 116 | words match { 117 | case head :: remaining => 118 | Process(head, remaining) 119 | case Nil => 120 | throw new IllegalArgumentException( 121 | s"The proc interpolator needs at least a process name" 122 | ) 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /prox-fs2-3/src/main/scala/io/github/vigoo/prox/ProxFS2.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox 2 | 3 | import java.io 4 | 5 | import cats.effect.{Concurrent, Outcome, Async, Sync} 6 | import cats.{Applicative, ApplicativeError, FlatMap, Traverse} 7 | 8 | trait ProxFS2[F[_]] extends Prox { 9 | 10 | val instances: Sync[F] & Concurrent[F] 11 | 12 | override type ProxExitCode = cats.effect.ExitCode 13 | override type ProxFiber[A] = cats.effect.Fiber[F, Throwable, A] 14 | override type ProxIO[A] = F[A] 15 | override type ProxResource[A] = cats.effect.Resource[F, A] 16 | 17 | override type ProxPipe[A, B] = fs2.Pipe[F, A, B] 18 | override type ProxSink[A] = fs2.Pipe[F, A, Unit] 19 | override type ProxStream[A] = fs2.Stream[F, A] 20 | 21 | override type ProxMonoid[A] = cats.kernel.Monoid[A] 22 | 23 | protected override final def exitCodeFromInt(value: Int): ProxExitCode = 24 | cats.effect.ExitCode(value) 25 | 26 | protected override final def unit: ProxIO[Unit] = 27 | Applicative[F](instances).unit 28 | 29 | protected override final def pure[A](value: A): ProxIO[A] = 30 | Applicative[F](instances).pure(value) 31 | 32 | protected override final def effect[A]( 33 | f: => A, 34 | wrapError: Throwable => ProxError 35 | ): ProxIO[A] = { 36 | implicit val i: Sync[F] & Concurrent[F] = instances 37 | Sync[F].adaptError(Sync[F].delay(f)) { case failure: Throwable => 38 | wrapError(failure).toThrowable 39 | } 40 | } 41 | 42 | protected override final def blockingEffect[A]( 43 | f: => A, 44 | wrapError: Throwable => ProxError 45 | ): ProxIO[A] = { 46 | implicit val i: Sync[F] & Concurrent[F] = instances 47 | Sync[F].adaptError(Sync[F].interruptibleMany(f)) { 48 | case failure: Throwable => wrapError(failure).toThrowable 49 | } 50 | } 51 | 52 | protected override final def raiseError(error: ProxError): ProxIO[Unit] = 53 | ApplicativeError[F, Throwable](instances).raiseError(error.toThrowable) 54 | 55 | protected override final def ioMap[A, B]( 56 | io: ProxIO[A], 57 | f: A => B 58 | ): ProxIO[B] = Applicative[F](instances).map(io)(f) 59 | 60 | protected override final def ioFlatMap[A, B]( 61 | io: ProxIO[A], 62 | f: A => ProxIO[B] 63 | ): ProxIO[B] = FlatMap[F](instances).flatMap(io)(f) 64 | 65 | protected override final def traverse[A, B](list: List[A])( 66 | f: A => ProxIO[B] 67 | ): ProxIO[List[B]] = Traverse[List].traverse(list)(f)(instances) 68 | 69 | protected override final def identityPipe[A]: ProxPipe[A, A] = 70 | identity[ProxStream[A]] 71 | 72 | protected override final def bracket[A, B]( 73 | acquire: ProxIO[A] 74 | )(use: A => ProxIO[B])(fin: (A, IOResult) => ProxIO[Unit]): ProxIO[B] = 75 | Sync[F](instances).bracketCase(acquire)(use) { 76 | case (value, Outcome.Succeeded(_)) => fin(value, Completed) 77 | case (value, Outcome.Errored(error)) => 78 | fin(value, Failed(List(UnknownProxError(error)))) 79 | case (value, Outcome.Canceled()) => fin(value, Canceled) 80 | } 81 | 82 | protected override final def makeResource[A]( 83 | acquire: ProxIO[A], 84 | release: A => ProxIO[Unit] 85 | ): ProxResource[A] = cats.effect.Resource.make(acquire)(release)(instances) 86 | 87 | protected override final def useResource[A, B]( 88 | r: ProxResource[A], 89 | f: A => ProxIO[B] 90 | ): ProxIO[B] = r.use(f)(instances) 91 | 92 | protected override final def joinFiber[A](f: ProxFiber[A]): ProxIO[A] = 93 | f.joinWithNever(instances) 94 | 95 | protected override final def cancelFiber[A](f: ProxFiber[A]): ProxIO[Unit] = 96 | f.cancel 97 | 98 | protected override final def startFiber[A]( 99 | f: ProxIO[A] 100 | ): ProxIO[ProxFiber[A]] = { 101 | implicit val i: Sync[F] & Concurrent[F] = instances 102 | Concurrent[F].start(f) 103 | } 104 | 105 | protected override final def drainStream[A]( 106 | s: ProxStream[A] 107 | ): ProxIO[Unit] = { 108 | implicit val i: Sync[F] & Concurrent[F] = instances 109 | s.compile.drain 110 | } 111 | 112 | protected override final def streamToVector[A]( 113 | s: ProxStream[A] 114 | ): ProxIO[Vector[A]] = { 115 | implicit val i: Sync[F] & Concurrent[F] = instances 116 | s.compile.toVector 117 | } 118 | 119 | protected override final def foldStream[A, B]( 120 | s: ProxStream[A], 121 | init: B, 122 | f: (B, A) => B 123 | ): ProxIO[B] = { 124 | implicit val i: Sync[F] & Concurrent[F] = instances 125 | s.compile.fold(init)(f) 126 | } 127 | 128 | protected override final def foldMonoidStream[A: ProxMonoid]( 129 | s: ProxStream[A] 130 | ): ProxIO[A] = { 131 | implicit val i: Sync[F] & Concurrent[F] = instances 132 | s.compile.foldMonoid 133 | } 134 | 135 | protected override final def streamThrough[A, B]( 136 | s: ProxStream[A], 137 | pipe: ProxPipe[A, B] 138 | ): ProxStream[B] = s.through(pipe) 139 | 140 | protected override final def runStreamTo[A]( 141 | s: ProxStream[A], 142 | sink: ProxSink[A] 143 | ): ProxIO[Unit] = { 144 | implicit val i: Sync[F] & Concurrent[F] = instances 145 | s.through(sink).compile.drain 146 | } 147 | 148 | protected override final def fromJavaInputStream( 149 | input: io.InputStream, 150 | chunkSize: Int 151 | ): ProxStream[Byte] = 152 | fs2.io.readInputStream(pure(input), chunkSize, closeAfterUse = true)( 153 | instances 154 | ) 155 | 156 | protected override final def drainToJavaOutputStream( 157 | stream: ProxStream[Byte], 158 | output: io.OutputStream, 159 | flushChunks: Boolean 160 | ): ProxIO[Unit] = { 161 | implicit val i: Sync[F] & Concurrent[F] = instances 162 | stream 163 | .through( 164 | if (flushChunks) writeAndFlushOutputStream(output)(_).drain 165 | else 166 | fs2.io.writeOutputStream( 167 | effect(output, UnknownProxError.apply), 168 | closeAfterUse = true 169 | ) 170 | ) 171 | .compile 172 | .drain 173 | } 174 | 175 | private def writeAndFlushOutputStream( 176 | stream: java.io.OutputStream 177 | ): ProxPipe[Byte, Unit] = { 178 | implicit val i: Sync[F] & Concurrent[F] = instances 179 | s => { 180 | fs2.Stream 181 | .bracket(Applicative[F].pure(stream))(os => Sync[F].delay(os.close())) 182 | .flatMap { os => 183 | s.chunks.evalMap { chunk => 184 | Sync[F].blocking { 185 | os.write(chunk.toArray) 186 | os.flush() 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | object ProxFS2 { 195 | def apply[F[_]](implicit a: Async[F]): ProxFS2[F] = new ProxFS2[F] { 196 | override val instances: Sync[F] & Concurrent[F] = a 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /prox-fs2-3/src/test/scala/io/github/vigoo/prox/tests/fs2/InterpolatorSpecs.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox.tests.fs2 2 | 3 | import zio.test.* 4 | import zio.{Scope, ZIO} 5 | 6 | object InterpolatorSpecs extends ZIOSpecDefault with ProxSpecHelpers { 7 | override val spec: Spec[TestEnvironment & Scope, Any] = 8 | suite("Process interpolators")( 9 | suite("cats-effect process interpolator")( 10 | proxTest("works with single-word process names") { prox => 11 | import prox.* 12 | 13 | val process = proc"ls" 14 | 15 | ZIO.succeed( 16 | assertTrue( 17 | process.command == "ls", 18 | process.arguments.isEmpty 19 | ) 20 | ) 21 | }, 22 | proxTest("works with interpolated process name") { prox => 23 | import prox.* 24 | 25 | val cmd = "ls" 26 | val process = proc"$cmd" 27 | 28 | ZIO.succeed( 29 | assertTrue( 30 | process.command == "ls", 31 | process.arguments.isEmpty 32 | ) 33 | ) 34 | }, 35 | proxTest("works with static parameters") { prox => 36 | import prox.* 37 | 38 | val process = proc"ls -hal tmp" 39 | 40 | ZIO.succeed( 41 | assertTrue( 42 | process.command == "ls", 43 | process.arguments == List("-hal", "tmp") 44 | ) 45 | ) 46 | }, 47 | proxTest("works with static parameters and interpolated process name") { 48 | prox => 49 | import prox.* 50 | 51 | val cmd = "ls" 52 | val process = proc"$cmd -hal tmp" 53 | 54 | ZIO.succeed( 55 | assertTrue( 56 | process.command == "ls", 57 | process.arguments == List("-hal", "tmp") 58 | ) 59 | ) 60 | }, 61 | proxTest("works with static process name and interpolated parameters") { 62 | prox => 63 | import prox.* 64 | 65 | val p1 = "-hal" 66 | val p2 = "tmp" 67 | val process = proc"ls $p1 $p2" 68 | 69 | ZIO.succeed( 70 | assertTrue( 71 | process.command == "ls", 72 | process.arguments == List("-hal", "tmp") 73 | ) 74 | ) 75 | }, 76 | proxTest("works with interpolated name and parameters") { prox => 77 | import prox.* 78 | 79 | val cmd = "ls" 80 | val p1 = "-hal" 81 | val p2 = "tmp" 82 | val process = proc"$cmd $p1 $p2" 83 | 84 | ZIO.succeed( 85 | assertTrue( 86 | process.command == "ls", 87 | process.arguments == List("-hal", "tmp") 88 | ) 89 | ) 90 | }, 91 | proxTest("works with mixed static and interpolated parameters") { 92 | prox => 93 | import prox.* 94 | 95 | val p1 = "hello" 96 | val p2 = "dear visitor" 97 | val process = proc"echo $p1, $p2!!!" 98 | 99 | ZIO.succeed( 100 | assertTrue( 101 | process.command == "echo", 102 | process.arguments == List("hello", ",", "dear visitor", "!!!") 103 | ) 104 | ) 105 | } 106 | ) 107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /prox-fs2-3/src/test/scala/io/github/vigoo/prox/tests/fs2/ProxSpecHelpers.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox.tests.fs2 2 | 3 | import io.github.vigoo.prox.ProxFS2 4 | import zio.interop.catz.* 5 | import zio.test.* 6 | import zio.{Task, ZIO} 7 | 8 | import java.io.File 9 | 10 | trait ProxSpecHelpers { 11 | 12 | def proxTest(label: String)( 13 | assertion: ProxFS2[Task] => ZIO[Any, Throwable, TestResult] 14 | ): Spec[Any, scala.Throwable] = { 15 | test(label) { 16 | ZIO.runtime[Any].flatMap { implicit env => 17 | assertion(ProxFS2[Task]) 18 | } 19 | } 20 | } 21 | 22 | def withTempFile[A]( 23 | inner: File => ZIO[Any, Throwable, A] 24 | ): ZIO[Any, Throwable, A] = 25 | ZIO.acquireReleaseWith( 26 | ZIO.attempt(File.createTempFile("test", "txt")) 27 | )(file => ZIO.attempt(file.delete()).orDie)(inner) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /prox-java9/src/main/scala/io/github/vigoo/prox/java9/runner.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox.java9 2 | 3 | import java.lang.Process as JvmProcess 4 | 5 | import io.github.vigoo.prox.{FailedToQueryState, Prox} 6 | 7 | trait Java9Module { 8 | this: Prox => 9 | 10 | case class JVM9ProcessInfo(pid: Long) extends JVMProcessInfo 11 | 12 | class JVM9ProcessRunner() extends JVMProcessRunnerBase[JVM9ProcessInfo] { 13 | 14 | override protected def getProcessInfo( 15 | process: JvmProcess 16 | ): ProxIO[JVM9ProcessInfo] = 17 | effect(process.pid(), FailedToQueryState.apply).map(pid => 18 | JVM9ProcessInfo(pid) 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /prox-zstream-2/src/main/scala/io/github/vigoo/prox/ProxZStream.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox 2 | 3 | import java.io 4 | import java.io.IOException 5 | 6 | import zio.prelude.Identity 7 | import zio.stream.{ZSink, ZStream, ZPipeline} 8 | import zio.* 9 | 10 | import scala.language.implicitConversions 11 | 12 | trait ProxZStream extends Prox { 13 | 14 | case class TransformAndSink[A, B]( 15 | transform: ZStream[Any, ProxError, A] => ZStream[Any, ProxError, B], 16 | sink: ZSink[Any, ProxError, B, Any, Unit] 17 | ) { 18 | private[ProxZStream] def run( 19 | s: ZStream[Any, ProxError, A] 20 | ): ZIO[Any, ProxError, Unit] = 21 | transform(s).run(sink) 22 | } 23 | object TransformAndSink { 24 | def apply[A, B]( 25 | transducer: ZPipeline[Any, ProxError, A, B], 26 | sink: ZSink[Any, ProxError, B, Any, Unit] 27 | ): TransformAndSink[A, B] = 28 | TransformAndSink(_.via(transducer), sink) 29 | } 30 | 31 | override type ProxExitCode = zio.ExitCode 32 | override type ProxFiber[A] = zio.Fiber[ProxError, A] 33 | override type ProxIO[A] = ZIO[Any, ProxError, A] 34 | override type ProxResource[A] = ZIO[Scope, ProxError, A] 35 | override type ProxStream[A] = ZStream[Any, ProxError, A] 36 | override type ProxPipe[A, B] = ProxStream[A] => ProxStream[B] 37 | override type ProxSink[A] = TransformAndSink[A, ?] 38 | override type ProxMonoid[A] = zio.prelude.Identity[A] 39 | 40 | protected override final def exitCodeFromInt(value: Int): ProxExitCode = 41 | zio.ExitCode(value) 42 | 43 | protected override final def unit: ProxIO[Unit] = 44 | ZIO.unit 45 | 46 | protected override final def pure[A](value: A): ProxIO[A] = 47 | ZIO.succeed(value) 48 | 49 | protected override final def effect[A]( 50 | f: => A, 51 | wrapError: Throwable => ProxError 52 | ): ProxIO[A] = 53 | ZIO.attempt(f).mapError(wrapError) 54 | 55 | protected override final def blockingEffect[A]( 56 | f: => A, 57 | wrapError: Throwable => ProxError 58 | ): ProxIO[A] = 59 | ZIO.attemptBlockingInterrupt(f).mapError(wrapError).interruptible 60 | 61 | protected override final def raiseError(error: ProxError): ProxIO[Unit] = 62 | ZIO.fail(error) 63 | 64 | protected override final def ioMap[A, B]( 65 | io: ProxIO[A], 66 | f: A => B 67 | ): ProxIO[B] = 68 | io.map(f) 69 | 70 | protected override final def ioFlatMap[A, B]( 71 | io: ProxIO[A], 72 | f: A => ProxIO[B] 73 | ): ProxIO[B] = 74 | io.flatMap(f) 75 | 76 | protected override final def traverse[A, B](list: List[A])( 77 | f: A => ProxIO[B] 78 | ): ProxIO[List[B]] = 79 | ZIO.foreach(list)(f) 80 | 81 | protected override final def identityPipe[A]: ProxPipe[A, A] = 82 | identity 83 | 84 | protected override final def bracket[A, B]( 85 | acquire: ProxIO[A] 86 | )(use: A => ProxIO[B])(fin: (A, IOResult) => ProxIO[Unit]): ProxIO[B] = { 87 | ZIO.acquireReleaseExitWith(acquire) { 88 | (value: A, exit: Exit[ProxError, B]) => 89 | exit match { 90 | case Exit.Success(_) => 91 | fin(value, Completed).mapError(_.toThrowable).orDie 92 | case Exit.Failure(cause) => 93 | if (cause.isInterrupted) { 94 | fin(value, Canceled).mapError(_.toThrowable).orDie 95 | } else { 96 | fin( 97 | value, 98 | Failed( 99 | cause.failures ++ cause.defects.map(UnknownProxError.apply) 100 | ) 101 | ).mapError(_.toThrowable).orDie 102 | } 103 | } 104 | }(a => ZIO.allowInterrupt *> use(a)) 105 | } 106 | 107 | protected override final def makeResource[A]( 108 | acquire: ProxIO[A], 109 | release: A => ProxIO[Unit] 110 | ): ProxResource[A] = 111 | ZIO.acquireRelease(acquire)(x => release(x).mapError(_.toThrowable).orDie) 112 | 113 | protected override final def useResource[A, B]( 114 | r: ProxResource[A], 115 | f: A => ProxIO[B] 116 | ): ProxIO[B] = 117 | ZIO.scoped(r.flatMap(f)) 118 | 119 | protected override final def joinFiber[A](f: ProxFiber[A]): ProxIO[A] = 120 | f.join 121 | 122 | protected override final def cancelFiber[A](f: ProxFiber[A]): ProxIO[Unit] = 123 | f.interrupt.unit 124 | 125 | protected override final def drainStream[A](s: ProxStream[A]): ProxIO[Unit] = 126 | s.runDrain 127 | 128 | protected override final def streamToVector[A]( 129 | s: ProxStream[A] 130 | ): ProxIO[Vector[A]] = 131 | s.runCollect.map(_.toVector) 132 | 133 | protected override final def foldStream[A, B]( 134 | s: ProxStream[A], 135 | init: B, 136 | f: (B, A) => B 137 | ): ProxIO[B] = 138 | s.runFold(init)(f) 139 | 140 | protected override final def foldMonoidStream[A: Identity]( 141 | s: ProxStream[A] 142 | ): ProxIO[A] = 143 | s.runFold(Identity[A].identity)((a, b) => Identity[A].combine(a, b)) 144 | 145 | protected override final def streamThrough[A, B]( 146 | s: ProxStream[A], 147 | pipe: ProxPipe[A, B] 148 | ): ProxStream[B] = 149 | pipe(s) 150 | 151 | override protected final def runStreamTo[A]( 152 | s: ProxStream[A], 153 | sink: ProxSink[A] 154 | ): ProxIO[Unit] = 155 | sink.run(s) 156 | 157 | protected override final def fromJavaInputStream( 158 | input: io.InputStream, 159 | chunkSize: Int 160 | ): ProxStream[Byte] = 161 | ZStream 162 | .fromInputStream(input, chunkSize) 163 | .mapError(FailedToReadProcessOutput.apply) 164 | 165 | protected override final def drainToJavaOutputStream( 166 | stream: ProxStream[Byte], 167 | output: io.OutputStream, 168 | flushChunks: Boolean 169 | ): ProxIO[Unit] = { 170 | val managedOutput = 171 | ZIO.acquireRelease(ZIO.succeed(output))(s => ZIO.attempt(s.close()).orDie) 172 | if (flushChunks) { 173 | stream 174 | .run( 175 | flushingOutputStreamSink(managedOutput) 176 | .mapError(FailedToWriteProcessInput.apply) 177 | ) 178 | .unit 179 | } else { 180 | stream 181 | .run( 182 | ZSink 183 | .fromOutputStreamScoped(managedOutput) 184 | .mapError(FailedToWriteProcessInput.apply) 185 | ) 186 | .unit 187 | } 188 | } 189 | 190 | private final def flushingOutputStreamSink( 191 | managedOutput: ZIO[Scope, Nothing, io.OutputStream] 192 | ): ZSink[Any, IOException, Byte, Byte, Long] = 193 | ZSink.unwrapScoped { 194 | managedOutput.map { os => 195 | ZSink.foldLeftChunksZIO(0L) { (bytesWritten, byteChunk: Chunk[Byte]) => 196 | ZIO 197 | .attemptBlockingInterrupt { 198 | val bytes = byteChunk.toArray 199 | os.write(bytes) 200 | os.flush() 201 | bytesWritten + bytes.length 202 | } 203 | .refineOrDie { case e: IOException => 204 | e 205 | } 206 | } 207 | } 208 | } 209 | 210 | protected override final def startFiber[A]( 211 | f: ProxIO[A] 212 | ): ProxIO[ProxFiber[A]] = 213 | f.fork 214 | 215 | implicit def transducerAsPipe[A, B]( 216 | transducer: ZPipeline[Any, ProxError, A, B] 217 | ): ProxPipe[A, B] = 218 | (s: ProxStream[A]) => s.via(transducer) 219 | 220 | implicit def transducerAsPipeThrowable[A, B]( 221 | transducer: ZPipeline[Any, Throwable, A, B] 222 | ): ProxPipe[A, B] = 223 | (s: ProxStream[A]) => 224 | s.via( 225 | ZPipeline.fromChannel( 226 | transducer.channel.mapError(UnknownProxError.apply) 227 | ) 228 | ) 229 | 230 | implicit def sinkAsTransformAndSink[A]( 231 | sink: ZSink[Any, ProxError, A, Any, Unit] 232 | ): TransformAndSink[A, A] = 233 | TransformAndSink(identity[ZStream[Any, ProxError, A]] _, sink) 234 | } 235 | 236 | object zstream extends ProxZStream 237 | -------------------------------------------------------------------------------- /prox-zstream-2/src/test/scala/io/github/vigoo/prox/tests/zstream/ProcessGroupSpecs.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox.tests.zstream 2 | 3 | import io.github.vigoo.prox.zstream.* 4 | import io.github.vigoo.prox.{UnknownProxError, zstream} 5 | import zio.* 6 | import zio.stream.{ZPipeline, ZSink, ZStream} 7 | import zio.test.* 8 | import zio.test.Assertion.* 9 | import zio.test.TestAspect.* 10 | 11 | import java.nio.charset.StandardCharsets 12 | import java.nio.file.Files 13 | 14 | object ProcessGroupSpecs extends ZIOSpecDefault with ProxSpecHelpers { 15 | implicit val processRunner: ProcessRunner[JVMProcessInfo] = 16 | new JVMProcessRunner 17 | 18 | override val spec: Spec[TestEnvironment & Scope, Any] = 19 | suite("Piping processes together")( 20 | suite("Piping")( 21 | test("is possible with two") { 22 | 23 | val processGroup = (Process( 24 | "echo", 25 | List("This is a test string") 26 | ) | Process("wc", List("-w"))) ># ZPipeline.utf8Decode 27 | val program = processGroup.run().map(_.output.trim) 28 | 29 | program.map(r => assertTrue(r == "5")) 30 | }, 31 | test("is possible with multiple") { 32 | 33 | val processGroup = ( 34 | Process("echo", List("cat\ncat\ndog\napple")) | 35 | Process("sort") | 36 | Process("uniq", List("-c")) | 37 | Process("head", List("-n 1")) 38 | ) >? (ZPipeline.utf8Decode >>> ZPipeline.splitLines) 39 | 40 | val program = processGroup 41 | .run() 42 | .map(r => r.output.map(_.stripLineEnd.trim).filter(_.nonEmpty)) 43 | 44 | program.map(r => assert(r)(hasSameElements(List("1 apple")))) 45 | }, 46 | test("is customizable with pipes") { 47 | val customPipe = (s: zstream.ProxStream[Byte]) => 48 | s 49 | .via( 50 | ZPipeline.fromChannel( 51 | (ZPipeline.utf8Decode >>> ZPipeline.splitLines).channel 52 | .mapError(UnknownProxError.apply) 53 | ) 54 | ) 55 | .map(_.split(' ').toVector) 56 | .map(v => v.map(_ + " !!!").mkString(" ")) 57 | .intersperse("\n") 58 | .flatMap(s => 59 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 60 | ) 61 | val processGroup = Process("echo", List("This is a test string")) 62 | .via(customPipe) 63 | .to(Process("wc", List("-w"))) ># ZPipeline.utf8Decode 64 | val program = processGroup.run().map(_.output.trim) 65 | 66 | program.map(r => assertTrue(r == "10")) 67 | }, 68 | test("can be mapped") { 69 | import zstream.Process.* 70 | 71 | val processGroup1 = (Process( 72 | "!echo", 73 | List("This is a test string") 74 | ) | Process("!wc", List("-w"))) ># ZPipeline.utf8Decode 75 | val processGroup2 = 76 | processGroup1.map(new ProcessGroup.Mapper[String, Unit] { 77 | override def mapFirst[ 78 | P <: Process[zstream.ProxStream[Byte], Unit] 79 | ](process: P): P = 80 | process.withCommand(process.command.tail).asInstanceOf[P] 81 | 82 | override def mapInnerWithIdx[ 83 | P <: UnboundIProcess[zstream.ProxStream[Byte], Unit] 84 | ](process: P, idx: Int): P = 85 | process.withCommand(process.command.tail).asInstanceOf[P] 86 | 87 | override def mapLast[P <: UnboundIProcess[String, Unit]]( 88 | process: P 89 | ): P = process.withCommand(process.command.tail).asInstanceOf[P] 90 | }) 91 | 92 | val program = processGroup2.run().map(_.output.trim) 93 | 94 | program.map(r => assertTrue(r == "5")) 95 | } 96 | ), 97 | suite("Termination")( 98 | test("can be terminated with cancellation") { 99 | 100 | val processGroup = 101 | Process( 102 | "perl", 103 | List("-e", """$SIG{TERM} = sub { exit 1 }; sleep 30; exit 0""") 104 | ) | 105 | Process("sort") 106 | val program = ZIO.scoped { 107 | processGroup.start().flatMap { fiber => fiber.interrupt.unit } 108 | } 109 | 110 | program.as(assertCompletes) 111 | } @@ TestAspect.timeout(5.seconds), 112 | test("can be terminated") { 113 | 114 | val p1 = Process( 115 | "perl", 116 | List("-e", """$SIG{TERM} = sub { exit 1 }; sleep 30; exit 0""") 117 | ) 118 | val p2 = Process("sort") 119 | val processGroup = p1 | p2 120 | 121 | val program = for { 122 | runningProcesses <- processGroup.startProcessGroup() 123 | _ <- ZIO.sleep(250.millis) 124 | result <- runningProcesses.terminate() 125 | } yield result.exitCodes.toList 126 | 127 | program.map(r => 128 | assert(r)( 129 | contains[(Process[Unit, Unit], ProxExitCode)](p1 -> ExitCode(1)) 130 | ) 131 | ) 132 | } @@ withLiveClock, 133 | test("can be killed") { 134 | 135 | val p1 = Process( 136 | "perl", 137 | List("-e", """$SIG{TERM} = 'IGNORE'; sleep 30; exit 2""") 138 | ) 139 | val p2 = Process("sort") 140 | val processGroup = p1 | p2 141 | 142 | val program = for { 143 | runningProcesses <- processGroup.startProcessGroup() 144 | _ <- ZIO.sleep(250.millis) 145 | result <- runningProcesses.kill() 146 | } yield result.exitCodes 147 | 148 | // Note: we can't assert on the second process' exit code because there is a race condition 149 | // between killing it directly and being stopped because of the upstream process got killed. 150 | program.map(r => assert(r)(contains(p1 -> ExitCode(137)))) 151 | } @@ withLiveClock 152 | ), 153 | suite("Input redirection")( 154 | test("can be fed with an input stream") { 155 | 156 | val stream = ZStream("This is a test string").flatMap(s => 157 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 158 | ) 159 | val processGroup = (Process("cat") | Process( 160 | "wc", 161 | List("-w") 162 | )) < stream ># ZPipeline.utf8Decode 163 | val program = processGroup.run().map(_.output.trim) 164 | 165 | program.map(r => assertTrue(r == "5")) 166 | }, 167 | test("can be fed with an input file") { 168 | 169 | withTempFile { tempFile => 170 | val program = for { 171 | _ <- ZIO 172 | .attempt( 173 | Files.write( 174 | tempFile.toPath, 175 | "This is a test string".getBytes("UTF-8") 176 | ) 177 | ) 178 | .mapError(UnknownProxError.apply) 179 | processGroup = (Process("cat") | Process( 180 | "wc", 181 | List("-w") 182 | )) < tempFile.toPath ># ZPipeline.utf8Decode 183 | result <- processGroup.run() 184 | } yield result.output.trim 185 | 186 | program.map(r => assertTrue(r == "5")) 187 | } 188 | } 189 | ), 190 | suite("Output redirection")( 191 | test("output can be redirected to file") { 192 | 193 | withTempFile { tempFile => 194 | val processGroup = (Process( 195 | "echo", 196 | List("This is a test string") 197 | ) | Process("wc", List("-w"))) > tempFile.toPath 198 | val program = for { 199 | _ <- processGroup.run() 200 | contents <- ZStream 201 | .fromFile(tempFile, 1024) 202 | .via(ZPipeline.utf8Decode) 203 | .runFold("")(_ + _) 204 | .mapError(UnknownProxError.apply) 205 | } yield contents.trim 206 | 207 | program.map(r => assertTrue(r == "5")) 208 | } 209 | } 210 | ), 211 | suite("Error redirection")( 212 | test("can redirect each error output to a stream") { 213 | 214 | val p1 = Process("perl", List("-e", """print STDERR "Hello"""")) 215 | val p2 = Process("perl", List("-e", """print STDERR "world"""")) 216 | val processGroup = (p1 | p2) !># ZPipeline.utf8Decode 217 | val program = processGroup.run() 218 | 219 | program.map { result => 220 | assert(result.errors.get(p1))(isSome(equalTo("Hello"))) && 221 | assert(result.errors.get(p2))(isSome(equalTo("world"))) && 222 | assert(result.output)(equalTo(())) && 223 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 224 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 225 | } 226 | }, 227 | test("can redirect each error output to a sink") { 228 | 229 | val builder = new StringBuilder 230 | val target: zstream.ProxSink[Byte] = ZSink.foreach((byte: Byte) => 231 | ZIO 232 | .attempt(builder.append(byte.toChar)) 233 | .mapError(UnknownProxError.apply) 234 | ) 235 | 236 | val p1 = Process("perl", List("-e", """print STDERR "Hello"""")) 237 | val p2 = Process("perl", List("-e", """print STDERR "world"""")) 238 | val processGroup = (p1 | p2) !> target 239 | val program = processGroup.run() 240 | 241 | program.map { result => 242 | assert(result.errors.get(p1))(isSome(equalTo(()))) && 243 | assert(result.errors.get(p2))(isSome(equalTo(()))) && 244 | assert(result.output)(equalTo(())) && 245 | assert(builder.toString.toSeq.sorted)( 246 | equalTo("Helloworld".toSeq.sorted) 247 | ) && 248 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 249 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 250 | } 251 | }, 252 | test("can redirect each error output to a vector") { 253 | 254 | val p1 = Process("perl", List("-e", """print STDERR "Hello"""")) 255 | val p2 = Process("perl", List("-e", """print STDERR "world!"""")) 256 | 257 | val stream = 258 | ZPipeline.utf8Decode >>> ZPipeline.splitLines >>> ZPipeline 259 | .map[String, Int](_.length) 260 | 261 | val processGroup = (p1 | p2) !>? stream 262 | val program = processGroup.run() 263 | 264 | program.map { result => 265 | assert(result.errors.get(p1))(isSome(hasSameElements(List(5)))) && 266 | assert(result.errors.get(p2))(isSome(hasSameElements(List(6)))) && 267 | assert(result.output)(equalTo(())) && 268 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 269 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 270 | } 271 | }, 272 | test("can drain each error output") { 273 | 274 | val p1 = Process("perl", List("-e", """print STDERR "Hello"""")) 275 | val p2 = Process("perl", List("-e", """print STDERR "world"""")) 276 | val processGroup = (p1 | p2) drainErrors ZPipeline.utf8Decode 277 | val program = processGroup.run() 278 | 279 | program.map { result => 280 | assert(result.errors.get(p1))(isSome(equalTo(()))) && 281 | assert(result.errors.get(p2))(isSome(equalTo(()))) && 282 | assert(result.output)(equalTo(())) && 283 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 284 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 285 | } 286 | }, 287 | test("can fold each error output") { 288 | 289 | val p1 = Process("perl", List("-e", "print STDERR 'Hello\nworld'")) 290 | val p2 = Process("perl", List("-e", "print STDERR 'Does\nit\nwork?'")) 291 | val processGroup = (p1 | p2).foldErrors( 292 | ZPipeline.utf8Decode >>> ZPipeline.splitLines, 293 | Vector.empty, 294 | (l: Vector[Option[Char]], s: String) => l :+ s.headOption 295 | ) 296 | val program = processGroup.run() 297 | 298 | program.map { result => 299 | assert(result.errors.get(p1))( 300 | isSome(equalTo(Vector(Some('H'), Some('w')))) 301 | ) && 302 | assert(result.errors.get(p2))( 303 | isSome(equalTo(Vector(Some('D'), Some('i'), Some('w')))) 304 | ) && 305 | assert(result.output)(equalTo(())) && 306 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 307 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 308 | } 309 | } 310 | ), 311 | suite("Error redirection customized per process")( 312 | test( 313 | "can redirect each error output to a stream customized per process" 314 | ) { 315 | 316 | val p1 = Process("perl", List("-e", """print STDERR "Hello"""")) 317 | val p2 = Process("perl", List("-e", """print STDERR "world"""")) 318 | val processGroup = (p1 | p2).customizedPerProcess.errorsToFoldMonoid { 319 | case p if p == p1 => 320 | ZPipeline.utf8Decode >>> ZPipeline.map(s => "P1: " + s) 321 | case p if p == p2 => 322 | ZPipeline.utf8Decode >>> ZPipeline.map(s => "P2: " + s) 323 | } 324 | val program = processGroup.run() 325 | 326 | program.map { result => 327 | assert(result.errors.get(p1))(isSome(equalTo("P1: HelloP1: "))) && 328 | assert(result.errors.get(p2))(isSome(equalTo("P2: worldP2: "))) && 329 | assert(result.output)(equalTo(())) && 330 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 331 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 332 | } 333 | }, 334 | test( 335 | "can redirect each error output to a sink customized per process" 336 | ) { 337 | 338 | val builder1 = new StringBuilder 339 | val builder2 = new StringBuilder 340 | 341 | val p1 = Process("perl", List("-e", """print STDERR "Hello"""")) 342 | val p2 = Process("perl", List("-e", """print STDERR "world"""")) 343 | val processGroup = (p1 | p2).customizedPerProcess.errorsToSink { 344 | case p if p == p1 => 345 | ZSink.foreach((byte: Byte) => 346 | ZIO 347 | .attempt(builder1.append(byte.toChar)) 348 | .mapError(UnknownProxError.apply) 349 | ) 350 | case p if p == p2 => 351 | ZSink.foreach((byte: Byte) => 352 | ZIO 353 | .attempt(builder2.append(byte.toChar)) 354 | .mapError(UnknownProxError.apply) 355 | ) 356 | } 357 | val program = processGroup.run() 358 | 359 | program.map { result => 360 | assert(result.errors.get(p1))(isSome(equalTo(()))) && 361 | assert(result.errors.get(p2))(isSome(equalTo(()))) && 362 | assert(result.output)(equalTo(())) && 363 | assert(builder1.toString)(equalTo("Hello")) && 364 | assert(builder2.toString)(equalTo("world")) && 365 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 366 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 367 | } 368 | }, 369 | test( 370 | "can redirect each error output to a vector customized per process" 371 | ) { 372 | 373 | val p1 = Process("perl", List("-e", """print STDERR "Hello"""")) 374 | val p2 = Process("perl", List("-e", """print STDERR "world!"""")) 375 | 376 | val stream = 377 | ZPipeline.utf8Decode >>> ZPipeline.splitLines >>> ZPipeline 378 | .map[String, Int](_.length) 379 | 380 | val processGroup = (p1 | p2).customizedPerProcess.errorsToVector { 381 | case p if p == p1 => stream >>> ZPipeline.map(l => (1, l)) 382 | case p if p == p2 => stream >>> ZPipeline.map(l => (2, l)) 383 | } 384 | val program = processGroup.run() 385 | 386 | program.map { result => 387 | assert(result.errors.get(p1))( 388 | isSome(hasSameElements(List((1, 5)))) 389 | ) && 390 | assert(result.errors.get(p2))( 391 | isSome(hasSameElements(List((2, 6)))) 392 | ) && 393 | assert(result.output)(equalTo(())) && 394 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 395 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 396 | } 397 | }, 398 | test("can drain each error output customized per process") { 399 | 400 | val p1 = Process("perl", List("-e", """print STDERR "Hello"""")) 401 | val p2 = Process("perl", List("-e", """print STDERR "world"""")) 402 | val processGroup = (p1 | p2).customizedPerProcess.drainErrors(_ => 403 | ZPipeline.utf8Decode 404 | ) 405 | val program = processGroup.run() 406 | 407 | program.map { result => 408 | assert(result.errors.get(p1))(isSome(equalTo(()))) && 409 | assert(result.errors.get(p2))(isSome(equalTo(()))) && 410 | assert(result.output)(equalTo(())) && 411 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 412 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 413 | } 414 | }, 415 | test("can fold each error output customized per process") { 416 | 417 | val p1 = Process("perl", List("-e", "print STDERR 'Hello\nworld'")) 418 | val p2 = Process("perl", List("-e", "print STDERR 'Does\nit\nwork?'")) 419 | val processGroup = (p1 | p2).customizedPerProcess.foldErrors( 420 | { 421 | case p if p == p1 => ZPipeline.utf8Decode >>> ZPipeline.splitLines 422 | case p if p == p2 => 423 | ZPipeline.utf8Decode >>> ZPipeline.splitLines >>> ZPipeline 424 | .map[String, String](_.reverse) 425 | }, 426 | Vector.empty, 427 | (l: Vector[Option[Char]], s: String) => l :+ s.headOption 428 | ) 429 | val program = processGroup.run() 430 | 431 | program.map { result => 432 | assert(result.errors.get(p1))( 433 | isSome(equalTo(Vector(Some('H'), Some('w')))) 434 | ) && 435 | assert(result.errors.get(p2))( 436 | isSome(equalTo(Vector(Some('s'), Some('t'), Some('?')))) 437 | ) && 438 | assert(result.output)(equalTo(())) && 439 | assert(result.exitCodes.get(p1))(isSome(equalTo(ExitCode(0)))) && 440 | assert(result.exitCodes.get(p2))(isSome(equalTo(ExitCode(0)))) 441 | } 442 | }, 443 | test("can redirect each error output to file") { 444 | 445 | withTempFile { tempFile1 => 446 | withTempFile { tempFile2 => 447 | val p1 = Process("perl", List("-e", """print STDERR "Hello"""")) 448 | val p2 = Process("perl", List("-e", """print STDERR "world"""")) 449 | val processGroup = (p1 | p2).customizedPerProcess.errorsToFile { 450 | case p if p == p1 => tempFile1.toPath 451 | case p if p == p2 => tempFile2.toPath 452 | } 453 | val program = for { 454 | _ <- processGroup.run() 455 | contents1 <- ZStream 456 | .fromFile(tempFile1, 1024) 457 | .via(ZPipeline.utf8Decode) 458 | .runFold("")(_ + _) 459 | .mapError(UnknownProxError.apply) 460 | contents2 <- ZStream 461 | .fromFile(tempFile2, 1024) 462 | .via(ZPipeline.utf8Decode) 463 | .runFold("")(_ + _) 464 | .mapError(UnknownProxError.apply) 465 | } yield (contents1, contents2) 466 | 467 | program.map(r => assertTrue(r == ("Hello", "world"))) 468 | } 469 | } 470 | } 471 | ), 472 | suite("Redirection ordering")( 473 | test( 474 | "can redirect each error output to a stream if fed with an input stream and redirected to an output stream" 475 | ) { 476 | 477 | val stream = ZStream("This is a test string").flatMap(s => 478 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 479 | ) 480 | val p1 = Process( 481 | "perl", 482 | List( 483 | "-e", 484 | """my $str=<>; print STDERR Hello; print STDOUT "$str"""" 485 | ) 486 | ) 487 | val p2 = Process("sort") 488 | val p3 = Process("wc", List("-w")) 489 | val processGroup = 490 | (p1 | p2 | p3) < stream ># ZPipeline.utf8Decode !># ZPipeline.utf8Decode 491 | 492 | processGroup 493 | .run() 494 | .map { result => 495 | assert(result.errors.get(p1))(isSome(equalTo("Hello"))) && 496 | assert(result.errors.get(p2))(isSome(equalTo(""))) && 497 | assert(result.errors.get(p3))(isSome(equalTo(""))) && 498 | assert(result.output.trim)(equalTo("5")) 499 | } 500 | }, 501 | test( 502 | "can redirect output if each error output and input are already redirected" 503 | ) { 504 | 505 | val stream = ZStream("This is a test string").flatMap(s => 506 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 507 | ) 508 | val p1 = Process( 509 | "perl", 510 | List( 511 | "-e", 512 | """my $str=<>; print STDERR Hello; print STDOUT "$str"""" 513 | ) 514 | ) 515 | val p2 = Process("sort") 516 | val p3 = Process("wc", List("-w")) 517 | val processGroup = 518 | ((p1 | p2 | p3) < stream !># ZPipeline.utf8Decode) ># ZPipeline.utf8Decode 519 | 520 | processGroup 521 | .run() 522 | .map { result => 523 | assert(result.errors.get(p1))(isSome(equalTo("Hello"))) && 524 | assert(result.errors.get(p2))(isSome(equalTo(""))) && 525 | assert(result.errors.get(p3))(isSome(equalTo(""))) && 526 | assert(result.output.trim)(equalTo("5")) 527 | } 528 | }, 529 | test( 530 | "can attach output and then input stream if each error output and standard output are already redirected" 531 | ) { 532 | 533 | val stream = ZStream("This is a test string").flatMap(s => 534 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 535 | ) 536 | val p1 = Process( 537 | "perl", 538 | List( 539 | "-e", 540 | """my $str=<>; print STDERR Hello; print STDOUT "$str"""" 541 | ) 542 | ) 543 | val p2 = Process("sort") 544 | val p3 = Process("wc", List("-w")) 545 | val processGroup = 546 | ((p1 | p2 | p3) !># ZPipeline.utf8Decode) ># ZPipeline.utf8Decode < stream 547 | 548 | processGroup 549 | .run() 550 | .map { result => 551 | assert(result.errors.get(p1))(isSome(equalTo("Hello"))) && 552 | assert(result.errors.get(p2))(isSome(equalTo(""))) && 553 | assert(result.errors.get(p3))(isSome(equalTo(""))) && 554 | assert(result.output.trim)(equalTo("5")) 555 | } 556 | }, 557 | test( 558 | "can attach input and then output stream if each error output and standard output are already redirected" 559 | ) { 560 | 561 | val stream = ZStream("This is a test string").flatMap(s => 562 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 563 | ) 564 | val p1 = Process( 565 | "perl", 566 | List( 567 | "-e", 568 | """my $str=<>; print STDERR Hello; print STDOUT "$str"""" 569 | ) 570 | ) 571 | val p2 = Process("sort") 572 | val p3 = Process("wc", List("-w")) 573 | val processGroup = 574 | ((p1 | p2 | p3) !># ZPipeline.utf8Decode) < stream ># ZPipeline.utf8Decode 575 | 576 | processGroup 577 | .run() 578 | .map { result => 579 | assert(result.errors.get(p1))(isSome(equalTo("Hello"))) && 580 | assert(result.errors.get(p2))(isSome(equalTo(""))) && 581 | assert(result.errors.get(p3))(isSome(equalTo(""))) && 582 | assert(result.output.trim)(equalTo("5")) 583 | } 584 | }, 585 | test( 586 | "can attach input stream and errors if standard output is already redirected" 587 | ) { 588 | 589 | val stream = ZStream("This is a test string").flatMap(s => 590 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 591 | ) 592 | val p1 = Process( 593 | "perl", 594 | List( 595 | "-e", 596 | """my $str=<>; print STDERR Hello; print STDOUT "$str"""" 597 | ) 598 | ) 599 | val p2 = Process("sort") 600 | val p3 = Process("wc", List("-w")) 601 | val processGroup = 602 | ((p1 | p2 | p3) ># ZPipeline.utf8Decode) < stream !># ZPipeline.utf8Decode 603 | 604 | processGroup 605 | .run() 606 | .map { result => 607 | assert(result.errors.get(p1))(isSome(equalTo("Hello"))) && 608 | assert(result.errors.get(p2))(isSome(equalTo(""))) && 609 | assert(result.errors.get(p3))(isSome(equalTo(""))) && 610 | assert(result.output.trim)(equalTo("5")) 611 | } 612 | }, 613 | test( 614 | "can attach errors and finally input stream if standard output is already redirected" 615 | ) { 616 | 617 | val stream = ZStream("This is a test string").flatMap(s => 618 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 619 | ) 620 | val p1 = Process( 621 | "perl", 622 | List( 623 | "-e", 624 | """my $str=<>; print STDERR Hello; print STDOUT "$str"""" 625 | ) 626 | ) 627 | val p2 = Process("sort") 628 | val p3 = Process("wc", List("-w")) 629 | val processGroup = 630 | (((p1 | p2 | p3) ># ZPipeline.utf8Decode) !># ZPipeline.utf8Decode) < stream 631 | 632 | processGroup 633 | .run() 634 | .map { result => 635 | assert(result.errors.get(p1))(isSome(equalTo("Hello"))) && 636 | assert(result.errors.get(p2))(isSome(equalTo(""))) && 637 | assert(result.errors.get(p3))(isSome(equalTo(""))) && 638 | assert(result.output.trim)(equalTo("5")) 639 | } 640 | } 641 | ), 642 | test("bound process is not pipeable") { 643 | assertZIO( 644 | typeCheck( 645 | """val bad = (Process("echo", List("Hello world")) ># ZPipeline.utf8Decode) | Process("wc", List("-w"))""" 646 | ) 647 | )( 648 | isLeft(anything) 649 | ) 650 | } 651 | ) @@ timeoutWarning(60.seconds) @@ sequential 652 | } 653 | -------------------------------------------------------------------------------- /prox-zstream-2/src/test/scala/io/github/vigoo/prox/tests/zstream/ProcessSpecs.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox.tests.zstream 2 | 3 | import io.github.vigoo.prox.zstream.* 4 | import io.github.vigoo.prox.{UnknownProxError, zstream} 5 | import zio.stream.{ZPipeline, ZSink, ZStream} 6 | import zio.test.* 7 | import zio.test.Assertion.{anything, equalTo, hasSameElements, isLeft} 8 | import zio.test.TestAspect.* 9 | import zio.* 10 | 11 | import java.nio.charset.StandardCharsets 12 | import java.nio.file.Files 13 | 14 | object ProcessSpecs extends ZIOSpecDefault with ProxSpecHelpers { 15 | implicit val processRunner: ProcessRunner[JVMProcessInfo] = 16 | new JVMProcessRunner 17 | 18 | override val spec: Spec[TestEnvironment & Scope, Any] = 19 | suite("Executing a process")( 20 | test("returns the exit code") { 21 | val program = for { 22 | trueResult <- Process("true").run() 23 | falseResult <- Process("false").run() 24 | } yield (trueResult.exitCode, falseResult.exitCode) 25 | 26 | program.map(r => assertTrue(r == (ExitCode(0), ExitCode(1)))) 27 | }, 28 | suite("Output redirection")( 29 | test("can redirect output to a file") { 30 | withTempFile { tempFile => 31 | val process = 32 | Process("echo", List("Hello world!")) > tempFile.toPath 33 | val program = for { 34 | _ <- process.run() 35 | contents <- ZStream 36 | .fromFile(tempFile, 1024) 37 | .via(ZPipeline.utf8Decode) 38 | .runFold("")(_ + _) 39 | .mapError(UnknownProxError.apply) 40 | } yield contents 41 | 42 | program.map(r => assertTrue(r == "Hello world!\n")) 43 | } 44 | }, 45 | test("can redirect output to append a file") { 46 | withTempFile { tempFile => 47 | val process1 = Process("echo", List("Hello")) > tempFile.toPath 48 | val process2 = Process("echo", List("world")) >> tempFile.toPath 49 | val program = for { 50 | _ <- process1.run() 51 | _ <- process2.run() 52 | contents <- ZStream 53 | .fromFile(tempFile, 1024) 54 | .via(ZPipeline.utf8Decode) 55 | .runFold("")(_ + _) 56 | .mapError(UnknownProxError.apply) 57 | } yield contents 58 | 59 | program.map(r => assertTrue(r == "Hello\nworld\n")) 60 | } 61 | }, 62 | test("can redirect output to stream") { 63 | val process = 64 | Process("echo", List("Hello world!")) ># ZPipeline.utf8Decode 65 | val program = process.run().map(_.output) 66 | 67 | program.map(r => assertTrue(r == "Hello world!\n")) 68 | }, 69 | test("can redirect output to stream folding monoid") { 70 | val process = Process( 71 | "echo", 72 | List("Hello\nworld!") 73 | ) ># (ZPipeline.utf8Decode >>> ZPipeline.splitLines) 74 | val program = process.run().map(_.output) 75 | 76 | program.map(r => assertTrue(r == "Helloworld!")) 77 | }, 78 | test("can redirect output to stream collected to vector") { 79 | case class StringLength(value: Int) 80 | 81 | val stream = 82 | ZPipeline.utf8Decode >>> 83 | ZPipeline.splitLines >>> 84 | ZPipeline.map[String, StringLength](s => StringLength(s.length)) 85 | 86 | val process = Process("echo", List("Hello\nworld!")) >? stream 87 | val program = process.run().map(_.output) 88 | 89 | program.map(r => 90 | assert(r)( 91 | hasSameElements(List(StringLength(5), StringLength(6))) 92 | ) 93 | ) 94 | }, 95 | test("can redirect output to stream and ignore it's result") { 96 | val process = Process("echo", List("Hello\nworld!")) 97 | .drainOutput(ZPipeline.utf8Decode >>> ZPipeline.splitLines) 98 | val program = process.run().map(_.output) 99 | 100 | program.as(assertCompletes) 101 | }, 102 | test("can redirect output to stream and fold it") { 103 | val process = Process("echo", List("Hello\nworld!")).foldOutput( 104 | ZPipeline.utf8Decode >>> ZPipeline.splitLines, 105 | Vector.empty, 106 | (l: Vector[Option[Char]], s: String) => l :+ s.headOption 107 | ) 108 | val program = process.run().map(_.output) 109 | 110 | program.map(r => assertTrue(r == Vector(Some('H'), Some('w')))) 111 | }, 112 | test("can redirect output to a sink") { 113 | val builder = new StringBuilder 114 | val target: zstream.ProxSink[Byte] = ZSink.foreach((byte: Byte) => 115 | ZIO 116 | .attempt(builder.append(byte.toChar)) 117 | .mapError(UnknownProxError.apply) 118 | ) 119 | 120 | val process = Process("echo", List("Hello world!")) > target 121 | val program = process.run().as(builder.toString) 122 | 123 | program.map(r => assertTrue(r == "Hello world!\n")) 124 | } 125 | ), 126 | suite("Error redirection")( 127 | test("can redirect error to a file") { 128 | withTempFile { tempFile => 129 | val process = Process( 130 | "perl", 131 | List("-e", "print STDERR 'Hello world!'") 132 | ) !> tempFile.toPath 133 | val program = for { 134 | _ <- process.run() 135 | contents <- ZStream 136 | .fromFile(tempFile, 1024) 137 | .via(ZPipeline.utf8Decode) 138 | .runFold("")(_ + _) 139 | .mapError(UnknownProxError.apply) 140 | } yield contents 141 | 142 | program.map(r => assertTrue(r == "Hello world!")) 143 | } 144 | }, 145 | test("can redirect error to append a file") { 146 | withTempFile { tempFile => 147 | val process1 = Process( 148 | "perl", 149 | List("-e", "print STDERR Hello") 150 | ) !> tempFile.toPath 151 | val process2 = Process( 152 | "perl", 153 | List("-e", "print STDERR world") 154 | ) !>> tempFile.toPath 155 | val program = for { 156 | _ <- process1.run() 157 | _ <- process2.run() 158 | contents <- ZStream 159 | .fromFile(tempFile, 1024) 160 | .via(ZPipeline.utf8Decode) 161 | .runFold("")(_ + _) 162 | .mapError(UnknownProxError.apply) 163 | } yield contents 164 | 165 | program.map(r => assertTrue(r == "Helloworld")) 166 | } 167 | }, 168 | test("can redirect error to stream") { 169 | val process = Process( 170 | "perl", 171 | List("-e", """print STDERR "Hello"""") 172 | ) !># ZPipeline.utf8Decode 173 | val program = process.run().map(_.error) 174 | 175 | program.map(r => assertTrue(r == "Hello")) 176 | }, 177 | test("can redirect error to stream folding monoid") { 178 | val process = Process( 179 | "perl", 180 | List("-e", "print STDERR 'Hello\nworld!'") 181 | ) !># (ZPipeline.utf8Decode >>> ZPipeline.splitLines) 182 | val program = process.run().map(_.error) 183 | 184 | program.map(r => assertTrue(r == "Helloworld!")) 185 | }, 186 | test("can redirect error to stream collected to vector") { 187 | case class StringLength(value: Int) 188 | 189 | val stream = 190 | ZPipeline.utf8Decode >>> 191 | ZPipeline.splitLines >>> 192 | ZPipeline.map[String, StringLength](s => StringLength(s.length)) 193 | 194 | val process = Process( 195 | "perl", 196 | List("-e", "print STDERR 'Hello\nworld!'") 197 | ) !>? stream 198 | val program = process.run().map(_.error) 199 | 200 | program.map(r => 201 | assert(r)( 202 | hasSameElements(List(StringLength(5), StringLength(6))) 203 | ) 204 | ) 205 | }, 206 | test("can redirect error to stream and ignore it's result") { 207 | val process = 208 | Process("perl", List("-e", "print STDERR 'Hello\nworld!'")) 209 | .drainError(ZPipeline.utf8Decode >>> ZPipeline.splitLines) 210 | val program = process.run().map(_.error) 211 | 212 | program.as(assertCompletes) 213 | }, 214 | test("can redirect error to stream and fold it") { 215 | val process = Process( 216 | "perl", 217 | List("-e", "print STDERR 'Hello\nworld!'") 218 | ).foldError( 219 | ZPipeline.utf8Decode >>> ZPipeline.splitLines, 220 | Vector.empty, 221 | (l: Vector[Option[Char]], s: String) => l :+ s.headOption 222 | ) 223 | val program = process.run().map(_.error) 224 | 225 | program.map(r => assertTrue(r == Vector(Some('H'), Some('w')))) 226 | }, 227 | test("can redirect error to a sink") { 228 | val builder = new StringBuilder 229 | val target: zstream.ProxSink[Byte] = ZSink.foreach((byte: Byte) => 230 | ZIO 231 | .attempt(builder.append(byte.toChar)) 232 | .mapError(UnknownProxError.apply) 233 | ) 234 | 235 | val process = 236 | Process("perl", List("-e", """print STDERR "Hello"""")) !> target 237 | val program = process.run().as(builder.toString) 238 | 239 | program.map(r => assertTrue(r == "Hello")) 240 | } 241 | ), 242 | suite("Redirection ordering")( 243 | test("can redirect first input and then error to stream") { 244 | val source = ZStream("This is a test string").flatMap(s => 245 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 246 | ) 247 | val process = Process( 248 | "perl", 249 | List("-e", """my $str = <>; print STDERR "$str"""".stripMargin) 250 | ) < source !># ZPipeline.utf8Decode 251 | val program = process.run().map(_.error) 252 | 253 | program.map(r => assertTrue(r == "This is a test string")) 254 | }, 255 | test("can redirect error first then output to stream") { 256 | val process = (Process( 257 | "perl", 258 | List("-e", """print STDOUT Hello; print STDERR World""".stripMargin) 259 | ) !># ZPipeline.utf8Decode) ># ZPipeline.utf8Decode 260 | val program = process.run().map(r => r.output + r.error) 261 | 262 | program.map(r => assertTrue(r == "HelloWorld")) 263 | }, 264 | test("can redirect output first then error to stream") { 265 | val process = (Process( 266 | "perl", 267 | List("-e", """print STDOUT Hello; print STDERR World""".stripMargin) 268 | ) ># ZPipeline.utf8Decode) !># ZPipeline.utf8Decode 269 | val program = process.run().map(r => r.output + r.error) 270 | 271 | program.map(r => assertTrue(r == "HelloWorld")) 272 | }, 273 | test("can redirect output first then error finally input to stream") { 274 | val source = ZStream("Hello").flatMap(s => 275 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 276 | ) 277 | val process = ((Process( 278 | "perl", 279 | List( 280 | "-e", 281 | """my $str = <>; print STDOUT "$str"; print STDERR World""".stripMargin 282 | ) 283 | ) 284 | ># ZPipeline.utf8Decode) 285 | !># ZPipeline.utf8Decode) < source 286 | val program = process.run().map(r => r.output + r.error) 287 | 288 | program.map(r => assertTrue(r == "HelloWorld")) 289 | }, 290 | test("can redirect output first then input finally error to stream") { 291 | val source = ZStream("Hello").flatMap(s => 292 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 293 | ) 294 | val process = ((Process( 295 | "perl", 296 | List( 297 | "-e", 298 | """my $str = <>; print STDOUT "$str"; print STDERR World""".stripMargin 299 | ) 300 | ) 301 | ># ZPipeline.utf8Decode) 302 | < source) !># ZPipeline.utf8Decode 303 | val program = process.run().map(r => r.output + r.error) 304 | 305 | program.map(r => assertTrue(r == "HelloWorld")) 306 | }, 307 | test("can redirect input first then error finally output to stream") { 308 | val source = ZStream("Hello").flatMap(s => 309 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 310 | ) 311 | val process = ((Process( 312 | "perl", 313 | List( 314 | "-e", 315 | """my $str = <>; print STDOUT "$str"; print STDERR World""".stripMargin 316 | ) 317 | ) 318 | < source) 319 | !># ZPipeline.utf8Decode) ># ZPipeline.utf8Decode 320 | val program = process.run().map(r => r.output + r.error) 321 | 322 | program.map(r => assertTrue(r == "HelloWorld")) 323 | } 324 | ), 325 | suite("Input redirection")( 326 | test("can use stream as input") { 327 | val source = ZStream("This is a test string").flatMap(s => 328 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 329 | ) 330 | val process = 331 | Process("wc", List("-w")) < source ># ZPipeline.utf8Decode 332 | val program = process.run().map(_.output.trim) 333 | 334 | program.map(r => assertTrue(r == "5")) 335 | }, 336 | test("can use stream as input flushing after each chunk") { 337 | val source = ZStream("This ", "is a test", " string").flatMap(s => 338 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 339 | ) 340 | val process = 341 | (Process("wc", List("-w")) !< source) ># ZPipeline.utf8Decode 342 | val program = process.run().map(_.output.trim) 343 | 344 | program.map(r => assertTrue(r == "5")) 345 | } 346 | ), 347 | suite("Termination")( 348 | test("can be terminated with cancellation") { 349 | val process = Process( 350 | "perl", 351 | List("-e", """$SIG{TERM} = sub { exit 1 }; sleep 30; exit 0""") 352 | ) 353 | val program = ZIO.scoped { 354 | process.start().flatMap { fiber => 355 | fiber.interrupt.unit.delay(250.millis) 356 | } 357 | } 358 | 359 | program.as(assertCompletes) 360 | } @@ TestAspect.withLiveClock @@ TestAspect.timeout( 361 | 5.seconds 362 | ) @@ TestAspect.diagnose(2.seconds), 363 | test("can be terminated by releasing the resource") { 364 | val process = Process( 365 | "perl", 366 | List("-e", """$SIG{TERM} = sub { exit 1 }; sleep 30; exit 0""") 367 | ) 368 | val program = ZIO.scoped { 369 | process.start() *> ZIO.sleep(250.millis) 370 | } 371 | 372 | program.as(assertCompletes) 373 | } @@ TestAspect.withLiveClock @@ TestAspect.timeout(5.seconds), 374 | test("can be terminated") { 375 | val process = Process( 376 | "perl", 377 | List("-e", """$SIG{TERM} = sub { exit 1 }; sleep 30; exit 0""") 378 | ) 379 | val program = for { 380 | runningProcess <- process.startProcess() 381 | _ <- ZIO.sleep(250.millis) 382 | result <- runningProcess.terminate() 383 | } yield result.exitCode 384 | 385 | program.map(r => assertTrue(r == ExitCode(1))) 386 | } @@ withLiveClock, 387 | test("can be killed") { 388 | val process = Process( 389 | "perl", 390 | List("-e", """$SIG{TERM} = 'IGNORE'; sleep 30; exit 2""") 391 | ) 392 | val program = for { 393 | runningProcess <- process.startProcess() 394 | _ <- ZIO.sleep(250.millis) 395 | result <- runningProcess.kill() 396 | } yield result.exitCode 397 | 398 | program.map(r => assertTrue(r == ExitCode(137))) 399 | } @@ withLiveClock, 400 | test("can be checked if is alive") { 401 | val process = Process("sleep", List("10")) 402 | val program = for { 403 | runningProcess <- process.startProcess() 404 | isAliveBefore <- runningProcess.isAlive 405 | _ <- runningProcess.terminate() 406 | isAliveAfter <- runningProcess.isAlive 407 | } yield (isAliveBefore, isAliveAfter) 408 | 409 | program.map(r => assertTrue(r == (true, false))) 410 | } 411 | ), 412 | suite("Customization")( 413 | test("can change the command") { 414 | val p1 = 415 | Process("something", List("Hello", "world")) ># ZPipeline.utf8Decode 416 | val p2 = p1.withCommand("echo") 417 | val program = p2.run().map(_.output) 418 | 419 | program.map(r => assertTrue(r == "Hello world\n")) 420 | }, 421 | test("can change the arguments") { 422 | val p1 = Process("echo") ># ZPipeline.utf8Decode 423 | val p2 = p1.withArguments(List("Hello", "world")) 424 | val program = p2.run().map(_.output) 425 | 426 | program.map(r => assertTrue(r == "Hello world\n")) 427 | }, 428 | test("respects the working directory") { 429 | ZIO.attempt(Files.createTempDirectory("prox")).flatMap { 430 | tempDirectory => 431 | val process = 432 | (Process("pwd") in tempDirectory) ># ZPipeline.utf8Decode 433 | val program = process.run().map(_.output.trim) 434 | 435 | program.map(r => 436 | assert(r)( 437 | equalTo(tempDirectory.toString) || equalTo( 438 | s"/private${tempDirectory}" 439 | ) 440 | ) 441 | ) 442 | } 443 | }, 444 | test("is customizable with environment variables") { 445 | val process = 446 | (Process("sh", List("-c", "echo \"Hello $TEST1! I am $TEST2!\"")) 447 | `with` ("TEST1" -> "world") 448 | `with` ("TEST2" -> "prox")) ># ZPipeline.utf8Decode 449 | val program = process.run().map(_.output) 450 | 451 | program.map(r => assertTrue(r == "Hello world! I am prox!\n")) 452 | }, 453 | test("is customizable with excluded environment variables") { 454 | val process = 455 | (Process("sh", List("-c", "echo \"Hello $TEST1! I am $TEST2!\"")) 456 | `with` ("TEST1" -> "world") 457 | `with` ("TEST2" -> "prox") 458 | `without` "TEST1") ># ZPipeline.utf8Decode 459 | val program = process.run().map(_.output) 460 | 461 | program.map(r => assertTrue(r == "Hello ! I am prox!\n")) 462 | }, 463 | test("is customizable with environment variables output is bound") { 464 | val process = (Process( 465 | "sh", 466 | List("-c", "echo \"Hello $TEST1! I am $TEST2!\"") 467 | ) ># ZPipeline.utf8Decode 468 | `with` ("TEST1" -> "world") 469 | `with` ("TEST2" -> "prox")) 470 | val program = process.run().map(_.output) 471 | 472 | program.map(r => assertTrue(r == "Hello world! I am prox!\n")) 473 | }, 474 | test("is customizable with environment variables if input is bound") { 475 | val source = ZStream("This is a test string").flatMap(s => 476 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 477 | ) 478 | val process = ((Process( 479 | "sh", 480 | List("-c", "cat > /dev/null; echo \"Hello $TEST1! I am $TEST2!\"") 481 | ) < source) 482 | `with` ("TEST1" -> "world") 483 | `with` ("TEST2" -> "prox")) ># ZPipeline.utf8Decode 484 | val program = process.run().map(_.output) 485 | 486 | program.map(r => assertTrue(r == "Hello world! I am prox!\n")) 487 | }, 488 | test("is customizable with environment variables if error is bound") { 489 | val process = ((Process( 490 | "sh", 491 | List("-c", "echo \"Hello $TEST1! I am $TEST2!\"") 492 | ) !># ZPipeline.utf8Decode) 493 | `with` ("TEST1" -> "world") 494 | `with` ("TEST2" -> "prox")) ># ZPipeline.utf8Decode 495 | val program = process.run().map(_.output) 496 | 497 | program.map(r => assertTrue(r == "Hello world! I am prox!\n")) 498 | }, 499 | test( 500 | "is customizable with environment variables if input and output are bound" 501 | ) { 502 | val source = ZStream("This is a test string").flatMap(s => 503 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 504 | ) 505 | val process = (((Process( 506 | "sh", 507 | List("-c", "cat > /dev/null; echo \"Hello $TEST1! I am $TEST2!\"") 508 | ) < source) ># ZPipeline.utf8Decode) 509 | `with` ("TEST1" -> "world") 510 | `with` ("TEST2" -> "prox")) 511 | val program = process.run().map(_.output) 512 | 513 | program.map(r => assertTrue(r == "Hello world! I am prox!\n")) 514 | }, 515 | test( 516 | "is customizable with environment variables if input and error are bound" 517 | ) { 518 | val source = ZStream("This is a test string").flatMap(s => 519 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 520 | ) 521 | val process = (((Process( 522 | "sh", 523 | List("-c", "cat > /dev/null; echo \"Hello $TEST1! I am $TEST2!\"") 524 | ) < source) !># ZPipeline.utf8Decode) 525 | `with` ("TEST1" -> "world") 526 | `with` ("TEST2" -> "prox")) ># ZPipeline.utf8Decode 527 | val program = process.run().map(_.output) 528 | 529 | program.map(r => assertTrue(r == "Hello world! I am prox!\n")) 530 | }, 531 | test( 532 | "is customizable with environment variables if output and error are bound" 533 | ) { 534 | val process = (((Process( 535 | "sh", 536 | List("-c", "echo \"Hello $TEST1! I am $TEST2!\"") 537 | ) !># ZPipeline.utf8Decode) ># ZPipeline.utf8Decode) 538 | `with` ("TEST1" -> "world") 539 | `with` ("TEST2" -> "prox")) 540 | val program = process.run().map(_.output) 541 | 542 | program.map(r => assertTrue(r == "Hello world! I am prox!\n")) 543 | }, 544 | test( 545 | "is customizable with environment variables if everything is bound" 546 | ) { 547 | val source = ZStream("This is a test string").flatMap(s => 548 | ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)) 549 | ) 550 | val process = ((((Process( 551 | "sh", 552 | List("-c", "cat > /dev/null; echo \"Hello $TEST1! I am $TEST2!\"") 553 | ) < source) !># ZPipeline.utf8Decode) ># ZPipeline.utf8Decode) 554 | `with` ("TEST1" -> "world") 555 | `with` ("TEST2" -> "prox")) 556 | val program = process.run().map(_.output) 557 | 558 | program.map(r => assertTrue(r == "Hello world! I am prox!\n")) 559 | } 560 | ), 561 | test("double output redirect is illegal") { 562 | assertZIO( 563 | typeCheck( 564 | """val bad = Process("echo", List("Hello world")) > new File("x").toPath > new File("y").toPath""" 565 | ) 566 | )( 567 | isLeft(anything) 568 | ) 569 | }, 570 | test("double error redirect is illegal") { 571 | assertZIO( 572 | typeCheck( 573 | """val bad = Process("echo", List("Hello world")) !> new File("x").toPath !> new File("y").toPath""" 574 | ) 575 | )( 576 | isLeft(anything) 577 | ) 578 | }, 579 | test("double input redirect is illegal") { 580 | assertZIO( 581 | typeCheck( 582 | """val bad = (Process("echo", List("Hello world")) < ZStream("X").flatMap(s => ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)))) < ZStream("Y").flatMap(s => ZStream.fromIterable(s.getBytes(StandardCharsets.UTF_8)))""" 583 | ) 584 | )( 585 | isLeft(anything) 586 | ) 587 | } 588 | ) @@ timeout(60.seconds) @@ sequential 589 | } 590 | -------------------------------------------------------------------------------- /prox-zstream-2/src/test/scala/io/github/vigoo/prox/tests/zstream/ProxSpecHelpers.scala: -------------------------------------------------------------------------------- 1 | package io.github.vigoo.prox.tests.zstream 2 | 3 | import java.io.File 4 | 5 | import io.github.vigoo.prox.{ProxError, UnknownProxError} 6 | import zio.ZIO 7 | 8 | trait ProxSpecHelpers { 9 | 10 | def withTempFile[A]( 11 | inner: File => ZIO[Any, ProxError, A] 12 | ): ZIO[Any, ProxError, A] = 13 | ZIO.acquireReleaseWith( 14 | ZIO 15 | .attempt(File.createTempFile("test", "txt")) 16 | .mapError(UnknownProxError.apply) 17 | )(file => ZIO.attempt(file.delete()).orDie)(inner) 18 | } 19 | -------------------------------------------------------------------------------- /scripts/decrypt_keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | openssl aes-256-cbc -K $encrypted_4abe426e5bff_key -iv $encrypted_4abe426e5bff_iv -in travis-deploy-key.enc -out travis-deploy-key -d 4 | chmod 600 travis-deploy-key 5 | cp travis-deploy-key ~/.ssh/id_rsa 6 | -------------------------------------------------------------------------------- /scripts/publish_microsite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | set -e 4 | 5 | git config --global user.email "daniel.vigovszky@gmail.com" 6 | git config --global user.name "Daniel Vigovszky" 7 | git config --global push.default simple 8 | 9 | sbt docs/publishMicrosite 10 | --------------------------------------------------------------------------------