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