Replace CommandOutput with StderrOutput option to choose to merge or ignore sterr, etc.

This commit is contained in:
Dillon Kearns 2024-04-13 14:57:29 -07:00
parent e2c75207eb
commit 9c4f42dfae
7 changed files with 143 additions and 134 deletions

File diff suppressed because one or more lines are too long

View File

@ -105,23 +105,28 @@ b =
(.body >> Expect.equal "src")
, Stream.fromString "invalid elm module"
|> Stream.pipe
(Stream.commandWithOptions (defaultCommandOptions |> Stream.allowNon0Status)
(Stream.commandWithOptions
(defaultCommandOptions
|> Stream.allowNon0Status
|> Stream.withOutput Stream.MergeStderrAndStdout
)
"elm-format"
[ "--stdin" ]
)
|> Stream.read
|> try
|> test "stderr"
(Expect.equal
{ body = ""
, metadata =
{ exitCode = 1
, stdout = ""
, stderr = "Unable to parse file <STDIN>:1:13 To see a detailed explanation, run elm make on the file.\n"
, combined = "Unable to parse file <STDIN>:1:13 To see a detailed explanation, run elm make on the file.\n"
}
}
)
(.body >> Expect.equal "Unable to parse file <STDIN>:1:13 To see a detailed explanation, run elm make on the file.\n")
, Stream.commandWithOptions
(defaultCommandOptions
|> Stream.allowNon0Status
)
"elm-review"
[ "--report=json" ]
|> Stream.readJson (Decode.field "type" Decode.string)
|> try
|> test "elm-review"
(.body >> Expect.equal "review-errors")
]

View File

@ -22,6 +22,7 @@ import * as zlib from "node:zlib";
import { Readable } from "node:stream";
import * as validateStream from "./validate-stream.js";
import { default as makeFetchHappenOriginal } from "make-fetch-happen";
import mergeStreams from "@sindresorhus/merge-streams";
let verbosity = 2;
const spinnies = new Spinnies();
@ -678,26 +679,15 @@ function runStream(req, portsFile) {
})
);
} else if (kind === "none") {
// lastStream.once("finish", async () => {
// resolve(jsonResponse(req, null));
// });
lastStream.once("close", async () => {
resolve(
jsonResponse(req, {
body: null,
metadata: await metadataResponse,
})
);
});
resolve(
jsonResponse(req, {
body: null,
metadata: await metadataResponse,
})
);
} else if (kind === "command") {
// already handled in parts.forEach
}
// lastStream.once("error", (error) => {
// console.log('Stream error!');
// console.error(error);
// reject(jsonResponse(req, null));
// });
} catch (error) {
if (lastStream) {
lastStream.destroy();
@ -788,43 +778,49 @@ async function pipePartToStream(
};
return { metadata, stream: response.body };
} else if (part.name === "command") {
const { command, args, allowNon0Status } = part;
const { command, args, allowNon0Status, output } = part;
/**
* @type {import('node:child_process').ChildProcess}
*/
let stderrKind = "pipe";
if (output === "Ignore") {
stderrKind = "ignore";
} else if (output === "Print") {
stderrKind = "inherit";
}
const newProcess = spawnCallback(command, args, {
stdio: ["pipe", "pipe", "pipe"],
stdio: [
"pipe",
// if we are capturing stderr instead of stdout, print out stdout with `inherit`
output === "InsteadOfStdout" ? "inherit" : "pipe",
stderrKind,
],
cwd: cwd,
env: env,
});
// newProcess.on("error", (error) => {
// console.error("ERROR in pipeline!", error);
// process.exit(1);
// });
let stderrOutput = "";
let combinedOutput = "";
newProcess.stderr.on("data", (data) => {
stderrOutput += data;
combinedOutput += data;
});
lastStream && lastStream.pipe(newProcess.stdin);
let newStream;
if (output === "MergeWithStdout") {
newStream = mergeStreams([newProcess.stdout, newProcess.stderr]);
} else if (output === "InsteadOfStdout") {
newStream = newProcess.stderr;
} else {
newStream = newProcess.stdout;
}
if (isLastProcess) {
return {
stream: newProcess.stdout,
stream: newStream,
metadata: new Promise((resolve) => {
newProcess.on("exit", (code) => {
resolve({
exitCode: code,
stdoutOutput: "",
stderrOutput,
combinedOutput,
});
});
}),
};
} else {
return { metadata: null, stream: newProcess.stdout };
return { metadata: null, stream: newStream };
}
} else if (part.name === "fromString") {
return { stream: Readable.from([part.string]) };

18
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "3.0.12",
"license": "BSD-3-Clause",
"dependencies": {
"@sindresorhus/merge-streams": "^3.0.0",
"busboy": "^1.6.0",
"chokidar": "^3.5.3",
"cli-cursor": "^4.0.0",
@ -982,9 +983,9 @@
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz",
"integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-3.0.0.tgz",
"integrity": "sha512-5Muw0TDzXvK/i0BmrL1tiTsb6Sh/DXe/e5d63GpmHWr59t7rUyQhhiIuw605q/yvJxyBf6gMWmsxCC2fqtcFvQ==",
"engines": {
"node": ">=18"
},
@ -4225,6 +4226,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globby/node_modules/@sindresorhus/merge-streams": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz",
"integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",

View File

@ -25,6 +25,7 @@
"author": "Dillon Kearns",
"license": "BSD-3-Clause",
"dependencies": {
"@sindresorhus/merge-streams": "^3.0.0",
"busboy": "^1.6.0",
"chokidar": "^3.5.3",
"cli-cursor": "^4.0.0",

View File

@ -1,13 +1,12 @@
module BackendTask.Stream exposing
( Stream
, fileRead, fileWrite, fromString, http, httpWithInput, pipe, stdin, stdout, stderr, gzip, unzip
, CommandOutput
, command
, read, readJson, run
, Error(..)
, commandWithOptions
, CommandOptions, defaultCommandOptions, allowNon0Status, inheritUnused, withOutput, withTimeout
, OutputChannel(..)
, StderrOutput(..)
, CommandOptions, defaultCommandOptions, allowNon0Status, withOutput, withTimeout
, customRead, customWrite, customDuplex
)
@ -49,8 +48,6 @@ End example
@docs fileRead, fileWrite, fromString, http, httpWithInput, pipe, stdin, stdout, stderr, gzip, unzip
@docs CommandOutput
## Shell Commands
@ -68,9 +65,9 @@ End example
@docs commandWithOptions
@docs CommandOptions, defaultCommandOptions, allowNon0Status, inheritUnused, withOutput, withTimeout
@docs StderrOutput
@docs OutputChannel
@docs CommandOptions, defaultCommandOptions, allowNon0Status, withOutput, withTimeout
## Custom Streams
@ -99,9 +96,11 @@ type alias Recoverable error =
{ fatal : FatalError, recoverable : error }
mapRecoverable : { a | fatal : b, recoverable : c } -> { fatal : b, recoverable : Error c }
mapRecoverable { fatal, recoverable } =
{ fatal = fatal, recoverable = CustomError recoverable }
mapRecoverable : Maybe body -> { a | fatal : b, recoverable : c } -> { fatal : b, recoverable : Error c body }
mapRecoverable maybeBody { fatal, recoverable } =
{ fatal = fatal
, recoverable = CustomError recoverable maybeBody
}
type StreamPart
@ -285,15 +284,15 @@ fromString string =
{-| -}
type Error error
type Error error body
= StreamError String
| CustomError error
| CustomError error (Maybe body)
{-| -}
read :
Stream error metadata { read : (), write : write }
-> BackendTask { fatal : FatalError, recoverable : Error error } { metadata : metadata, body : String }
-> BackendTask { fatal : FatalError, recoverable : Error error String } { metadata : metadata, body : String }
read ((Stream ( decoderName, decoder ) pipeline) as stream) =
BackendTask.Internal.Request.request
{ name = "stream"
@ -331,7 +330,12 @@ read ((Stream ( decoderName, decoder ) pipeline) as stream) =
(Decode.field "body" Decode.string)
Err error ->
error |> mapRecoverable |> Err |> Decode.succeed
Decode.field "body" Decode.string
|> Decode.maybe
|> Decode.map
(\body ->
error |> mapRecoverable body |> Err
)
)
]
)
@ -356,7 +360,7 @@ decodeLog decoder =
readJson :
Decoder value
-> Stream error metadata { read : (), write : write }
-> BackendTask { fatal : FatalError, recoverable : Error error } { metadata : metadata, body : value }
-> BackendTask { fatal : FatalError, recoverable : Error error value } { metadata : metadata, body : value }
readJson decoder ((Stream ( decoderName, metadataDecoder ) pipeline) as stream) =
BackendTask.Internal.Request.request
{ name = "stream"
@ -365,13 +369,22 @@ readJson decoder ((Stream ( decoderName, metadataDecoder ) pipeline) as stream)
BackendTask.Http.expectJson
(Decode.field "metadata" metadataDecoder
|> Decode.andThen
(\result ->
case result of
Ok metadata ->
(\result1 ->
let
bodyResult : Decoder (Result Decode.Error value)
bodyResult =
Decode.field "body" Decode.value
|> Decode.map
(\bodyValue ->
case Decode.decodeValue decoder bodyValue of
Decode.decodeValue decoder bodyValue
)
in
bodyResult
|> Decode.map
(\result ->
case result1 of
Ok metadata ->
case result of
Ok body ->
Ok
{ metadata = metadata
@ -379,17 +392,18 @@ readJson decoder ((Stream ( decoderName, metadataDecoder ) pipeline) as stream)
}
Err decoderError ->
Err
(FatalError.recoverable
{ title = "Failed to decode body"
, body = "Failed to decode body"
}
(StreamError (Decode.errorToString decoderError))
)
)
FatalError.recoverable
{ title = "Failed to decode body"
, body = "Failed to decode body"
}
(StreamError (Decode.errorToString decoderError))
|> Err
Err error ->
error |> mapRecoverable |> Err |> Decode.succeed
Err error ->
error
|> mapRecoverable (Result.toMaybe result)
|> Err
)
)
)
}
@ -403,32 +417,27 @@ readBytes stream =
{-| -}
command : String -> List String -> Stream Int CommandOutput { read : read, write : write }
command : String -> List String -> Stream Int () { read : read, write : write }
command command_ args_ =
commandWithOptions defaultCommandOptions command_ args_
commandDecoder : Bool -> ( String, Decoder (Result (Recoverable Int) CommandOutput) )
commandDecoder : Bool -> ( String, Decoder (Result (Recoverable Int) ()) )
commandDecoder allowNon0 =
( "command"
, commandOutputDecoder
|> Decode.map
(\output ->
if output.exitCode == 0 || allowNon0 || True then
Ok
{ stdout = output.stdout
, stderr = output.stderr
, combined = output.combined
, exitCode = output.exitCode
}
(\exitCode ->
if exitCode == 0 || allowNon0 || True then
Ok ()
else
Err
(FatalError.recoverable
{ title = "Command Failed"
, body = "Command failed with exit code " ++ String.fromInt output.exitCode
, body = "Command failed with exit code " ++ String.fromInt exitCode
}
output.exitCode
exitCode
)
)
)
@ -439,7 +448,7 @@ commandDecoder allowNon0 =
{-| -}
commandWithOptions : CommandOptions -> String -> List String -> Stream Int CommandOutput { read : read, write : write }
commandWithOptions : CommandOptions -> String -> List String -> Stream Int () { read : read, write : write }
commandWithOptions (CommandOptions options) command_ args_ =
single (commandDecoder options.allowNon0Status)
"command"
@ -461,21 +470,13 @@ nullable encoder maybeValue =
Encode.null
{-| -}
type OutputChannel
= Stdout
| Stderr
| Both
{-| -}
type CommandOptions
= CommandOptions CommandOptions_
type alias CommandOptions_ =
{ output : OutputChannel
, inheritUnused : Bool
{ output : StderrOutput
, allowNon0Status : Bool
, timeoutInMs : Maybe Int
}
@ -485,15 +486,14 @@ type alias CommandOptions_ =
defaultCommandOptions : CommandOptions
defaultCommandOptions =
CommandOptions
{ output = Stdout
, inheritUnused = False
{ output = PrintStderr
, allowNon0Status = False
, timeoutInMs = Nothing
}
{-| -}
withOutput : OutputChannel -> CommandOptions -> CommandOptions
withOutput : StderrOutput -> CommandOptions -> CommandOptions
withOutput output (CommandOptions cmd) =
CommandOptions { cmd | output = output }
@ -510,45 +510,37 @@ withTimeout timeoutMs (CommandOptions cmd) =
CommandOptions { cmd | timeoutInMs = Just timeoutMs }
{-| -}
inheritUnused : CommandOptions -> CommandOptions
inheritUnused (CommandOptions cmd) =
CommandOptions { cmd | inheritUnused = True }
encodeChannel : OutputChannel -> Encode.Value
encodeChannel : StderrOutput -> Encode.Value
encodeChannel output =
Encode.string
(case output of
Stdout ->
"stdout"
IgnoreStderr ->
"Ignore"
Stderr ->
"stderr"
PrintStderr ->
"Print"
Both ->
"both"
MergeStderrAndStdout ->
"MergeWithStdout"
StderrInsteadOfStdout ->
"InsteadOfStdout"
)
{-| -}
type alias CommandOutput =
{ stdout : String
, stderr : String
, combined : String
, exitCode : Int
}
commandOutputDecoder : Decoder CommandOutput
commandOutputDecoder : Decoder Int
commandOutputDecoder =
Decode.map4 CommandOutput
(Decode.field "stdoutOutput" Decode.string)
(Decode.field "stderrOutput" Decode.string)
(Decode.field "combinedOutput" Decode.string)
(Decode.field "exitCode" Decode.int)
Decode.field "exitCode" Decode.int
commandToString : String -> List String -> String
commandToString command_ args_ =
command_ ++ " " ++ String.join " " args_
{-| -}
type StderrOutput
= IgnoreStderr
| PrintStderr
| MergeStderrAndStdout
| StderrInsteadOfStdout

View File

@ -43,7 +43,7 @@ Read more about using the `elm-pages` CLI to run (or bundle) scripts, plus a bri
import BackendTask exposing (BackendTask)
import BackendTask.Http
import BackendTask.Internal.Request
import BackendTask.Stream as Stream
import BackendTask.Stream as Stream exposing (defaultCommandOptions)
import Cli.OptionsParser as OptionsParser
import Cli.Program as Program
import FatalError exposing (FatalError)
@ -250,7 +250,10 @@ exec command_ args_ =
{-| -}
command : String -> List String -> BackendTask FatalError String
command command_ args_ =
Stream.command command_ args_
Stream.commandWithOptions
(defaultCommandOptions |> Stream.withOutput Stream.MergeStderrAndStdout)
command_
args_
|> Stream.read
|> BackendTask.map (.metadata >> .combined)
|> BackendTask.map .body
|> BackendTask.allowFatal