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