1 module served.commands.dcd_update;
2 
3 import served.extension;
4 import served.io.git_build;
5 import served.io.http_wrap;
6 import served.types;
7 
8 import workspaced.api;
9 import workspaced.coms;
10 
11 import rm.rf;
12 
13 import std.algorithm : endsWith;
14 import std.format : format;
15 import std.path : baseName, buildPath, chainPath, isAbsolute;
16 
17 import fs = std.file;
18 import io = std.stdio;
19 
20 
21 version (Win32)
22 {
23 	version (X86_64)
24 	{
25 		// wat
26 	}
27 	else version (X86)
28 	{
29 		version = RuntimeSourceCheck;
30 		// check for WoW64
31 		bool isDCDFromSource()
32 		{
33 			// needed to check for 64 bit process compatibility on 32 bit binaries because of WoW64
34 			import core.sys.windows.windows : GetNativeSystemInfo, SYSTEM_INFO,
35 				PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_IA64;
36 
37 			SYSTEM_INFO sysInfo;
38 			GetNativeSystemInfo(&sysInfo);
39 			// only 64 bit releases
40 			return sysInfo.wProcessorArchitecture != PROCESSOR_ARCHITECTURE_AMD64
41 				&& sysInfo.wProcessorArchitecture != PROCESSOR_ARCHITECTURE_IA64;
42 		}
43 	}
44 	else version = DCDFromSource;
45 }
46 else version (X86_64)
47 {
48 	version (Win64)
49 	{
50 	}
51 	else version (linux)
52 	{
53 	}
54 	else version (OSX)
55 	{
56 	}
57 	else version = DCDFromSource;
58 }
59 else version (AArch64)
60 {
61 	version (OSX)
62 	{
63 	}
64 	else version = DCDFromSource;
65 }
66 else
67 {
68 	version = DCDFromSource;
69 }
70 
71 version (RuntimeSourceCheck) {}
72 else
73 {
74 	version (DCDFromSource)
75 		enum isDCDFromSource = true;
76 	else
77 		enum isDCDFromSource = false;
78 }
79 
80 __gshared string dcdUpdateReason = null;
81 __gshared bool dcdUpdating;
82 @protocolNotification("served/updateDCD")
83 void updateDCD()
84 {
85 	scope (exit)
86 	{
87 		dcdUpdating = false;
88 		dcdUpdateReason = null;
89 	}
90 
91 	if (dcdUpdateReason.length)
92 		rpc.notifyMethod("coded/logInstall", "Installing DCD: " ~ dcdUpdateReason);
93 	else
94 		rpc.notifyMethod("coded/logInstall", "Installing DCD");
95 
96 	string outputFolder = determineOutputFolder;
97 	if (fs.exists(outputFolder))
98 	{
99 		foreach (file; ["dcd", "DCD", "dcd-client", "dcd-server"])
100 		{
101 			auto path = buildPath(outputFolder, file);
102 			if (fs.exists(path))
103 			{
104 				if (fs.isFile(path))
105 					fs.remove(path);
106 				else
107 					rmdirRecurseForce(path);
108 			}
109 		}
110 	}
111 	if (!fs.exists(outputFolder))
112 		fs.mkdirRecurse(outputFolder);
113 	string ext = "";
114 	version (Windows)
115 		ext = ".exe";
116 	string finalDestinationClient;
117 	string finalDestinationServer;
118 
119 	bool success;
120 
121 	enum latestReleaseBinaryBroken = false;
122 
123 	// the latest version that has downloads available
124 	enum bundledDCDVersion = "v0.15.0";
125 
126 	bool compileFromSource = isDCDFromSource || latestReleaseBinaryBroken;
127 	if (!compileFromSource
128 		&& !checkVersion(bundledDCDVersion, DCDComponent.latestKnownVersion))
129 		compileFromSource = true;
130 
131 	string[] triedPaths;
132 
133 	if (compileFromSource)
134 	{
135 		success = compileDependency(outputFolder, "DCD",
136 				"https://github.com/dlang-community/DCD.git", [
137 					[
138 						anyConfig.git.userPath, "submodule", "update", "--init",
139 						"--recursive"
140 					], ["dub", "build", "--config=client"],
141 					["dub", "build", "--config=server"]
142 				]);
143 		finalDestinationClient = buildPath(outputFolder, "DCD", "dcd-client" ~ ext);
144 		if (!fs.exists(finalDestinationClient))
145 			finalDestinationClient = buildPath(outputFolder, "DCD", "bin", "dcd-client" ~ ext);
146 		finalDestinationServer = buildPath(outputFolder, "DCD", "dcd-server" ~ ext);
147 		if (!fs.exists(finalDestinationServer))
148 			finalDestinationServer = buildPath(outputFolder, "DCD", "bin", "dcd-server" ~ ext);
149 
150 		triedPaths = [
151 			"DCD/dcd-client" ~ ext, "DCD/dcd-server" ~ ext, "DCD/bin/dcd-client" ~ ext,
152 			"DCD/bin/dcd-server" ~ ext
153 		];
154 	}
155 	else
156 	{
157 		string url;
158 
159 		enum commonPrefix = "https://github.com/dlang-community/DCD/releases/download/"
160 			~ bundledDCDVersion ~ "/dcd-" ~ bundledDCDVersion;
161 
162 		version (Windows)
163 			url = commonPrefix ~ "-windows-x86_64.zip";
164 		else version (linux)
165 			url = commonPrefix ~ "-linux-x86_64.tar.gz";
166 		else version (OSX)
167 		{
168 			version (AArch64)
169 				url = commonPrefix ~ "-osx-arm64.tar.gz";
170 			else
171 				url = commonPrefix ~ "-osx-x86_64.tar.gz";
172 		}
173 
174 		if (!url.length)
175 			assert(false, "this branch should not be reachable on this platform");
176 
177 		import std.process : pipeProcess, Redirect, Config, wait;
178 		import std.zip : ZipArchive;
179 		import core.thread : Fiber;
180 
181 		string destFile = buildPath(outputFolder, url.baseName);
182 
183 		try
184 		{
185 			rpc.notifyMethod("coded/logInstall", "Downloading from " ~ url ~ " to " ~ outputFolder);
186 
187 			if (fs.exists(destFile))
188 				rpc.notifyMethod("coded/logInstall",
189 						"Zip file already exists! Trying to install existing zip.");
190 			else
191 				downloadFile(url, "Downloading DCD...", destFile);
192 
193 			rpc.notifyMethod("coded/logInstall", "Extracting download...");
194 			if (url.endsWith(".tar.gz"))
195 			{
196 				rpc.notifyMethod("coded/logInstall", "> tar xvfz " ~ url.baseName);
197 				auto proc = pipeProcess(["tar", "xvfz", url.baseName],
198 						Redirect.stdout | Redirect.stderrToStdout, null, Config.none, outputFolder);
199 				foreach (line; proc.stdout.byLineCopy)
200 					rpc.notifyMethod("coded/logInstall", line);
201 				proc.pid.wait;
202 			}
203 			else if (url.endsWith(".zip"))
204 			{
205 				auto zip = new ZipArchive(fs.read(destFile));
206 				foreach (name, am; zip.directory)
207 				{
208 					if (name.isAbsolute)
209 						name = "." ~ name;
210 					zip.expand(am);
211 					fs.write(chainPath(outputFolder, name), am.expandedData);
212 				}
213 			}
214 			success = true;
215 
216 			finalDestinationClient = buildPath(outputFolder, "dcd-client" ~ ext);
217 			finalDestinationServer = buildPath(outputFolder, "dcd-server" ~ ext);
218 
219 			if (!fs.exists(finalDestinationClient))
220 				finalDestinationClient = buildPath(outputFolder, "bin", "dcd-client" ~ ext);
221 			if (!fs.exists(finalDestinationServer))
222 				finalDestinationServer = buildPath(outputFolder, "bin", "dcd-client" ~ ext);
223 
224 			triedPaths = [
225 				"dcd-client" ~ ext, "dcd-server" ~ ext, "bin/dcd-client" ~ ext,
226 				"bin/dcd-server" ~ ext
227 			];
228 		}
229 		catch (Exception e)
230 		{
231 			rpc.notifyMethod("coded/logInstall", "Failed installing: " ~ e.toString ~ "\n\n");
232 			rpc.notifyMethod("coded/logInstall",
233 					"If you have troube downloading via code-d, try manually downloading the DCD archive and placing it in "
234 					~ destFile ~ "!");
235 			success = false;
236 		}
237 	}
238 
239 	if (success && (!fs.exists(finalDestinationClient) || !fs.exists(finalDestinationServer)))
240 	{
241 		rpc.notifyMethod("coded/logInstall",
242 				"Successfully downloaded DCD, but could not find the executables.");
243 		rpc.notifyMethod("coded/logInstall",
244 				"Please open your user settings and insert the paths for dcd-client and dcd-server manually.");
245 		rpc.notifyMethod("coded/logInstall", "Download base location: " ~ outputFolder);
246 		rpc.notifyMethod("coded/logInstall", "");
247 		rpc.notifyMethod("coded/logInstall", format("Tried %(%s, %)", triedPaths));
248 
249 		finalDestinationClient = "dcd-client";
250 		finalDestinationServer = "dcd-server";
251 	}
252 
253 	if (success)
254 	{
255 		dcdUpdating = false;
256 
257 		backend.globalConfiguration.set("dcd", "clientPath", finalDestinationClient);
258 		backend.globalConfiguration.set("dcd", "serverPath", finalDestinationServer);
259 
260 		foreach (ref workspace; workspaces)
261 		{
262 			workspace.config.d.dcdClientPath = finalDestinationClient;
263 			workspace.config.d.dcdServerPath = finalDestinationServer;
264 		}
265 		rpc.notifyMethod("coded/updateSetting", UpdateSettingParams("dcdClientPath",
266 				JsonValue(finalDestinationClient), true));
267 		rpc.notifyMethod("coded/updateSetting", UpdateSettingParams("dcdServerPath",
268 				JsonValue(finalDestinationServer), true));
269 		rpc.notifyMethod("coded/logInstall", "Successfully installed DCD");
270 
271 		foreach (ref workspace; workspaces)
272 		{
273 			auto instance = backend.getInstance(workspace.folder.uri.uriToFile);
274 			if (instance is null)
275 				rpc.notifyMethod("coded/logInstall",
276 						"Failed to find workspace to start DCD for " ~ workspace.folder.uri);
277 			else
278 			{
279 				instance.config.set("dcd", "clientPath", finalDestinationClient);
280 				instance.config.set("dcd", "serverPath", finalDestinationServer);
281 
282 				lazyStartDCDServer(instance, workspace.folder.uri);
283 			}
284 		}
285 	}
286 }