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