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(scope const(char)[] code,
32 			string[] dmdArguments = [], int count = 1, int timeoutMsecs = 5000)
33 	{
34 		return typeof(return).async(() => measureSync(code, dmdArguments, count, timeoutMsecs));
35 	}
36 
37 	/// ditto
38 	DMDMeasureReturn measureSync(scope const(char)[] code,
39 			string[] dmdArguments = [], 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 const
98 	{
99 		return config.get("dmd", "path", "dmd");
100 	}
101 }
102 
103 ///
104 version (DigitalMars) unittest
105 {
106 	scope backend = new WorkspaceD();
107 	auto workspace = makeTemporaryTestingWorkspace;
108 	auto instance = backend.addInstance(workspace.directory);
109 	backend.register!DMDComponent;
110 	auto measure = backend.get!DMDComponent(workspace.directory)
111 		.measure("import std.stdio;", null, 100).getBlocking;
112 	assert(measure.success);
113 	assert(measure.duration < 5.seconds);
114 }
115 
116 ///
117 struct DMDMeasureReturn
118 {
119 	/// true if dmd returned 0
120 	bool success;
121 	/// true if an ICE occured (segfault / negative return code)
122 	bool crash;
123 	/// compilation output
124 	string[] log;
125 	/// how long compilation took (serialized to msecs float in json)
126 	Duration duration;
127 
128 	/// Converts a json object to [DMDMeasureReturn]
129 	static DMDMeasureReturn fromJSON(JSONValue value)
130 	{
131 		DMDMeasureReturn ret;
132 		if (auto success = "success" in value)
133 			ret.success = success.type == JSONType.true_;
134 		if (auto crash = "crash" in value)
135 			ret.crash = crash.type == JSONType.true_;
136 		if (auto log = "log" in value)
137 			ret.log = (*log).fromJSON!(string[]);
138 		if (auto duration = "duration" in value)
139 			ret.duration = (cast(long)(duration.floating * 10_000)).hnsecs;
140 		return ret;
141 	}
142 
143 	/// Converts this object to a [JSONValue]
144 	JSONValue toJSON() const
145 	{
146 		//dfmt off
147 		return JSONValue([
148 			"success": JSONValue(success),
149 			"crash": JSONValue(crash),
150 			"log": log.toJSON,
151 			"duration": JSONValue(duration.total!"hnsecs" / cast(double) 10_000)
152 		]);
153 		//dfmt on
154 	}
155 }