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 }