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