microsoft/TypeAgent

Public

mirrored fromhttps://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
d4944c6517c9a96a3c419f827769f518913f1b75

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

dotnet/autoShell/AutoShell.cs

189lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System;
5using System.IO;
6using System.Runtime.InteropServices;
7using System.Text.Json;
8using autoShell.Logging;
9using autoShell.Services.Interop;
10
11namespace autoShell;
12
13/// <summary>
14/// Entry point for the autoShell Windows automation console application.
15/// Reads JSON commands from stdin (interactive mode) or command-line arguments
16/// and dispatches them to the appropriate handler via <see cref="ActionDispatcher"/>.
17/// </summary>
18/// <remarks>
19/// Each JSON command is a single object where property names are command names
20/// and values are parameters, e.g. <c>{"Volume":50}</c> or <c>{"Mute":true}</c>.
21/// Multiple commands can be batched in one object: <c>{"Volume":50,"Mute":false}</c>.
22/// The special command <c>"quit"</c> exits the application.
23/// </remarks>
24internal class AutoShell
25{
26 #region P/Invoke
27
28 [DllImport(NativeDlls.Kernel32, CharSet = CharSet.Unicode)]
29 private static extern IntPtr GetCommandLineW();
30
31 #endregion P/Invoke
32
33 private static readonly ConsoleLogger s_logger = new();
34 private static readonly ActionDispatcher s_dispatcher = ActionDispatcher.Create(s_logger);
35
36 /// <summary>
37 /// Program entry point. Runs in one of two modes:
38 /// <list type="bullet">
39 /// <item><description>Command-line mode: executes the JSON command(s) passed as arguments and exits.</description></item>
40 /// <item><description>Interactive mode (no args): reads JSON commands from stdin in a loop until "quit" or EOF.</description></item>
41 /// </list>
42 /// </summary>
43 private static void Main(string[] args)
44 {
45 if (args.Length > 0)
46 {
47 RunFromCommandLine();
48 }
49 else
50 {
51 RunInteractive();
52 }
53 }
54
55 /// <summary>
56 /// Executes JSON command(s) from command-line arguments and exits.
57 /// Accepts a single JSON object (<c>{"Volume":50}</c>) or an array
58 /// (<c>[{"Volume":50},{"Mute":true}]</c>).
59 /// </summary>
60 /// <remarks>
61 /// Uses raw command line via P/Invoke to preserve original quoting and spacing,
62 /// since the CLR args array strips quotes and splits on spaces.
63 /// </remarks>
64 private static void RunFromCommandLine()
65 {
66 string rawCmdLine = Marshal.PtrToStringUni(GetCommandLineW());
67 string cmdLine = StripExecutableName(rawCmdLine);
68
69 try
70 {
71 using var doc = JsonDocument.Parse(cmdLine);
72 var root = doc.RootElement;
73
74 if (root.ValueKind == JsonValueKind.Array)
75 {
76 foreach (var element in root.EnumerateArray())
77 {
78 var result = ExecLine(element);
79 Console.WriteLine(JsonSerializer.Serialize(result));
80 }
81 }
82 else
83 {
84 var result = ExecLine(root);
85 Console.WriteLine(JsonSerializer.Serialize(result));
86 }
87 }
88 catch (Exception ex)
89 {
90 var result = ActionResult.Fail($"Failed to parse command line: {ex.Message}");
91 Console.WriteLine(JsonSerializer.Serialize(result));
92 }
93 }
94
95 /// <summary>
96 /// Reads JSON commands from stdin line by line until "quit" or EOF.
97 /// This is the primary mode when autoShell is launched as a child process
98 /// by the TypeAgent desktop connector.
99 /// </summary>
100 private static void RunInteractive()
101 {
102 while (true)
103 {
104 string line = null;
105 try
106 {
107 line = Console.ReadLine();
108
109 // Null means stdin was closed (e.g., parent process exited)
110 if (line == null)
111 {
112 break;
113 }
114
115 // Each line is a JSON object with one or more command keys
116 using var doc = JsonDocument.Parse(line);
117 var root = doc.RootElement;
118 ActionResult result = ExecLine(root);
119
120 // Pass through the request id if present
121 if (root.TryGetProperty("id", out JsonElement idElement))
122 {
123 result.Id = idElement.ToString();
124 }
125 Console.WriteLine(JsonSerializer.Serialize(result));
126
127 if (result.IsQuit)
128 {
129 break;
130 }
131 }
132 catch (Exception ex)
133 {
134 // Always write a JSON failure to stdout so the TS sendAction
135 // correlation doesn't hang waiting for a response.
136 var errorResult = ActionResult.Fail($"Error: {ex.Message}");
137
138 // Try to extract the request id from the raw line for correlation
139 try
140 {
141 using var errDoc = JsonDocument.Parse(line);
142 if (errDoc.RootElement.TryGetProperty("id", out JsonElement errorId))
143 {
144 errorResult.Id = errorId.ToString();
145 }
146 }
147 catch { /* line wasn't valid JSON — send response without id */ }
148
149 Console.WriteLine(JsonSerializer.Serialize(errorResult));
150 s_logger.Error(ex);
151 }
152 }
153 }
154
155 /// <summary>
156 /// Strips the executable name/path from the raw command line string,
157 /// leaving only the arguments portion.
158 /// </summary>
159 private static string StripExecutableName(string rawCmdLine)
160 {
161 // Try quoted path first: "C:\path\autoShell.exe"
162 string exe = $"\"{Environment.ProcessPath}\"";
163 string cmdLine = rawCmdLine.Replace(exe, "");
164
165 if (cmdLine.StartsWith(exe, StringComparison.OrdinalIgnoreCase))
166 {
167 return cmdLine[exe.Length..];
168 }
169
170 // Try unquoted filename: autoShell.exe
171 var processFileName = Path.GetFileName(Environment.ProcessPath);
172 if (cmdLine.StartsWith(processFileName, StringComparison.OrdinalIgnoreCase))
173 {
174 return cmdLine[processFileName.Length..];
175 }
176
177 // Try filename without extension: autoShell
178 var processFileNameNoExt = Path.GetFileNameWithoutExtension(processFileName);
179 return cmdLine.StartsWith(processFileNameNoExt, StringComparison.OrdinalIgnoreCase)
180 ? cmdLine[processFileNameNoExt.Length..]
181 : cmdLine;
182 }
183
184 /// <summary>
185 /// Dispatches a parsed JSON command object to the appropriate handler.
186 /// </summary>
187 private static ActionResult ExecLine(JsonElement root)
188 => s_dispatcher.Dispatch(root);
189}
190