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         immutable arch = isX86_64 ? "x86_64" : "x86";
93     }
94     else
95     {
96         immutable arch = "none";
97     }
98 
99     dlsArchiveName = format("dls-%%s.%s.%s.zip", os, arch);
100 }
101 
102 @property JSONValue[] allReleases()
103 {
104     import std.format : format;
105     import std.json : parseJSON;
106 
107     return parseJSON(cast(char[]) standardDownload(format!apiEndpoint("releases"))).array;
108 }
109 
110 @property bool canDownloadDls()
111 {
112     import core.time : hours;
113     import std.algorithm : min;
114     import std.datetime : Clock, SysTime;
115     import std.format : format;
116 
117     static if (__VERSION__ >= 2082L)
118     {
119         import std.json : JSONType;
120 
121         alias jsonFalse = JSONType.false_;
122         alias jsonInt = JSONType.integer;
123     }
124     else
125     {
126         import std.json : JSON_TYPE;
127 
128         alias jsonFalse = JSON_TYPE.FALSE;
129         alias jsonInt = JSON_TYPE.INTEGER;
130     }
131 
132     try
133     {
134         foreach (release; allReleases)
135         {
136             immutable releaseDate = SysTime.fromISOExtString(release["published_at"].str);
137 
138             if (Clock.currTime.toUTC() - releaseDate > 1.hours
139                     && release["prerelease"].type == jsonFalse)
140             {
141                 foreach (asset; release["assets"].array)
142                 {
143                     if (asset["name"].str == format(dlsArchiveName, release["tag_name"].str))
144                     {
145                         downloadVersion = release["tag_name"].str;
146                         downloadUrl = asset["browser_download_url"].str;
147                         downloadSize = cast(size_t)(asset["size"].type == jsonInt
148                                 ? asset["size"].integer : asset["size"].uinteger);
149                         return true;
150                     }
151                 }
152             }
153         }
154     }
155     catch (Exception e)
156     {
157         // The download URL couldn't be retrieved
158     }
159 
160     return false;
161 }
162 
163 void downloadDls(const void function(size_t size) totalSizeCallback = null,
164         const void function(size_t size) chunkSizeCallback = null,
165         const void function() extractCallback = null)
166 {
167     import std.array : appender;
168     import std.net.curl : HTTP;
169     import std.file : exists, isFile, mkdirRecurse, remove, rmdirRecurse, write;
170     import std.format : format;
171     import std.path : buildNormalizedPath;
172     import std.zip : ZipArchive;
173 
174     if (downloadUrl.length > 0 || canDownloadDls)
175     {
176         immutable dlsDir = buildNormalizedPath(dubBinDir, format(dlsDirName, downloadVersion));
177 
178         if (exists(dlsDir))
179         {
180             if (isFile(dlsDir))
181             {
182                 remove(dlsDir);
183             }
184             else
185             {
186                 rmdirRecurse(dlsDir);
187             }
188         }
189 
190         mkdirRecurse(dlsDir);
191 
192         if (totalSizeCallback !is null)
193         {
194             totalSizeCallback(downloadSize);
195         }
196 
197         auto archiveData = standardDownload(downloadUrl, chunkSizeCallback);
198 
199         if (extractCallback !is null)
200         {
201             extractCallback();
202         }
203 
204         auto archive = new ZipArchive(archiveData);
205 
206         foreach (name, member; archive.directory)
207         {
208             immutable memberPath = buildNormalizedPath(dlsDir, name);
209             write(memberPath, archive.expand(member));
210 
211             version (Posix)
212             {
213                 import std.process : execute;
214 
215                 if (name == dlsExecutable)
216                 {
217                     execute(["chmod", "+x", memberPath]);
218                 }
219             }
220         }
221     }
222     else
223     {
224         throw new UpgradeFailedException("Cannot download DLS");
225     }
226 }
227 
228 string linkDls()
229 {
230     import std.file : exists, isFile, mkdirRecurse, remove;
231     import std.format : format;
232     import std.path : baseName, buildNormalizedPath;
233     import std..string : endsWith;
234 
235     mkdirRecurse(dubBinDir);
236 
237     immutable dlsDir = buildNormalizedPath(dubBinDir, format(dlsDirName, downloadVersion));
238     immutable oldDlsLink = buildNormalizedPath(dubBinDir, dlsExecutable);
239     immutable dlsLatestDir = buildNormalizedPath(dubBinDir, dlsLatestDirName);
240 
241     if (exists(oldDlsLink) && !exists(dlsLatestDir))
242     {
243         makeLink(buildNormalizedPath(dlsLatestDir, dlsExecutable), oldDlsLink, false);
244     }
245 
246     makeLink(dlsDir, dlsLatestDir, true);
247 
248     return buildNormalizedPath(dlsLatestDir, dlsExecutable);
249 }
250 
251 @property string dubBinDir()
252 {
253     import std.path : buildNormalizedPath;
254     import std.process : environment;
255 
256     version (Windows)
257     {
258         immutable dubDirPath = environment["LOCALAPPDATA"];
259         immutable dubDirName = "dub";
260     }
261     else version (Posix)
262     {
263         immutable dubDirPath = environment["HOME"];
264         immutable dubDirName = ".dub";
265     }
266     else
267     {
268         static assert(false, "Platform not supported");
269     }
270 
271     return buildNormalizedPath(dubDirPath, dubDirName, "packages", ".bin");
272 }
273 
274 private ubyte[] standardDownload(string url, const void function(size_t size) callback = null)
275 {
276     static if (networkBackend == NetworkBackend.wininet)
277     {
278         return wininetDownload(url, callback);
279     }
280     else static if (networkBackend == NetworkBackend.curl)
281     {
282         return curlDownload(url, callback);
283     }
284     else
285     {
286         static assert(false, "No available network library");
287     }
288 }
289 
290 static if (networkBackend == NetworkBackend.wininet)
291 {
292     private ubyte[] wininetDownload(string url, const void function(size_t size) callback = null)
293     {
294         import core.sys.windows.winbase : GetLastError;
295         import core.sys.windows.windef : BOOL, DWORD, ERROR_SUCCESS, TRUE;
296         import core.sys.windows.wininet : HINTERNET, INTERNET_OPEN_TYPE_PRECONFIG,
297             InternetOpenA, InternetOpenUrlA, InternetReadFile;
298         import core.time : Duration, msecs;
299         import std..string : toStringz;
300 
301         static if (__VERSION__ >= 2075L)
302         {
303             import std.datetime.stopwatch : StopWatch;
304         }
305         else
306         {
307             import std.datetime : StopWatch;
308         }
309 
310         static void throwIfNull(const HINTERNET h)
311         {
312             if (h is null)
313             {
314                 throw new UpgradeFailedException("Could not create Internet handle");
315             }
316         }
317 
318         ubyte[] result;
319         StopWatch watch;
320         auto agentCStr = toStringz("DLS");
321         auto hInternet = InternetOpenA(agentCStr, INTERNET_OPEN_TYPE_PRECONFIG, null, null, 0);
322         throwIfNull(hInternet);
323         auto urlCStr = toStringz(url);
324         auto hFile = InternetOpenUrlA(hInternet, urlCStr, null, 0, 0, 0);
325         throwIfNull(hFile);
326 
327         DWORD bytesRead;
328         ubyte[64 * 1024] buffer;
329         BOOL success;
330 
331         if (callback !is null)
332         {
333             watch.start();
334         }
335 
336         do
337         {
338             success = InternetReadFile(hFile, buffer.ptr, cast(DWORD) buffer.length, &bytesRead);
339 
340             if (GetLastError() != ERROR_SUCCESS)
341             {
342                 throw new UpgradeFailedException("Could not download DLS");
343             }
344 
345             result ~= buffer[0 .. bytesRead];
346 
347             if (callback !is null && cast(Duration) watch.peek() >= 500.msecs)
348             {
349                 watch.reset();
350                 callback(result.length);
351             }
352         }
353         while (success == TRUE && bytesRead > 0);
354 
355         if (callback !is null)
356         {
357             watch.stop();
358             callback(result.length);
359         }
360 
361         return result;
362     }
363 }
364 else
365 {
366     private ubyte[] curlDownload(string url, const void function(size_t size) callback = null)
367     {
368         import core.time : Duration, msecs;
369         import std.net.curl : HTTP;
370 
371         static if (__VERSION__ >= 2075L)
372         {
373             import std.datetime.stopwatch : StopWatch;
374         }
375         else
376         {
377             import std.datetime : StopWatch;
378         }
379 
380         ubyte[] result;
381         StopWatch watch;
382 
383         auto request = HTTP(url);
384 
385         request.onReceive = (ubyte[] data) { result ~= data; return data.length; };
386         request.onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {
387             static bool started;
388             static bool stopped;
389 
390             if (!started && dlTotal > 0)
391             {
392                 started = true;
393                 watch.start();
394             }
395 
396             if (started && !stopped && callback !is null && dlNow > 0
397                     && (cast(Duration) watch.peek() >= 500.msecs || dlNow == dlTotal))
398             {
399                 watch.reset();
400                 callback(dlNow);
401 
402                 if (dlNow == dlTotal)
403                 {
404                     stopped = true;
405                     watch.stop();
406                 }
407             }
408 
409             return 0;
410         };
411 
412         request.perform();
413         return result;
414     }
415 }
416 
417 private void makeLink(const string target, const string link, bool directory)
418 {
419     version (Windows)
420     {
421         import std.array : join;
422         import std.file : exists, isFile, remove, rmdir;
423         import std.format : format;
424         import std.process : execute;
425 
426         if (exists(link))
427         {
428             if (isFile(link))
429             {
430                 remove(link);
431             }
432             else
433             {
434                 rmdir(link);
435             }
436         }
437 
438         immutable mklinkCommand = format!`mklink %s "%s" "%s"`(directory ? "/J" : "", link, target);
439         const powershellArgs = ["Start-Process", "-Wait", "-FilePath", "cmd.exe",
440             "-ArgumentList", format!"'/c %s'"(mklinkCommand), "-WindowStyle", "Hidden"] ~ (directory
441                 ? [] : ["-Verb", "runas"]);
442         immutable result = execute(["powershell.exe", powershellArgs.join(' ')]);
443 
444         if (result.status != 0)
445         {
446             throw new UpgradeFailedException("Symlink failed: " ~ result.output);
447         }
448     }
449     else version (Posix)
450     {
451         import std.file : exists, remove, symlink;
452 
453         if (exists(link))
454         {
455             remove(link);
456         }
457 
458         symlink(target, link);
459     }
460     else
461     {
462         static assert(false, "Platform not supported");
463     }
464 }
465 
466 class UpgradeFailedException : Exception
467 {
468     this(const string message)
469     {
470         super("Upgrade failed: " ~ message);
471     }
472 }