// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using autoShell.Logging;
using autoShell.Services.Interop;
namespace autoShell;
///
/// Entry point for the autoShell Windows automation console application.
/// Reads JSON commands from stdin (interactive mode) or command-line arguments
/// and dispatches them to the appropriate handler via .
///
///
/// Each JSON command is a single object where property names are command names
/// and values are parameters, e.g. {"Volume":50} or {"Mute":true}.
/// Multiple commands can be batched in one object: {"Volume":50,"Mute":false}.
/// The special command "quit" exits the application.
///
internal class AutoShell
{
#region P/Invoke
[DllImport(NativeDlls.Kernel32, CharSet = CharSet.Unicode)]
private static extern IntPtr GetCommandLineW();
#endregion P/Invoke
private static readonly ConsoleLogger s_logger = new();
private static readonly ActionDispatcher s_dispatcher = ActionDispatcher.Create(s_logger);
///
/// Program entry point. Runs in one of two modes:
///
/// - Command-line mode: executes the JSON command(s) passed as arguments and exits.
/// - Interactive mode (no args): reads JSON commands from stdin in a loop until "quit" or EOF.
///
///
private static void Main(string[] args)
{
if (args.Length > 0)
{
RunFromCommandLine();
}
else
{
RunInteractive();
}
}
///
/// Executes JSON command(s) from command-line arguments and exits.
/// Accepts a single JSON object ({"Volume":50}) or an array
/// ([{"Volume":50},{"Mute":true}]).
///
///
/// Uses raw command line via P/Invoke to preserve original quoting and spacing,
/// since the CLR args array strips quotes and splits on spaces.
///
private static void RunFromCommandLine()
{
string rawCmdLine = Marshal.PtrToStringUni(GetCommandLineW());
string cmdLine = StripExecutableName(rawCmdLine);
try
{
using var doc = JsonDocument.Parse(cmdLine);
var root = doc.RootElement;
if (root.ValueKind == JsonValueKind.Array)
{
foreach (var element in root.EnumerateArray())
{
var result = ExecLine(element);
Console.WriteLine(JsonSerializer.Serialize(result));
}
}
else
{
var result = ExecLine(root);
Console.WriteLine(JsonSerializer.Serialize(result));
}
}
catch (Exception ex)
{
var result = ActionResult.Fail($"Failed to parse command line: {ex.Message}");
Console.WriteLine(JsonSerializer.Serialize(result));
}
}
///
/// Reads JSON commands from stdin line by line until "quit" or EOF.
/// This is the primary mode when autoShell is launched as a child process
/// by the TypeAgent desktop connector.
///
private static void RunInteractive()
{
while (true)
{
string line = null;
try
{
line = Console.ReadLine();
// Null means stdin was closed (e.g., parent process exited)
if (line == null)
{
break;
}
// Each line is a JSON object with one or more command keys
using var doc = JsonDocument.Parse(line);
var root = doc.RootElement;
ActionResult result = ExecLine(root);
// Pass through the request id if present
if (root.TryGetProperty("id", out JsonElement idElement))
{
result.Id = idElement.ToString();
}
Console.WriteLine(JsonSerializer.Serialize(result));
if (result.IsQuit)
{
break;
}
}
catch (Exception ex)
{
// Always write a JSON failure to stdout so the TS sendAction
// correlation doesn't hang waiting for a response.
var errorResult = ActionResult.Fail($"Error: {ex.Message}");
// Try to extract the request id from the raw line for correlation
try
{
using var errDoc = JsonDocument.Parse(line);
if (errDoc.RootElement.TryGetProperty("id", out JsonElement errorId))
{
errorResult.Id = errorId.ToString();
}
}
catch { /* line wasn't valid JSON — send response without id */ }
Console.WriteLine(JsonSerializer.Serialize(errorResult));
s_logger.Error(ex);
}
}
}
///
/// Strips the executable name/path from the raw command line string,
/// leaving only the arguments portion.
///
private static string StripExecutableName(string rawCmdLine)
{
// Try quoted path first: "C:\path\autoShell.exe"
string exe = $"\"{Environment.ProcessPath}\"";
string cmdLine = rawCmdLine.Replace(exe, "");
if (cmdLine.StartsWith(exe, StringComparison.OrdinalIgnoreCase))
{
return cmdLine[exe.Length..];
}
// Try unquoted filename: autoShell.exe
var processFileName = Path.GetFileName(Environment.ProcessPath);
if (cmdLine.StartsWith(processFileName, StringComparison.OrdinalIgnoreCase))
{
return cmdLine[processFileName.Length..];
}
// Try filename without extension: autoShell
var processFileNameNoExt = Path.GetFileNameWithoutExtension(processFileName);
return cmdLine.StartsWith(processFileNameNoExt, StringComparison.OrdinalIgnoreCase)
? cmdLine[processFileNameNoExt.Length..]
: cmdLine;
}
///
/// Dispatches a parsed JSON command object to the appropriate handler.
///
private static ActionResult ExecLine(JsonElement root)
=> s_dispatcher.Dispatch(root);
}