1 /** 2 Copyright: Copyright (c) 2015 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 7 module launcher; 8 9 import std.experimental.logger; 10 import std.process; 11 import std.string; 12 import std.algorithm; 13 import std.stdio; 14 import std.array; 15 import std.range; 16 import std.typecons : Flag, Yes, No; 17 18 import voxelman.utils.messagewindow; 19 import voxelman.utils.linebuffer; 20 import gui; 21 22 struct PluginInfo 23 { 24 string id; 25 string semver; 26 string downloadUrl; 27 bool isEnabled = true; 28 PluginInfo*[] dependencies; 29 PluginInfo*[] dependants; 30 } 31 32 struct PluginPack 33 { 34 string id; 35 string semver; 36 string filename; 37 PluginInfo*[] plugins; 38 } 39 40 enum AppType 41 { 42 client, 43 server 44 } 45 46 enum JobType : int 47 { 48 run, 49 compile, 50 compileAndRun 51 } 52 53 enum JobState 54 { 55 compile, 56 run 57 } 58 59 struct JobParams 60 { 61 string pluginPack = "default"; 62 AppType appType = AppType.client; 63 Flag!"start" start = Yes.start; 64 Flag!"build" build = Yes.build; 65 Flag!"arch64" arch64 = Yes.arch64; 66 Flag!"nodeps" nodeps = Yes.nodeps; 67 Flag!"force" force = No.force; 68 Flag!"release" release = No.release; 69 JobType jobType; 70 } 71 72 struct Job 73 { 74 JobParams params; 75 string command; 76 MessageWindow messageWindow; 77 ProcessPipes pipes; 78 79 JobState jobState = JobState.compile; 80 bool isRunning; 81 bool needsClose; 82 bool needsRestart; 83 int status; 84 } 85 86 immutable buildFolder = "builds/default"; 87 struct Launcher 88 { 89 string pluginFolderPath; 90 string pluginPackFolderPath; 91 PluginInfo*[] plugins; 92 PluginInfo*[string] pluginsById; 93 PluginPack*[] pluginPacks; 94 PluginPack*[string] pluginsPacksById; 95 96 Job*[] jobs; 97 size_t numRunningJobs; 98 LineBuffer appLog; 99 100 void startJob(JobParams params = JobParams.init) 101 { 102 auto job = new Job(params); 103 job.messageWindow.init(); 104 job.messageWindow.messageHandler = (string com)=>sendCommand(job,com); 105 updateJobType(job); 106 restartJobState(job); 107 restartJob(job); 108 jobs ~= job; 109 } 110 111 void updateJobType(Job* job) 112 { 113 final switch(job.params.jobType) with(JobType) { 114 case run: 115 job.params.build = No.build; 116 job.params.start = Yes.start; 117 break; 118 case compile: 119 job.params.build = Yes.build; 120 job.params.start = No.start; 121 break; 122 case compileAndRun: 123 job.params.build = Yes.build; 124 job.params.start = Yes.start; 125 break; 126 } 127 } 128 129 void restartJobState(Job* job) 130 { 131 final switch(job.params.jobType) with(JobType) { 132 case run: job.jobState = JobState.run; break; 133 case compile: job.jobState = JobState.compile; break; 134 case compileAndRun: job.jobState = JobState.compile; break; 135 } 136 } 137 138 void restartJob(Job* job) 139 { 140 assert(!job.isRunning); 141 ++numRunningJobs; 142 143 string command; 144 string workDir; 145 if (job.jobState == JobState.compile) { 146 command = makeCompileCommand(job.params); 147 workDir = ""; 148 } 149 else if (job.jobState == JobState.run) { 150 command = makeRunCommand(job.params); 151 workDir = buildFolder; 152 } 153 154 ProcessPipes pipes = pipeShell(command, Redirect.all, null, Config.none, workDir); 155 156 (*job) = Job(job.params, command, job.messageWindow, pipes, job.jobState); 157 job.isRunning = true; 158 } 159 160 size_t stopProcesses() 161 { 162 foreach(job; jobs) 163 job.pipes.pid.kill; 164 return jobs.length; 165 } 166 167 bool anyProcessesRunning() @property 168 { 169 return numRunningJobs > 0; 170 } 171 172 void update() 173 { 174 foreach(job; jobs) logPipes(job); 175 176 foreach(job; jobs) 177 { 178 if (job.isRunning) 179 { 180 auto res = job.pipes.pid.tryWait(); 181 if (res.terminated) 182 { 183 --numRunningJobs; 184 job.isRunning = false; 185 job.status = res.status; 186 187 bool success = job.status == 0; 188 bool doneCompilation = job.jobState == JobState.compile; 189 bool needsStart = job.params.start; 190 if (doneCompilation) job.messageWindow.putln("Compilation successful"); 191 if (success && doneCompilation && needsStart) 192 { 193 job.jobState = JobState.run; 194 job.messageWindow.lineBuffer.clear(); 195 restartJob(job); 196 } 197 } 198 } 199 200 if (!job.isRunning && job.needsRestart) 201 { 202 job.messageWindow.lineBuffer.clear(); 203 restartJobState(job); 204 restartJob(job); 205 } 206 207 job.needsRestart = false; 208 } 209 210 jobs = remove!(a => a.needsClose && !a.isRunning)(jobs); 211 jobs.each!(j => j.needsClose = false); 212 } 213 214 void setRootPath(string pluginFolder, string pluginPackFolder) 215 { 216 pluginFolderPath = pluginFolder; 217 pluginPackFolderPath = pluginPackFolder; 218 } 219 220 void clear() 221 { 222 plugins = null; 223 pluginsById = null; 224 pluginPacks = null; 225 pluginsPacksById = null; 226 } 227 228 void readPlugins() 229 { 230 import std.file : exists, read, dirEntries, SpanMode; 231 import std.path : baseName; 232 233 if (!exists(pluginFolderPath)) return; 234 foreach (entry; dirEntries(pluginFolderPath, SpanMode.depth)) 235 { 236 if (entry.isFile && baseName(entry.name) == "plugininfo.d") 237 { 238 string fileData = cast(string)read(entry.name); 239 auto p = readPluginInfo(fileData); 240 plugins ~= p; 241 pluginsById[p.id] = p; 242 } 243 } 244 } 245 246 void printPlugins() 247 { 248 foreach(p; plugins) 249 { 250 infof("%s %s", p.id, p.semver); 251 } 252 } 253 254 void readPluginPacks() 255 { 256 import std.file : read, dirEntries, SpanMode; 257 import std.path : extension, absolutePath, buildNormalizedPath; 258 259 foreach (entry; dirEntries(pluginPackFolderPath, SpanMode.depth)) 260 { 261 if (entry.isFile && entry.name.extension == ".txt") 262 { 263 string fileData = cast(string)read(entry.name); 264 auto pack = readPluginPack(fileData); 265 pack.filename = entry.name.absolutePath.buildNormalizedPath; 266 pluginPacks ~= pack; 267 pluginsPacksById[pack.id] = pack; 268 } 269 } 270 } 271 } 272 273 274 string makeCompileCommand(JobParams params) 275 { 276 immutable arch = params.arch64 ? `--arch=x86_64` : `--arch=x86`; 277 immutable conf = params.appType == AppType.client ? `--config=client` : `--config=server`; 278 immutable deps = params.nodeps ? ` --nodeps` : ``; 279 immutable doForce = params.force ? ` --force` : ``; 280 immutable release = params.release ? `--build=release` : `--build=debug`; 281 return format("dub build -q %s %s%s%s %s\0", arch, conf, deps, doForce, release); 282 } 283 284 string makeRunCommand(JobParams params) 285 { 286 string conf = params.appType == AppType.client ? `client.exe` : `server.exe`; 287 return format("%s --pack=%s\0", conf, params.pluginPack); 288 } 289 290 void sendCommand(Job* job, string command) 291 { 292 if (job.jobState != JobState.run) return; 293 job.pipes.stdin.rawWrite(command); 294 job.pipes.stdin.rawWrite("\n"); 295 } 296 297 void logPipes(Job* job) 298 { 299 import std.exception : ErrnoException; 300 try 301 { 302 foreach(pipe; only(job.pipes.stdout, job.pipes.stderr)) 303 { 304 auto size = pipe.size; 305 if (size > 0) 306 { 307 char[1024] buf; 308 size_t charsToRead = min(pipe.size, buf.length); 309 char[] data = pipe.rawRead(buf[0..charsToRead]); 310 job.messageWindow.lineBuffer.put(data); 311 } 312 } 313 } 314 catch(ErrnoException e) 315 { // Ignore e 316 // It happens only when both launcher and child process is 32bit 317 // and child crashes with access violation (in opengl call for example). 318 // exception std.exception.ErrnoException@std\stdio.d(920): 319 // Could not seek in file `HANDLE(32C)' (Invalid argument) 320 } 321 } 322 323 PluginInfo* readPluginInfo(string fileData) 324 { 325 import std.regex : matchFirst, ctRegex; 326 327 auto pinfo = new PluginInfo; 328 329 auto idCapture = matchFirst(fileData, ctRegex!(`id\s*=\s*"(?P<id>[^"]*)"`, "s")); 330 pinfo.id = idCapture["id"].toCString; 331 332 auto semverCapture = matchFirst(fileData, ctRegex!(`semver\s*=\s*"(?P<semver>[^"]*)"`, "s")); 333 pinfo.semver = semverCapture["semver"].toCString; 334 335 return pinfo; 336 } 337 338 PluginPack* readPluginPack(string fileData) 339 { 340 import std.array : empty; 341 import std.regex : matchFirst, ctRegex; 342 import std.string : lineSplitter; 343 344 auto pack = new PluginPack; 345 346 auto input = fileData.lineSplitter; 347 348 if (!input.empty) { 349 auto packInfo = matchFirst(input.front, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m")); 350 pack.id = packInfo["id"].toCString; 351 pack.semver = packInfo["semver"].toCString; 352 input.popFront; 353 } 354 355 foreach(line; input) 356 { 357 if (line.empty) 358 continue; 359 360 auto pluginInfo = matchFirst(line, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m")); 361 auto pinfo = new PluginInfo; 362 pinfo.id = pluginInfo["id"].toCString; 363 pinfo.semver = pluginInfo["semver"].toCString; 364 pack.plugins ~= pinfo; 365 } 366 367 return pack; 368 } 369 370 string toCString(in const(char)[] s) 371 { 372 import std.exception : assumeUnique; 373 auto copy = new char[s.length + 1]; 374 copy[0..s.length] = s[]; 375 copy[s.length] = 0; 376 return assumeUnique(copy[0..s.length]); 377 }