Add installer heavily based on Stephan Darnell's

This commit is contained in:
Sashko Stubailo
2014-11-19 08:02:22 -08:00
committed by Sashko Stubailo
parent 82a37c2c7c
commit 149121f07f
3 changed files with 561 additions and 0 deletions

3
scripts/windows/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.tar.gz
*.exe
*.pdb

View File

@@ -0,0 +1,550 @@
// Executable to launch meteor after bootstrapping the local warehouse
//
// Copyright 2013 - 2014 Stephen Darnell
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;
[assembly: AssemblyTitle("Windows Meteor installer")]
[assembly: AssemblyDescription("Downloads the Meteor bootstrap package installs it")]
[assembly: AssemblyCompany("Meteor Development Group")]
[assembly: AssemblyProduct("Meteor")]
[assembly: AssemblyCopyright("Copyright 2014 Meteor Development Group")]
[assembly: AssemblyVersion("0.1.0.0")]
[assembly: AssemblyFileVersion("0.1.0.0")]
namespace LaunchMeteor
{
class Program
{
// private const string BOOTSTRAP_FILE = "meteor-bootstrap-Windows_i686.tar.gz"; // pre-0.9.x
private const string BOOTSTRAP_FILE = "meteor-bootstrap-os.windows.x86_32-0.0.6.tar.gz";
private const string BOOTSTRAP_URL = "https://warehouse.meteor.com/windows/bootstrap/" + BOOTSTRAP_FILE;
private const string METEOR_WAREHOUSE_DIR = "METEOR_WAREHOUSE_DIR";
private static string bootstrapFile = null;
private static bool looksLikeNewConsole = false;
private static int consoleWindowWidth = 80;
private static void InitialiseConsoleInfo()
{
// Try/catch needed when not connected to a console
try
{
looksLikeNewConsole = Console.CursorLeft == 0 && Console.CursorTop == 0;
consoleWindowWidth = Console.WindowWidth;
} catch {}
}
static void Main(string[] args)
{
InitialiseConsoleInfo();
// Avoid console vanishing without warning if invoked from a non-console app
AppDomain.CurrentDomain.UnhandledException += (sender, handlerArgs) =>
{
Console.WriteLine("Unexpected exception: {0}", handlerArgs.ExceptionObject);
Exit(1);
};
if (args.Length == 1 && args[0] == "--downloaded")
{
bootstrapFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, BOOTSTRAP_FILE);
args = new string[0];
}
var home = Environment.GetEnvironmentVariable("LOCALAPPDATA") ??
Environment.GetEnvironmentVariable("APPDATA");
var warehouse = Path.Combine(home, ".meteor");
Environment.SetEnvironmentVariable(METEOR_WAREHOUSE_DIR, warehouse);
// XXX will this overwrite if Meteor is already installed?
// we would like it to
BootstrapWarehouse(warehouse);
Console.WriteLine("To run Meteor, open a new Command Prompt and type 'meteor'");
Exit(1);
}
#region Executing child processes
private static void Exec(string command, string extra, string[] args)
{
if (extra != null)
{
var list = new List<string>(args);
list.Insert(0, extra);
args = list.ToArray();
}
string commandLine = string.Join(" ", Array.ConvertAll<string, string>(args, QuoteArg));
if (!File.Exists(command))
{
Console.WriteLine("Unable to find executable for command:");
Console.WriteLine(" {0} {1}", command, commandLine);
Exit(1);
}
var child = Process.Start(new ProcessStartInfo(command, commandLine) { UseShellExecute = false });
child.WaitForExit();
Exit(child.ExitCode);
}
private static string QuoteArg(string unquoted)
{
if (unquoted.Length > 0 && unquoted.IndexOfAny(" \t\n\v\"".ToCharArray()) == -1)
return unquoted;
var result = new StringBuilder("\"");
int slashes = 0;
foreach (var ch in unquoted)
{
if (ch == '"') // Double up any slashes and escape the quote
{
while (slashes-- >= 0) result.Append('\\');
}
result.Append(ch);
slashes = (ch == '\\') ? slashes + 1 : 0;
}
return result.Append('"').ToString();
}
public static void Exit(int exitCode)
{
if (looksLikeNewConsole)
{
Console.WriteLine("\nPlease press any key to exit.");
Console.ReadKey(true);
}
Environment.Exit(exitCode);
}
#endregion
#region Boostrap the warehouse
private static MemoryStream DownloadBoostrapFile()
{
Console.WriteLine("Downloading initial Meteor files...");
DownloadDataCompletedEventArgs download = null;
var complete = new AutoResetEvent(false);
var barWidth = consoleWindowWidth - 5;
using (var client = new WebClient())
{
if (client.Proxy != null)
{
client.Proxy.Credentials = CredentialCache.DefaultCredentials;
}
client.UseDefaultCredentials = true;
client.DownloadProgressChanged += (sender, e) =>
{
var sb = new StringBuilder();
sb.AppendFormat("\r{0:00} ", e.ProgressPercentage);
int blobs = (barWidth * e.ProgressPercentage) / 100;
for (int i = 0; i < barWidth; i++) sb.Append(i < blobs ? '#' : '-');
Console.Write(sb.ToString());
};
client.DownloadDataCompleted += (sender, e) =>
{
download = e;
complete.Set();
};
client.DownloadDataAsync(new Uri(BOOTSTRAP_URL));
}
complete.WaitOne();
if (download.Error != null)
throw download.Error;
if (download.Result.Length < 10 * 1024 * 1024 ||
(download.Result[0] != 0x1f || download.Result[1] != 0x8b))
{
throw new InvalidDataException("Unexpected data returned from: " + BOOTSTRAP_URL);
}
Console.WriteLine(" \rDownload complete ({0:#.#} MB)", download.Result.Length / (1024.0 * 1024.0));
var stream = new MemoryStream(download.Result);
download = null;
return stream;
}
private static void BootstrapWarehouse(string warehouse)
{
MemoryStream stream;
if (bootstrapFile != null)
{
var data = File.ReadAllBytes(bootstrapFile);
stream = new MemoryStream(data);
data = null;
}
else
{
try
{
stream = DownloadBoostrapFile();
}
catch (Exception)
{
Console.WriteLine("\nERROR: A problem occurred while downloading the bootstrap package.");
Console.WriteLine("\nIf this persists, you can download it manually from:");
Console.WriteLine(" " + BOOTSTRAP_URL);
Console.WriteLine("and put it in the same directory as LaunchMeteor.exe and run:");
Console.WriteLine(" LaunchMeteor.exe -downloaded");
Console.WriteLine("\nHere are some details of the error:");
throw;
}
}
Console.WriteLine("Extracting files to {0}", warehouse);
var tempDir = warehouse + "~";
if (File.Exists(tempDir))
File.Delete(tempDir);
DirectoryDelete(tempDir);
try
{
var regex = new Regex(@"^\.meteor\\");
ExtractTgz(stream, tempDir, p => regex.Replace(p, ""));
DirectoryDelete(warehouse);
Directory.Move(tempDir, warehouse);
}
catch
{
DirectoryDelete(tempDir);
throw;
}
Console.WriteLine("Files extracted successfully\n");
var path = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty;
var paths = path.Split(';');
if (!Array.Exists(paths, p => p.Equals(warehouse, StringComparison.OrdinalIgnoreCase)))
{
Console.WriteLine("Updating PATH to include {0}", warehouse);
path += ((path.Length > 0) ? ";" : "") + warehouse;
Environment.SetEnvironmentVariable("PATH", path, EnvironmentVariableTarget.User);
}
}
private static void DirectoryDelete(string path)
{
for (int attempt = 1; Directory.Exists(path) && attempt <= 5; attempt++)
{
if (attempt == 1)
Console.WriteLine("Deleting directory: {0}", path);
else
Console.WriteLine("Deleting directory: {0} attempt {1}", path, attempt);
try { RecursiveDeleteDirectory(path); } catch {}
if (Directory.Exists(path))
Thread.Sleep(1000);
}
// Throw the exception
if (Directory.Exists(path))
RecursiveDeleteDirectory(path);
}
#endregion
#region Tar file extraction
public static void ExtractTgz(string archive, string targetDirectory)
{
using (var fileStream = File.OpenRead(archive))
{
ExtractTgz(fileStream, targetDirectory, p => p);
}
}
public static void ExtractTgz(Stream stream, string directory, Func<string, string> transform)
{
int totalFiles = 0, totalData = 0;
var buffer = new byte[512];
using (var decompressed = new GZipStream(stream, CompressionMode.Decompress))
{
string longName = null;
for (int n; (n = decompressed.Read(buffer, 0, buffer.Length)) > 0; )
{
if (n != buffer.Length)
throw new InvalidDataException("Unexpected end of TAR file");
if (TarField(buffer, 257, 5) != "ustar") continue;
var type = (TarType)buffer[156];
var length = Convert.ToInt32(TarField(buffer, 124, 12).Trim(), 8);
var link = TarField(buffer, 157, 100);
var path = longName ?? Path.Combine(TarField(buffer, 345, 155), TarField(buffer, 0, 100));
longName = null;
if (type == TarType.LongName)
{
var data = new MemoryStream(length);
for (; length > 0; length -= buffer.Length)
{
if (decompressed.Read(buffer, 0, buffer.Length) != buffer.Length)
throw new InvalidDataException("Unexpected end of TAR file");
data.Write(buffer, 0, Math.Min(length, buffer.Length));
}
longName = TarField(data.ToArray(), 0, (int)data.Length);
continue;
}
//Console.WriteLine("{0} {1} {2}", type, length.ToString().PadLeft(9), path);
if (type == TarType.AltReg || type == TarType.Reg || type == TarType.Contig ||
type == TarType.Sym || type == TarType.Lnk)
{
if (((++totalFiles) & 0xF) == 0) Console.Write(".");
path = path.Replace('/', '\\');
if (("\\" + path + "\\").Contains("\\..\\"))
throw new InvalidDataException("Filenames containing '..' are not allowed");
path = Path.Combine(directory, transform(path));
try
{
CreateDirectory(GetDirectoryName(path));
using (var fstream = CreateWritableFile(path))
{
if (type == TarType.Lnk || type == TarType.Sym)
{
var data = Encoding.UTF8.GetBytes(link);
fstream.Write(data, 0, data.Length);
length = 0;
}
totalData += length;
for (; length > 0; length -= buffer.Length)
{
if (decompressed.Read(buffer, 0, buffer.Length) != buffer.Length)
throw new InvalidDataException("Unexpected end of TAR file");
fstream.Write(buffer, 0, Math.Min(length, buffer.Length));
}
}
}
catch
{
Console.WriteLine();
Console.WriteLine("Error processing path: {0}", path);
throw;
}
}
}
Console.WriteLine("\nExtracted {0} files ({1:#.#} MB)", totalFiles, totalData / (1024.0 * 1024.0));
}
}
private enum TarType : int { AltReg = 0, Reg = '0', Lnk = '1', Sym = '2', Chr = '3', Blk = '4', Dir = '5', Fifo = '6', Contig = '7', LongName = 'L' }
private static string TarField(byte[] buffer, int start, int len)
{
var str = Encoding.UTF8.GetString(buffer, start, len);
int pos = str.IndexOf('\0');
return pos < 0 ? str : str.Substring(0, pos);
}
#endregion
// Get directory name (supporting long file names)
private static string GetDirectoryName(string path)
{
path = path.Replace('/', '\\');
int pos = path.LastIndexOf('\\');
return (pos >= 0) ? path.Substring(0, pos) : null;
}
// Create a file, supporting long file names
private static FileStream CreateWritableFile(string path)
{
SafeFileHandle handle = NativeMethods.CreateFile(@"\\?\" + path,
EFileAccess.GenericWrite, EFileShare.None, IntPtr.Zero,
ECreationDisposition.CreateAlways, 0, IntPtr.Zero);
int error = Marshal.GetLastWin32Error();
if (handle.IsInvalid)
throw new System.ComponentModel.Win32Exception(error);
// Pass the file handle to FileStream. FileStream will close it.
return new FileStream(handle, FileAccess.Write);
}
// Create a directory, supporting long file names
private static void CreateDirectory(string path)
{
bool result = NativeMethods.CreateDirectory(@"\\?\" + path, IntPtr.Zero);
int error = Marshal.GetLastWin32Error();
if (result || error == NativeMethods.ERROR_ALREADY_EXISTS)
return;
if (error != NativeMethods.ERROR_PATH_NOT_FOUND)
throw new System.ComponentModel.Win32Exception(error);
// Try to create parent first, before trying again
CreateDirectory(GetDirectoryName(path));
CreateDirectory(path);
}
private static void RecursiveDeleteDirectory(string path)
{
path = path.TrimEnd('\\');
IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
WIN32_FIND_DATA findData;
IntPtr handle = NativeMethods.FindFirstFile(@"\\?\" + path + @"\*", out findData);
if (handle != INVALID_HANDLE_VALUE)
{
for (bool more = true; more; more = NativeMethods.FindNextFile(handle, out findData))
{
string name = findData.cFileName;
if (((int)findData.dwFileAttributes & NativeMethods.FILE_ATTRIBUTE_DIRECTORY) != 0)
{
if (name != "." && name != "..")
RecursiveDeleteDirectory(Path.Combine(path, name));
}
else
{
if (!NativeMethods.DeleteFile(@"\\?\" + Path.Combine(path, name)))
{
int error = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(error);
}
}
}
}
NativeMethods.FindClose(handle);
if (!NativeMethods.RemoveDirectory(@"\\?\" + path))
{
int error = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(error);
}
}
}
// PInvoke support for long file names
internal static class NativeMethods
{
public const int FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
public const int ERROR_PATH_NOT_FOUND = 3;
public const int ERROR_ALREADY_EXISTS = 183;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern SafeFileHandle CreateFile(
string lpFileName,
EFileAccess dwDesiredAccess,
EFileShare dwShareMode,
IntPtr lpSecurityAttributes,
ECreationDisposition dwCreationDisposition,
EFileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteFile(string lpFileName);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CreateDirectory(string lpPathName, IntPtr lpSecurityAttributes);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool RemoveDirectory(string lpPathName);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FindClose(IntPtr hFindFile);
}
[StructLayout(LayoutKind.Sequential)]
internal struct FILETIME
{
internal uint dwLowDateTime;
internal uint dwHighDateTime;
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct WIN32_FIND_DATA
{
internal EFileAttributes dwFileAttributes;
internal FILETIME ftCreationTime;
internal FILETIME ftLastAccessTime;
internal FILETIME ftLastWriteTime;
internal int nFileSizeHigh;
internal int nFileSizeLow;
internal int dwReserved0;
internal int dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
internal string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
internal string cAlternate;
}
[Flags]
public enum EFileAccess : uint
{
GenericRead = 0x80000000,
GenericWrite = 0x40000000,
GenericExecute = 0x20000000,
GenericAll = 0x10000000,
}
[Flags]
public enum EFileShare : uint
{
None = 0x00000000,
Read = 0x00000001,
Write = 0x00000002,
Delete = 0x00000004,
}
public enum ECreationDisposition : uint
{
New = 1,
CreateAlways = 2,
OpenExisting = 3,
OpenAlways = 4,
TruncateExisting = 5,
}
[Flags]
public enum EFileAttributes : uint
{
Readonly = 0x00000001,
Hidden = 0x00000002,
System = 0x00000004,
Directory = 0x00000010,
Archive = 0x00000020,
Device = 0x00000040,
Normal = 0x00000080,
Temporary = 0x00000100,
SparseFile = 0x00000200,
ReparsePoint = 0x00000400,
Compressed = 0x00000800,
Offline = 0x00001000,
NotContentIndexed = 0x00002000,
Encrypted = 0x00004000,
Write_Through = 0x80000000,
Overlapped = 0x40000000,
NoBuffering = 0x20000000,
RandomAccess = 0x10000000,
SequentialScan = 0x08000000,
DeleteOnClose = 0x04000000,
BackupSemantics = 0x02000000,
PosixSemantics = 0x01000000,
OpenReparsePoint = 0x00200000,
OpenNoRecall = 0x00100000,
FirstPipeInstance = 0x00080000
}
}

View File

@@ -0,0 +1,8 @@
@echo off
pushd "%~dp0"
echo Compiling InstallMeteor
%WINDIR%\Microsoft.NET\Framework\v3.5\csc.exe InstallMeteor.cs /debug /nologo
popd