1 /*
2  *Copyright (C) 2018 Laurent Tréguier
3  *
4  *This file is part of DLS.
5  *
6  *DLS is free software: you can redistribute it and/or modify
7  *it under the terms of the GNU General Public License as published by
8  *the Free Software Foundation, either version 3 of the License, or
9  *(at your option) any later version.
10  *
11  *DLS is distributed in the hope that it will be useful,
12  *but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *GNU General Public License for more details.
15  *
16  *You should have received a copy of the GNU General Public License
17  *along with DLS.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 module dls.bootstrap;
22 
23 import std.json : JSONValue;
24 
25 enum NetworkBackend : string
26 {
27     wininet = "wininet",
28     curl = "curl"
29 }
30 
31 version (CRuntime_Microsoft)
32 {
33     immutable networkBackend = NetworkBackend.wininet;
34 }
35 else
36 {
37     immutable networkBackend = NetworkBackend.curl;
38 }
39 
40 immutable apiEndpoint = "https://api.github.com/repos/d-language-server/dls/%s";
41 
42 version (Windows)
43 {
44     private immutable os = "windows";
45 }
46 else version (OSX)
47 {
48     private immutable os = "osx";
49 }
50 else version (linux)
51 {
52     private immutable os = "linux";
53 }
54 else version (FreeBSD)
55 {
56     private immutable os = "linux";
57 }
58 else
59 {
60     private immutable os = "none";
61 }
62 
63 version (Windows)
64 {
65     private immutable dlsExecutable = "dls.exe";
66 }
67 else
68 {
69     private immutable dlsExecutable = "dls";
70 }
71 
72 private immutable string dlsArchiveName;
73 private immutable string dlsDirName = "dls-%s";
74 private immutable string dlsLatestDirName = "dls-latest";
75 private string downloadVersion;
76 private string downloadUrl;
77 private size_t downloadSize;
78 
79 version (X86_64)
80     version = IntelArchitecture;
81 else version (X86)
82     version = IntelArchitecture;
83 
84 shared static this()
85 {
86     import std.format : format;
87 
88     version (IntelArchitecture)
89     {
90         import core.cpuid : isX86_64;
91 
92         version (Posix)
93         {
94             import std.process : execute;
95             import std.string : strip;
96             import std.uni : toLower;
97 
98             immutable arch = execute("uname").output.strip()
99                 .toLower() != "freebsd" && isX86_64 ? "x86_64" : "x86";
100         }
101         else
102         {
103             immutable arch = isX86_64 ? "x86_64" : "x86";
104         }
105     }
106     else
107     {
108         immutable arch = "none";
109     }
110 
111     dlsArchiveName = format("dls-%%s.%s.%s.zip", os, arch);
112 }
113 
114 @property JSONValue[] allReleases()
115 {
116     import std.format : format;
117     import std.json : parseJSON;
118 
119     return parseJSON(cast(char[]) standardDownload(format!apiEndpoint("releases"))).array;
120 }
121 
122 @property bool canDownloadDls()
123 {
124     import core.time : hours;
125     import std.algorithm : min;
126     import std.datetime : Clock, SysTime;
127     import std.format : format;
128     import std.json : JSON_TYPE;
129 
130     try
131     {
132         foreach (release; allReleases)
133         {
134             immutable releaseDate = SysTime.fromISOExtString(release["published_at"].str);
135 
136             if (Clock.currTime.toUTC() - releaseDate > 1.hours
137                     && release["prerelease"].type == JSON_TYPE.FALSE)
138             {
139                 foreach (asset; release["assets"].array)
140                 {
141                     if (asset["name"].str == format(dlsArchiveName, release["tag_name"].str))
142                     {
143                         downloadVersion = release["tag_name"].str;
144                         downloadUrl = asset["browser_download_url"].str;
145                         downloadSize = cast(size_t)(asset["size"].type == JSON_TYPE.INTEGER
146                                 ? asset["size"].integer : asset["size"].uinteger);
147                         return true;
148                     }
149                 }
150             }
151         }
152     }
153     catch (Exception e)
154     {
155         // The download URL couldn't be retrieved
156     }
157 
158     return false;
159 }
160 
161 void downloadDls(const void function(size_t size) totalSizeCallback = null,
162         const void function(size_t size) chunkSizeCallback = null,
163         const void function() extractCallback = null)
164 {
165     import std.array : appender;
166     import std.net.curl : HTTP;
167     import std.file : exists, isFile, mkdirRecurse, remove, rmdirRecurse, write;
168     import std.format : format;
169     import std.path : buildNormalizedPath;
170     import std.zip : ZipArchive;
171 
172     if (downloadUrl.length > 0 || canDownloadDls)
173     {
174         immutable dlsDir = buildNormalizedPath(dubBinDir, format(dlsDirName, downloadVersion));
175 
176         if (exists(dlsDir))
177         {
178             if (isFile(dlsDir))
179             {
180                 remove(dlsDir);
181             }
182             else
183             {
184                 rmdirRecurse(dlsDir);
185             }
186         }
187 
188         mkdirRecurse(dlsDir);
189 
190         if (totalSizeCallback !is null)
191         {
192             totalSizeCallback(downloadSize);
193         }
194 
195         auto archiveData = standardDownload(downloadUrl, chunkSizeCallback);
196 
197         if (extractCallback !is null)
198         {
199             extractCallback();
200         }
201 
202         auto archive = new ZipArchive(archiveData);
203 
204         foreach (name, member; archive.directory)
205         {
206             immutable memberPath = buildNormalizedPath(dlsDir, name);
207             write(memberPath, archive.expand(member));
208 
209             version (Posix)
210             {
211                 import std.process : execute;
212 
213                 if (name == dlsExecutable)
214                 {
215                     execute(["chmod", "+x", memberPath]);
216                 }
217             }
218         }
219     }
220     else
221     {
222         throw new UpgradeFailedException("Cannot download DLS");
223     }
224 }
225 
226 string linkDls()
227 {
228     import std.file : exists, isFile, mkdirRecurse, remove;
229     import std.format : format;
230     import std.path : baseName, buildNormalizedPath;
231     import std.string : endsWith;
232 
233     mkdirRecurse(dubBinDir);
234 
235     immutable dlsDir = buildNormalizedPath(dubBinDir, format(dlsDirName, downloadVersion));
236     immutable oldDlsLink = buildNormalizedPath(dubBinDir, dlsExecutable);
237     immutable dlsLatestDir = buildNormalizedPath(dubBinDir, dlsLatestDirName);
238 
239     if (exists(oldDlsLink) && !exists(dlsLatestDir))
240     {
241         makeLink(buildNormalizedPath(dlsLatestDir, dlsExecutable), oldDlsLink, false);
242     }
243 
244     makeLink(dlsDir, dlsLatestDir, true);
245 
246     return buildNormalizedPath(dlsLatestDir, dlsExecutable);
247 }
248 
249 @property string dubBinDir()
250 {
251     import std.path : buildNormalizedPath;
252     import std.process : environment;
253 
254     version (Windows)
255     {
256         immutable dubDirPath = environment["LOCALAPPDATA"];
257         immutable dubDirName = "dub";
258     }
259     else version (Posix)
260     {
261         immutable dubDirPath = environment["HOME"];
262         immutable dubDirName = ".dub";
263     }
264     else
265     {
266         static assert(false, "Platform not supported");
267     }
268 
269     return buildNormalizedPath(dubDirPath, dubDirName, "packages", ".bin");
270 }
271 
272 private ubyte[] standardDownload(string url, const void function(size_t size) callback = null)
273 {
274     static if (networkBackend == NetworkBackend.wininet)
275     {
276         return wininetDownload(url, callback);
277     }
278     else static if (networkBackend == NetworkBackend.curl)
279     {
280         return curlDownload(url, callback);
281     }
282     else
283     {
284         static assert(false, "No available network library");
285     }
286 }
287 
288 static if (networkBackend == NetworkBackend.wininet)
289 {
290     private ubyte[] wininetDownload(string url, const void function(size_t size) callback = null)
291     {
292         import core.sys.windows.winbase : GetLastError;
293         import core.sys.windows.windef : BOOL, DWORD, ERROR_SUCCESS, TRUE;
294         import core.sys.windows.wininet : HINTERNET, INTERNET_OPEN_TYPE_PRECONFIG,
295             InternetOpenA, InternetOpenUrlA, InternetReadFile;
296         import core.time : Duration, msecs;
297         import std.string : toStringz;
298 
299         static if (__VERSION__ >= 2075L)
300         {
301             import std.datetime.stopwatch : StopWatch;
302         }
303         else
304         {
305             import std.datetime : StopWatch;
306         }
307 
308         static void throwIfNull(const HINTERNET h)
309         {
310             if (h is null)
311             {
312                 throw new UpgradeFailedException("Could not create Internet handle");
313             }
314         }
315 
316         ubyte[] result;
317         StopWatch watch;
318         auto agentCStr = toStringz("DLS");
319         auto hInternet = InternetOpenA(agentCStr, INTERNET_OPEN_TYPE_PRECONFIG, null, null, 0);
320         throwIfNull(hInternet);
321         auto urlCStr = toStringz(url);
322         auto hFile = InternetOpenUrlA(hInternet, urlCStr, null, 0, 0, 0);
323         throwIfNull(hFile);
324 
325         DWORD bytesRead;
326         ubyte[64 * 1024] buffer;
327         BOOL success;
328 
329         if (callback !is null)
330         {
331             watch.start();
332         }
333 
334         do
335         {
336             success = InternetReadFile(hFile, buffer.ptr, cast(DWORD) buffer.length, &bytesRead);
337 
338             if (GetLastError() != ERROR_SUCCESS)
339             {
340                 throw new UpgradeFailedException("Could not download DLS");
341             }
342 
343             result ~= buffer[0 .. bytesRead];
344 
345             if (callback !is null && cast(Duration) watch.peek() >= 500.msecs)
346             {
347                 watch.reset();
348                 callback(result.length);
349             }
350         }
351         while (success == TRUE && bytesRead > 0);
352 
353         if (callback !is null)
354         {
355             watch.stop();
356             callback(result.length);
357         }
358 
359         return result;
360     }
361 }
362 else
363 {
364     private ubyte[] curlDownload(string url, const void function(size_t size) callback = null)
365     {
366         import core.time : Duration, msecs;
367         import std.net.curl : HTTP;
368 
369         static if (__VERSION__ >= 2075L)
370         {
371             import std.datetime.stopwatch : StopWatch;
372         }
373         else
374         {
375             import std.datetime : StopWatch;
376         }
377 
378         ubyte[] result;
379         StopWatch watch;
380 
381         auto request = HTTP(url);
382 
383         request.onReceive = (ubyte[] data) { result ~= data; return data.length; };
384         request.onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {
385             static bool started;
386             static bool stopped;
387 
388             if (!started && dlTotal > 0)
389             {
390                 started = true;
391                 watch.start();
392             }
393 
394             if (started && !stopped && callback !is null && dlNow > 0
395                     && (cast(Duration) watch.peek() >= 500.msecs || dlNow == dlTotal))
396             {
397                 watch.reset();
398                 callback(dlNow);
399 
400                 if (dlNow == dlTotal)
401                 {
402                     stopped = true;
403                     watch.stop();
404                 }
405             }
406 
407             return 0;
408         };
409 
410         request.perform();
411         return result;
412     }
413 }
414 
415 private void makeLink(const string target, const string link, bool directory)
416 {
417     version (Windows)
418     {
419         import std.array : join;
420         import std.file : exists, isFile, remove, rmdir;
421         import std.format : format;
422         import std.process : execute;
423 
424         if (exists(link))
425         {
426             if (isFile(link))
427             {
428                 remove(link);
429             }
430             else
431             {
432                 rmdir(link);
433             }
434         }
435 
436         immutable mklinkCommand = format!`mklink %s "%s" "%s"`(directory ? "/J" : "", link, target);
437         const powershellArgs = ["Start-Process", "-Wait", "-FilePath", "cmd.exe",
438             "-ArgumentList", format!"'/c %s'"(mklinkCommand), "-WindowStyle", "Hidden"] ~ (directory
439                 ? [] : ["-Verb", "runas"]);
440         immutable result = execute(["powershell.exe", powershellArgs.join(' ')]);
441 
442         if (result.status != 0)
443         {
444             throw new UpgradeFailedException("Symlink failed: " ~ result.output);
445         }
446     }
447     else version (Posix)
448     {
449         import std.file : exists, remove, symlink;
450 
451         if (exists(link))
452         {
453             remove(link);
454         }
455 
456         symlink(target, link);
457     }
458     else
459     {
460         static assert(false, "Platform not supported");
461     }
462 }
463 
464 class UpgradeFailedException : Exception
465 {
466     this(const string message)
467     {
468         super("Upgrade failed: " ~ message);
469     }
470 }