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 }