├── .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 | 
3 | [](https://codecov.io/gh/vigoo/prox)
4 | [](http://www.apache.org/licenses/LICENSE-2.0)
5 | [](https://index.scala-lang.org/vigoo/prox/prox-core)
6 | 
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 |
5 |
--------------------------------------------------------------------------------
/docs/src/microsite/img/third-feature-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------