Skip to content

Commit cf05b25

Browse files
Fix text output returned despite --output json in detached mode (#1588)
* Initial plan * Fix text output returned despite --output json in detached mode Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> * Use consistent JSONL envelope format for --output json in detached mode Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> Co-authored-by: Waldek Mastykarz <waldek@mastykarz.nl>
1 parent b0d7b34 commit cf05b25

1 file changed

Lines changed: 152 additions & 21 deletions

File tree

DevProxy/Program.cs

Lines changed: 152 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
using DevProxy.Proxy;
88
using DevProxy.State;
99
using System.Diagnostics;
10+
using System.Globalization;
1011
using System.Net;
1112
using System.Runtime.InteropServices;
1213
using System.Text;
14+
using System.Text.Json;
15+
using System.Text.Json.Serialization;
1316

1417
// Handle detached mode - spawn a child process and exit
1518
// Only applies to root command (starting the proxy), not subcommands
@@ -46,12 +49,22 @@
4649

4750
static async Task<int> StartDetachedProcessAsync(string[] args)
4851
{
52+
var isJsonOutput = IsJsonOutputRequested(args);
53+
4954
// Check if an instance is already running
5055
if (await StateManager.IsInstanceRunningAsync())
5156
{
5257
var existingState = await StateManager.LoadStateAsync();
53-
await Console.Error.WriteLineAsync($"Dev Proxy is already running (PID: {existingState?.Pid}).");
54-
await Console.Error.WriteLineAsync("Use 'devproxy stop' to stop it first.");
58+
if (isJsonOutput)
59+
{
60+
await Console.Error.WriteLineAsync(
61+
FormatJsonLogEntry("error", $"Dev Proxy is already running (PID: {existingState?.Pid}). Use 'devproxy stop' to stop it first."));
62+
}
63+
else
64+
{
65+
await Console.Error.WriteLineAsync($"Dev Proxy is already running (PID: {existingState?.Pid}).");
66+
await Console.Error.WriteLineAsync("Use 'devproxy stop' to stop it first.");
67+
}
5568
return 1;
5669
}
5770

@@ -68,7 +81,15 @@ static async Task<int> StartDetachedProcessAsync(string[] args)
6881
var executablePath = Environment.ProcessPath;
6982
if (string.IsNullOrEmpty(executablePath))
7083
{
71-
await Console.Error.WriteLineAsync("Could not determine executable path.");
84+
if (isJsonOutput)
85+
{
86+
await Console.Error.WriteLineAsync(
87+
FormatJsonLogEntry("error", "Could not determine executable path."));
88+
}
89+
else
90+
{
91+
await Console.Error.WriteLineAsync("Could not determine executable path.");
92+
}
7293
return 1;
7394
}
7495

@@ -94,7 +115,15 @@ static async Task<int> StartDetachedProcessAsync(string[] args)
94115
var process = Process.Start(startInfo);
95116
if (process == null)
96117
{
97-
await Console.Error.WriteLineAsync("Failed to start Dev Proxy process.");
118+
if (isJsonOutput)
119+
{
120+
await Console.Error.WriteLineAsync(
121+
FormatJsonLogEntry("error", "Failed to start Dev Proxy process."));
122+
}
123+
else
124+
{
125+
await Console.Error.WriteLineAsync("Failed to start Dev Proxy process.");
126+
}
98127
return 1;
99128
}
100129

@@ -109,15 +138,27 @@ static async Task<int> StartDetachedProcessAsync(string[] args)
109138
var state = await StateManager.LoadStateAsync();
110139
if (state != null)
111140
{
112-
await Console.Out.WriteLineAsync("Dev Proxy started in background.");
113-
await Console.Out.WriteLineAsync();
114-
await Console.Out.WriteLineAsync($" PID: {state.Pid}");
115-
await Console.Out.WriteLineAsync($" API URL: {state.ApiUrl}");
116-
await Console.Out.WriteLineAsync($" Log file: {state.LogFile}");
117-
await Console.Out.WriteLineAsync();
118-
await Console.Out.WriteLineAsync("Use 'devproxy status' to check status.");
119-
await Console.Out.WriteLineAsync("Use 'devproxy logs' to view logs.");
120-
await Console.Out.WriteLineAsync("Use 'devproxy stop' to stop.");
141+
if (isJsonOutput)
142+
{
143+
await Console.Out.WriteLineAsync(FormatJsonResultEntry(new
144+
{
145+
state.Pid,
146+
state.ApiUrl,
147+
state.LogFile
148+
}));
149+
}
150+
else
151+
{
152+
await Console.Out.WriteLineAsync("Dev Proxy started in background.");
153+
await Console.Out.WriteLineAsync();
154+
await Console.Out.WriteLineAsync($" PID: {state.Pid}");
155+
await Console.Out.WriteLineAsync($" API URL: {state.ApiUrl}");
156+
await Console.Out.WriteLineAsync($" Log file: {state.LogFile}");
157+
await Console.Out.WriteLineAsync();
158+
await Console.Out.WriteLineAsync("Use 'devproxy status' to check status.");
159+
await Console.Out.WriteLineAsync("Use 'devproxy logs' to view logs.");
160+
await Console.Out.WriteLineAsync("Use 'devproxy stop' to stop.");
161+
}
121162
return 0;
122163
}
123164

@@ -127,30 +168,120 @@ static async Task<int> StartDetachedProcessAsync(string[] args)
127168
var errorOutput = await process.StandardError.ReadToEndAsync();
128169
var standardOutput = await process.StandardOutput.ReadToEndAsync();
129170

130-
await Console.Error.WriteLineAsync("Dev Proxy failed to start.");
131-
if (!string.IsNullOrEmpty(errorOutput))
171+
if (isJsonOutput)
132172
{
133-
await Console.Error.WriteLineAsync(errorOutput);
173+
var message = "Dev Proxy failed to start.";
174+
if (!string.IsNullOrEmpty(errorOutput))
175+
{
176+
message += $" {errorOutput.Trim()}";
177+
}
178+
if (!string.IsNullOrEmpty(standardOutput))
179+
{
180+
message += $" {standardOutput.Trim()}";
181+
}
182+
await Console.Error.WriteLineAsync(FormatJsonLogEntry("error", message));
134183
}
135-
if (!string.IsNullOrEmpty(standardOutput))
184+
else
136185
{
137-
await Console.Error.WriteLineAsync(standardOutput);
186+
await Console.Error.WriteLineAsync("Dev Proxy failed to start.");
187+
if (!string.IsNullOrEmpty(errorOutput))
188+
{
189+
await Console.Error.WriteLineAsync(errorOutput);
190+
}
191+
if (!string.IsNullOrEmpty(standardOutput))
192+
{
193+
await Console.Error.WriteLineAsync(standardOutput);
194+
}
138195
}
139196
return process.ExitCode;
140197
}
141198
}
142199

143-
await Console.Error.WriteLineAsync("Timeout waiting for Dev Proxy to start.");
144-
await Console.Error.WriteLineAsync($"Check the log folder: {StateManager.GetLogsFolder()}");
200+
if (isJsonOutput)
201+
{
202+
await Console.Error.WriteLineAsync(
203+
FormatJsonLogEntry("error", $"Timeout waiting for Dev Proxy to start. Check the log folder: {StateManager.GetLogsFolder()}"));
204+
}
205+
else
206+
{
207+
await Console.Error.WriteLineAsync("Timeout waiting for Dev Proxy to start.");
208+
await Console.Error.WriteLineAsync($"Check the log folder: {StateManager.GetLogsFolder()}");
209+
}
145210
return 1;
146211
}
147212
catch (Exception ex)
148213
{
149-
await Console.Error.WriteLineAsync($"Failed to start Dev Proxy: {ex.Message}");
214+
if (isJsonOutput)
215+
{
216+
await Console.Error.WriteLineAsync(
217+
FormatJsonLogEntry("error", $"Failed to start Dev Proxy: {ex.Message}"));
218+
}
219+
else
220+
{
221+
await Console.Error.WriteLineAsync($"Failed to start Dev Proxy: {ex.Message}");
222+
}
150223
return 1;
151224
}
152225
}
153226

227+
/// <summary>
228+
/// Formats a result entry matching the JSONL envelope produced by JsonConsoleFormatter:
229+
/// {"type":"result","data":{...},"timestamp":"..."}
230+
/// </summary>
231+
static string FormatJsonResultEntry(object data)
232+
{
233+
#pragma warning disable CA1869 // Called at most once per process lifetime
234+
var jsonlOptions = new JsonSerializerOptions
235+
{
236+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
237+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
238+
};
239+
#pragma warning restore CA1869
240+
var entry = new
241+
{
242+
type = "result",
243+
data = JsonSerializer.SerializeToElement(data, jsonlOptions),
244+
timestamp = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)
245+
};
246+
return JsonSerializer.Serialize(entry, jsonlOptions);
247+
}
248+
249+
/// <summary>
250+
/// Formats a log entry matching the JSONL envelope produced by JsonConsoleFormatter:
251+
/// {"type":"log","level":"...","message":"...","timestamp":"..."}
252+
/// </summary>
253+
static string FormatJsonLogEntry(string level, string message)
254+
{
255+
#pragma warning disable CA1869 // Called at most once per process lifetime
256+
var jsonlOptions = new JsonSerializerOptions
257+
{
258+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
259+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
260+
};
261+
#pragma warning restore CA1869
262+
var entry = new
263+
{
264+
type = "log",
265+
level,
266+
message,
267+
timestamp = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)
268+
};
269+
return JsonSerializer.Serialize(entry, jsonlOptions);
270+
}
271+
272+
static bool IsJsonOutputRequested(string[] args)
273+
{
274+
for (var i = 0; i < args.Length; i++)
275+
{
276+
if (string.Equals(args[i], DevProxyCommand.OutputOptionName, StringComparison.OrdinalIgnoreCase) &&
277+
i + 1 < args.Length)
278+
{
279+
return string.Equals(args[i + 1], "json", StringComparison.OrdinalIgnoreCase);
280+
}
281+
}
282+
return false;
283+
}
284+
154285
static string EscapeArgument(string arg)
155286
{
156287
// Simple escaping for command line arguments

0 commit comments

Comments
 (0)