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 }