1 module workspaced.com.dmd;
2 
3 import core.thread;
4 import std.array;
5 import std.datetime;
6 import std.datetime.stopwatch : StopWatch;
7 import std.file;
8 import std.json;
9 import std.path;
10 import std.process;
11 import std.random;
12 
13 import painlessjson;
14 
15 import workspaced.api;
16 
17 @component("dmd")
18 class DMDComponent : ComponentWrapper
19 {
20 	mixin DefaultComponentWrapper;
21 
22 	/// Tries to compile a snippet of code with the import paths in the current directory. The arguments `-c -o-` are implicit.
23 	/// The sync function may be used to prevent other measures from running while this is running.
24 	/// Params:
25 	///   cb = async callback
26 	///   code = small code snippet to try to compile
27 	///   dmdArguments = additional arguments to pass to dmd before file name
28 	///   count = how often to compile (duration is divided by either this or less in case timeout is reached)
29 	///   timeoutMsecs = when to abort compilation after, note that this will not abort mid-compilation but not do another iteration if this timeout has been reached.
30 	/// Returns: [DMDMeasureReturn] containing logs from only the first compilation pass
31 	Future!DMDMeasureReturn measure(string code, string[] dmdArguments = [],
32 			int count = 1, int timeoutMsecs = 5000)
33 	{
34 		return Future!DMDMeasureReturn.async(() => measureSync(code, dmdArguments, count, timeoutMsecs));
35 	}
36 
37 	/// ditto
38 	DMDMeasureReturn measureSync(string code, string[] dmdArguments = [],
39 			int count = 1, int timeoutMsecs = 5000)
40 	{
41 		dmdArguments ~= ["-c", "-o-"];
42 		DMDMeasureReturn ret;
43 
44 		auto timeout = timeoutMsecs.msecs;
45 
46 		StopWatch sw;
47 
48 		int effective;
49 
50 		foreach (i; 0 .. count)
51 		{
52 			if (sw.peek >= timeout)
53 				break;
54 			string[] baseArgs = [path];
55 			foreach (path; importPaths)
56 				baseArgs ~= "-I=" ~ path;
57 			foreach (path; stringImportPaths)
58 				baseArgs ~= "-J=" ~ path;
59 			auto pipes = pipeProcess(baseArgs ~ dmdArguments ~ "-",
60 					Redirect.stderrToStdout | Redirect.stdout | Redirect.stdin, null,
61 					Config.none, instance.cwd);
62 			pipes.stdin.write(code);
63 			pipes.stdin.close();
64 			if (i == 0)
65 			{
66 				if (count == 0)
67 					sw.start();
68 				ret.log = pipes.stdout.byLineCopy().array;
69 				auto status = pipes.pid.wait();
70 				if (count == 0)
71 					sw.stop();
72 				ret.success = status == 0;
73 				ret.crash = status < 0;
74 			}
75 			else
76 			{
77 				if (count < 10 || i != 1)
78 					sw.start();
79 				pipes.pid.wait();
80 				if (count < 10 || i != 1)
81 					sw.stop();
82 				pipes.stdout.close();
83 				effective++;
84 			}
85 			if (!ret.success)
86 				break;
87 		}
88 
89 		ret.duration = sw.peek;
90 
91 		if (effective > 0)
92 			ret.duration = ret.duration / effective;
93 
94 		return ret;
95 	}
96 
97 	string path() @property @ignoredFunc
98 	{
99 		return config.get("dmd", "path", "dmd");
100 	}
101 }
102 
103 ///
104 unittest
105 {
106 	import std.stdio;
107 
108 	auto backend = new WorkspaceD();
109 	auto workspace = makeTemporaryTestingWorkspace;
110 	auto instance = backend.addInstance(workspace.directory);
111 	backend.register!DMDComponent;
112 	auto measure = backend.get!DMDComponent(workspace.directory)
113 		.measure("import std.stdio;", null, 100).getBlocking;
114 	assert(measure.success);
115 	assert(measure.duration < 5.seconds);
116 }
117 
118 ///
119 struct DMDMeasureReturn
120 {
121 	/// true if dmd returned 0
122 	bool success;
123 	/// true if an ICE occured (segfault / negative return code)
124 	bool crash;
125 	/// compilation output
126 	string[] log;
127 	/// how long compilation took (serialized to msecs float in json)
128 	Duration duration;
129 
130 	/// Converts a json object to [DMDMeasureReturn]
131 	static DMDMeasureReturn fromJSON(JSONValue value)
132 	{
133 		DMDMeasureReturn ret;
134 		if (auto success = "success" in value)
135 			ret.success = success.type == JSON_TYPE.TRUE;
136 		if (auto crash = "crash" in value)
137 			ret.crash = crash.type == JSON_TYPE.TRUE;
138 		if (auto log = "log" in value)
139 			ret.log = (*log).fromJSON!(string[]);
140 		if (auto duration = "duration" in value)
141 			ret.duration = (cast(long)(duration.floating * 10_000)).hnsecs;
142 		return ret;
143 	}
144 
145 	/// Converts this object to a [JSONValue]
146 	JSONValue toJSON() const
147 	{
148 		//dfmt off
149 		return JSONValue([
150 			"success": JSONValue(success),
151 			"crash": JSONValue(crash),
152 			"log": log.toJSON,
153 			"duration": JSONValue(duration.total!"hnsecs" / cast(double) 10_000)
154 		]);
155 		//dfmt on
156 	}
157 }