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 immutable apiEndpoint = "https://api.github.com/repos/d-language-server/dls/%s";
26 
27 version (Windows)
28 {
29     private immutable os = "windows";
30 }
31 else version (OSX)
32 {
33     private immutable os = "osx";
34 }
35 else version (linux)
36 {
37     private immutable os = "linux";
38 }
39 else version (FreeBSD)
40 {
41     private immutable os = "linux";
42 }
43 else
44 {
45     private immutable os = "none";
46 }
47 
48 version (Windows)
49 {
50     private immutable dlsExecutable = "dls.exe";
51 }
52 else
53 {
54     private immutable dlsExecutable = "dls";
55 }
56 
57 private immutable string dlsArchiveName;
58 private immutable string dlsDirName = "dls-%s";
59 private immutable string dlsLatestDirName = "dls-latest";
60 private string downloadUrl;
61 private string downloadVersion;
62 
63 version (X86_64)
64     version = IntelArchitecture;
65 else version (X86)
66     version = IntelArchitecture;
67 
68 shared static this()
69 {
70     import std.format : format;
71 
72     version (IntelArchitecture)
73     {
74         import core.cpuid : isX86_64;
75 
76         version (Posix)
77         {
78             import std.process : execute;
79             import std..string : strip;
80             import std.uni : toLower;
81 
82             immutable arch = execute("uname").output.strip()
83                 .toLower() != "freebsd" && isX86_64 ? "x86_64" : "x86";
84         }
85         else
86         {
87             immutable arch = isX86_64 ? "x86_64" : "x86";
88         }
89     }
90     else
91     {
92         immutable arch = "none";
93     }
94 
95     dlsArchiveName = format("dls-%%s.%s.%s.zip", os, arch);
96 }
97 
98 @property JSONValue[] allReleases()
99 {
100     import std.format : format;
101     import std.json : parseJSON;
102     import std.net.curl : get;
103 
104     return parseJSON(get(format!apiEndpoint("releases"))).array;
105 }
106 
107 @property bool canDownloadDls()
108 {
109     import core.time : hours;
110     import std.algorithm : min;
111     import std.datetime : Clock, SysTime;
112     import std.format : format;
113     import std.json : JSON_TYPE;
114 
115     try
116     {
117         foreach (release; allReleases)
118         {
119             immutable releaseDate = SysTime.fromISOExtString(release["published_at"].str);
120 
121             if (Clock.currTime.toUTC() - releaseDate > 1.hours
122                     && release["prerelease"].type == JSON_TYPE.FALSE)
123             {
124                 foreach (asset; release["assets"].array)
125                 {
126                     if (asset["name"].str == format(dlsArchiveName, release["tag_name"].str))
127                     {
128                         downloadUrl = asset["browser_download_url"].str;
129                         downloadVersion = release["tag_name"].str;
130                         return true;
131                     }
132                 }
133             }
134         }
135     }
136     catch (Exception e)
137     {
138         // The download URL couldn't be retrieved
139     }
140 
141     return false;
142 }
143 
144 void downloadDls(const void function(size_t size) totalSizeCallback = null,
145         const void function(size_t size) chunkSizeCallback = null,
146         const void function() extractCallback = null)
147 {
148     import std.array : appender;
149     import std.net.curl : HTTP;
150     import std.file : exists, isFile, mkdirRecurse, remove, rmdirRecurse, write;
151     import std.format : format;
152     import std.path : buildNormalizedPath;
153     import std.zip : ZipArchive;
154 
155     if (downloadUrl.length > 0 || canDownloadDls)
156     {
157         immutable dlsDir = buildNormalizedPath(dubBinDir, format(dlsDirName, downloadVersion));
158         auto request = HTTP(downloadUrl);
159         auto archiveData = appender!(ubyte[]);
160 
161         if (exists(dlsDir))
162         {
163             if (isFile(dlsDir))
164             {
165                 remove(dlsDir);
166             }
167             else
168             {
169                 rmdirRecurse(dlsDir);
170             }
171         }
172 
173         mkdirRecurse(dlsDir);
174 
175         request.onReceive = (ubyte[] data) {
176             archiveData ~= data;
177             return data.length;
178         };
179 
180         request.onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {
181             import core.time : Duration, msecs;
182 
183             static if (__VERSION__ >= 2075L)
184             {
185                 import std.datetime.stopwatch : StopWatch;
186             }
187             else
188             {
189                 import std.datetime : StopWatch;
190             }
191 
192             static bool started;
193             static bool stopped;
194             static StopWatch watch;
195 
196             if (!started && dlTotal > 0)
197             {
198                 started = true;
199                 watch.start();
200 
201                 if (totalSizeCallback !is null)
202                 {
203                     totalSizeCallback(dlTotal);
204                 }
205             }
206 
207             if (started && !stopped && chunkSizeCallback !is null && dlNow > 0
208                     && (cast(Duration) watch.peek() >= 500.msecs || dlNow == dlTotal))
209             {
210                 watch.reset();
211                 chunkSizeCallback(dlNow);
212 
213                 if (dlNow == dlTotal)
214                 {
215                     stopped = true;
216                     watch.stop();
217                 }
218             }
219 
220             return 0;
221         };
222 
223         request.perform();
224 
225         if (extractCallback !is null)
226         {
227             extractCallback();
228         }
229 
230         auto archive = new ZipArchive(archiveData.data);
231 
232         foreach (name, member; archive.directory)
233         {
234             immutable memberPath = buildNormalizedPath(dlsDir, name);
235             write(memberPath, archive.expand(member));
236 
237             version (Posix)
238             {
239                 import std.process : execute;
240 
241                 if (name == dlsExecutable)
242                 {
243                     execute(["chmod", "+x", memberPath]);
244                 }
245             }
246         }
247     }
248     else
249     {
250         throw new UpgradeFailedException("Cannot download DLS");
251     }
252 }
253 
254 void buildDls(const string dlsDir, const string[] additionalArgs = [])
255 {
256     import core.cpuid : isX86_64;
257     import std.path : buildNormalizedPath;
258     import std.process : Config, execute;
259 
260     auto cmdLine = ["dub", "build", "--build=release"] ~ additionalArgs;
261 
262     version (Windows)
263     {
264         cmdLine ~= ["--compiler=dmd", "--arch=" ~ (isX86_64 ? "x86_64" : "x86_mscoff")];
265     }
266 
267     immutable result = execute(cmdLine, null, Config.none, size_t.max, dlsDir);
268 
269     if (result.status != 0)
270     {
271         throw new UpgradeFailedException("Build failed: " ~ result.output);
272     }
273 }
274 
275 string linkDls()
276 {
277     import std.file : exists, isFile, mkdirRecurse, remove;
278     import std.format : format;
279     import std.path : baseName, buildNormalizedPath;
280     import std..string : endsWith;
281 
282     mkdirRecurse(dubBinDir);
283 
284     immutable dlsDir = buildNormalizedPath(dubBinDir, format(dlsDirName, downloadVersion));
285     immutable oldDlsLink = buildNormalizedPath(dubBinDir, dlsExecutable);
286     immutable dlsLatestDir = buildNormalizedPath(dubBinDir, dlsLatestDirName);
287 
288     if (exists(oldDlsLink) && !exists(dlsLatestDir))
289     {
290         makeLink(buildNormalizedPath(dlsLatestDir, dlsExecutable), oldDlsLink, false);
291     }
292 
293     makeLink(dlsDir, dlsLatestDir, true);
294 
295     return buildNormalizedPath(dlsLatestDir, dlsExecutable);
296 }
297 
298 @property string dubBinDir()
299 {
300     import std.path : buildNormalizedPath;
301     import std.process : environment;
302 
303     version (Windows)
304     {
305         immutable dubDirPath = environment["LOCALAPPDATA"];
306         immutable dubDirName = "dub";
307     }
308     else version (Posix)
309     {
310         immutable dubDirPath = environment["HOME"];
311         immutable dubDirName = ".dub";
312     }
313     else
314     {
315         static assert(false, "Platform not supported");
316     }
317 
318     return buildNormalizedPath(dubDirPath, dubDirName, "packages", ".bin");
319 }
320 
321 private void makeLink(const string target, const string link, bool directory)
322 {
323     version (Windows)
324     {
325         import std.array : join;
326         import std.file : exists, isFile, remove, rmdir;
327         import std.format : format;
328         import std.process : execute;
329 
330         if (exists(link))
331         {
332             if (isFile(link))
333             {
334                 remove(link);
335             }
336             else
337             {
338                 rmdir(link);
339             }
340         }
341 
342         immutable mklinkCommand = format!`mklink %s "%s" "%s"`(directory ? "/J" : "", link, target);
343         const powershellArgs = ["Start-Process", "-Wait", "-FilePath", "cmd.exe",
344             "-ArgumentList", format!"'/c %s'"(mklinkCommand), "-WindowStyle", "Hidden"] ~ (directory
345                 ? [] : ["-Verb", "runas"]);
346         immutable result = execute(["powershell.exe", powershellArgs.join(' ')]);
347 
348         if (result.status != 0)
349         {
350             throw new UpgradeFailedException("Symlink failed: " ~ result.output);
351         }
352     }
353     else version (Posix)
354     {
355         import std.file : exists, remove, symlink;
356 
357         if (exists(link))
358         {
359             remove(link);
360         }
361 
362         symlink(target, link);
363     }
364     else
365     {
366         static assert(false, "Platform not supported");
367     }
368 }
369 
370 class UpgradeFailedException : Exception
371 {
372     this(const string message)
373     {
374         super(message);
375     }
376 }