1 /*******************************************************************************
2  * Command pipe
3  * 
4  * Communicate with the launched program commands through stdio pipes.
5  */
6 module gendoc.cmdpipe;
7 
8 import std..string, std.path, std.parallelism, std.process;
9 import std.exception, std.algorithm, std.array, std.range;
10 import dub.internal.vibecompat.data.json;
11 import gendoc.config, gendoc.modmgr;
12 
13 
14 
15 /*******************************************************************************
16  * Communicate with the launched program through stdio pipes.
17  * 
18  * gendoc responds to requests from the launched guest program.
19  * One request or response communicates via a JSON object without lines.
20  * Guest program requests are passed line by line to the standard error output.
21  * gendoc responds to requests with one line.
22  * The request starts with `::gendoc-request::`, followed by a JSON string.
23  * 
24  * ```
25  * ::gendoc-request::{ "type": "ReqEcho", "value": {"msg": "test"} }
26  * ```
27  * 
28  * The response type corresponds to the request type.
29  * 
30  * | Request Type          | Response Type        |
31  * |:----------------------|:---------------------|
32  * | ReqEcho               | ResEcho, ResErr      |
33  * | ReqInfo               | ResInfo, ResErr      |
34  * 
35  * Each piece of information is composed of a JSON object composed of `type` and `value` as follows,
36  * and the `value` includes a payload.
37  * The following examples include line breaks and indents for readability,
38  * but do not break lines in the data actually used.
39  * 
40  * ```
41  * {
42  *  "type":  "ReqInfo",
43  *  "value": { }
44  * }
45  * ```
46  */
47 struct CommandPipe
48 {
49 private:
50 	alias Config = gendoc.config.Config;
51 	string[]            _result;
52 	string[]            _stderr;
53 	string[]            _stdout;
54 	int                 _status;
55 	const(Config)*      _config;
56 	const(DubPkgInfo)[] _pkgInfo;
57 	
58 	void processing(ProcessPipes pipe)
59 	{
60 		
61 		auto responceTask = scopedTask({
62 			foreach (line; pipe.stderr.byLine)
63 			{
64 				Json json(T)(T val)
65 				{
66 					return Json(["type": Json(T.stringof), "value": serializeToJson(val)]);
67 				}
68 				try
69 				{
70 					enum header = "::gendoc-request::";
71 					if (!line.startsWith(header))
72 					{
73 						_stderr ~= line.chomp().idup;
74 						_result ~= _stderr[$-1];
75 						continue;
76 					}
77 					auto inputContent = cast(string)(line[header.length..$].idup);
78 					auto command = parseJson(inputContent);
79 					switch (command["type"].to!string)
80 					{
81 					case "ReqEcho":
82 						auto req = deserializeJson!ReqEcho(command["value"]);
83 						pipe.stdin.writeln(json(ResEcho(req.msg)).toString());
84 						break;
85 					case "ReqInfo":
86 						auto req = deserializeJson!ReqInfo(command["value"]);
87 						pipe.stdin.writeln(json(ResInfo(*_config, _pkgInfo)).toString());
88 						break;
89 					default:
90 						enforce(0, "Unknown request type.");
91 					}
92 				}
93 				catch (Exception e)
94 				{
95 					pipe.stdin.writeln(json(ResErr(e.msg)).toString());
96 				}
97 				pipe.stdin.flush();
98 			}
99 		});
100 		responceTask.executeInNewThread();
101 		import std..string;
102 		foreach (line; pipe.stdout.byLine)
103 		{
104 			_stdout ~= line.chomp().idup;
105 			_result ~= _stdout[$-1];
106 		}
107 		responceTask.yieldForce();
108 		_status = pipe.pid.wait();
109 	}
110 	
111 public:
112 	///
113 	this(in ref Config cfg, in DubPkgInfo[] pkgInfo)
114 	{
115 		_config  = &cfg;
116 		_pkgInfo = pkgInfo;
117 	}
118 	///
119 	void run(string[] args, string workDir = null, string[string] env = null)
120 	{
121 		processing(pipeProcess(args, Redirect.all, env, std.process.Config.none, workDir));
122 	}
123 	/// ditto
124 	void run(string args, string workDir = null, string[string] env = null)
125 	{
126 		processing(pipeShell(args, Redirect.all, env, std.process.Config.none, workDir));
127 	}
128 	///
129 	auto result() const @property
130 	{
131 		auto chompLen = _result.retro.count!(a => a.length == 0);
132 		return _result[0..$-chompLen];
133 	}
134 	///
135 	auto stdout() const @property
136 	{
137 		auto chompLen = _stdout.retro.count!(a => a.length == 0);
138 		return _stdout[0..$-chompLen];
139 	}
140 	///
141 	auto stderr() const @property
142 	{
143 		auto chompLen = _stderr.retro.count!(a => a.length == 0);
144 		return _stderr[0..$-chompLen];
145 	}
146 	///
147 	int status() const @property
148 	{
149 		return _status;
150 	}
151 }
152 
153 ///
154 @system unittest
155 {
156 	import std.path;
157 	gendoc.config.Config cfg;
158 	ModuleManager modmgr;
159 	modmgr.addSources("root", "v1.2.3", __FILE_FULL_PATH__.buildNormalizedPath("../.."), [__FILE__], null);
160 	auto pipe = CommandPipe(cfg, modmgr.dubPackages);
161 	pipe.run("echo xxx");
162 	pipe.run(["rdmd", "--eval", q{
163 		stderr.writeln(`test1`);
164 		stderr.writeln(`::gendoc-request::{ "type": "ReqEcho", "value": {"msg": "test-echo"} }`);
165 		stderr.writeln(`test2`);
166 		auto res = stdin.readln();
167 		writeln(parseJSON(res)["value"]["msg"].str);
168 		
169 		stderr.writeln(`::gendoc-request::{ "type": "XXXXX", "value": {"msg": "test"} }`);
170 		res = stdin.readln();
171 		writeln(parseJSON(res)["value"]["msg"].str);
172 		
173 		stderr.writeln(`::gendoc-request::{ "type": "ReqInfo", "value": {} }`);
174 		res = stdin.readln();
175 		writeln(parseJSON(res)["value"]["dubPkgInfos"][0]["packageVersion"].str);
176 	}]);
177 	assert(pipe.stderr.equal(["test1", "test2"]));
178 	assert(pipe.stdout.equal(["xxx", "test-echo", "Unknown request type.", "v1.2.3"]));
179 	assert(pipe.result.equal(["xxx", "test1", "test2", "test-echo", "Unknown request type.", "v1.2.3"]));
180 	assert(pipe.status == 0);
181 }
182 
183 
184 
185 /*******************************************************************************
186  * Test request to return the specified msg without processing.
187  * 
188  * The main return value is ResEcho.
189  * ResErr will be returned if something goes wrong.
190  * 
191  * Returns:
192  * - ResEcho
193  * - ResErr
194  */
195 struct ReqEcho
196 {
197 	///
198 	string msg;
199 }
200 
201 /*******************************************************************************
202  * Main return value of ReqEcho
203  */
204 struct ResEcho
205 {
206 	///
207 	string msg;
208 }
209 
210 /*******************************************************************************
211  * Request information that gendoc has.
212  * 
213  * The main return value is ResInfo.
214  * ResErr will be returned if something goes wrong.
215  * 
216  * Returns:
217  * - ResInfo
218  * - ResErr
219  */
220 struct ReqInfo
221 {
222 	
223 }
224 
225 /*******************************************************************************
226  * Main return value of ReqInfo
227  */
228 struct ResInfo
229 {
230 	private alias Config = gendoc.config.Config;
231 	/// $(REF Config, gendoc, _config)
232 	Config config;
233 	/// $(REF DubPkgInfo, gendoc, modmgr)
234 	DubPkgInfo[] dubPkgInfos;
235 private:
236 	this(in ref gendoc.config.Config cfg, in DubPkgInfo[] dpi)
237 	{
238 		config = cast()cfg;
239 		dubPkgInfos = (cast(DubPkgInfo[])dpi[]).dup;
240 	}
241 }
242 
243 /*******************************************************************************
244  * Return-value when something wrong.
245  */
246 struct ResErr
247 {
248 	///
249 	string msg;
250 }