-
Notifications
You must be signed in to change notification settings - Fork 17
Portable install of Nodel and App Launcher on a blank Windows client
- use a sensible naming convention
A working nodel-init-bootstrap.bat
online version (supporting online dependencies) can be used from here for convenience but it is strongly recommended you host your own "on-prem" landing content (info further below).
Pick from one of four convenient ways:
- use the console to launch:
mkdir \Nodel
cd \Nodel
curl https://gist.githubusercontent.com/justparking/a2e7b7e39160897d135e45e0f95b867d/raw/nodel-init-bootstrap.bat -o nodel-init-bootstrap.bat
nodel-init-bootstrap.bat
- OR use
notepad.exe
, copy-n-paste text and createnodel-init-bootstrap.bat
- OR copy the batch file using standard Windows Explorer² methods remembering to Unblock if necessary (right-click & see Properties)
- OR launch via network share or downloaded and launch via browser, e.g.
http://NODEL_SERVER:8088/nodes/landing/nodel-init-bootstrap.bat
NOTE:
- Place the batch file in intended Nodel home directory, e.g.
C:\Nodel\nodel-init-bootstrap.bat
might be standard
- double-click batch file OR run from command prompt
- if Shell Swap required, launch with Admin rights, e.g. Run as administrator
-
optional If third-party application is known, use first argument as entry-point, e.g.
> nodel-init-bootstrap.bat D:\MyFunkyContent\MyFunkyContent.exe
- NOTE: argument is optional. App Launcher config can be done at any time post-installation.
- Fast, low impact, portable installer with useful feedback
- Centralised custom installation
- Centralised hosting of any binary dependencies (Internet not required)
- Version management (effectively auto-update)
- Highly customisable
Blank Windows 10 Installation, nothing else.
- For local deployment, Internet not required
- downloads¹ any targeted Java JDK version
- downloads¹ any targeted Nodel version
- downloads¹ any other any targeted dependencies
- Windows PC node created and intelligently named e.g. MM-2-07 - Windows PC (Intel Core i7-7700HQ CPU 2.80GHz by GIGABYTE)
- Application Launcher node created and intelligently named
- Launch shortcut added to Start Menu/Startup
- Explorer Shell swapped (using
WINLOGON/Shell
registry) - other steps can be easily added
Just one tiny batch file, nodel-init-bootstrap.bat
, is required that bootstraps a much larger installation program/script.
That bootstrapper is hosted on a Nodel server called nodel-init-bootstrap.bat
, conveniently hosted via a reasonably permanent node, say Landing
on a port, say 8088.
Convenience, portability, simplicity of operation and understandability.
¹ Large downloads will be cached
² Any familiar method applies, e.g. via USB Drive, Remote Desktop, VNC, TeamViewer, etc.
Adjust top constant(s) to suit
-
For example
landing-host/nodes/Landing/content
should host: -
Example nodel-init-bootstrap.bat
@echo off
set BOOTSTRAP_URI=http://172.16.80.27:8088/nodes/Landing/NodelInit.cs
set REV=15
rem This bootstraps the centrally managed NodelInit binary installer.
set TMPFOLDER=tmp
set SRC=%TMPFOLDER%\tmp1.cs
cd /D "%~dp0"
echo batch: START (rev. %REV%)
mkdir %TMPFOLDER% 2> NUL
echo using System; > %SRC%
echo using System.Collections.Generic; >> %SRC%
echo using System.IO; >> %SRC%
echo using System.Net; >> %SRC%
echo using System.Web; >> %SRC%
echo namespace NodelInitBootstrap { >> %SRC%
echo class Program { >> %SRC%
echo static void Main(string[] args) { >> %SRC%
echo System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; >> %SRC%
echo var uri = new Uri("%BOOTSTRAP_URI%"); >> %SRC%
echo Console.WriteLine("nodel-init-bootstrap: start (rev. " + %REV% + ")"); >> %SRC%
echo string[] segments = uri.Segments; >> %SRC%
echo string filename = "%TMPFOLDER%\\tmp2.cs"; >> %SRC%
echo var req = WebRequest.Create(uri); >> %SRC%
echo req.Method = "GET"; >> %SRC%
echo req.Timeout = 10000; >> %SRC%
echo File.Delete(filename); >> %SRC%
echo var file = File.Create(filename); >> %SRC%
echo using (var resp = req.GetResponse().GetResponseStream()) { >> %SRC%
echo byte[] buffer = new byte[10240]; >> %SRC%
echo for (; ; ) { >> %SRC%
echo int bytesRead = resp.Read(buffer, 0, buffer.Length); >> %SRC%
echo if (bytesRead ^<= 0) >> %SRC%
echo break; >> %SRC%
echo file.Write(buffer, 0, bytesRead); >> %SRC%
echo } >> %SRC%
echo } >> %SRC%
echo file.Close(); >> %SRC%
echo } >> %SRC%
echo } >> %SRC%
echo } >> %SRC%
%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\csc.exe /nologo /reference:C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.IO.Compression.FileSystem.dll -out:%TMPFOLDER%\_nodel-init-bootstrap.exe %SRC%
rem No error checking, leaving as best effort
rem if NOT ERRORLEVEL 0 goto FAILURE
rem if ERRORLEVEL 1 goto FAILURE
%TMPFOLDER%\_nodel-init-bootstrap.exe %*
rem if NOT ERRORLEVEL 0 goto TRYANYWAY
rem if ERRORLEVEL 1 goto TRYANYWAY
%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\csc.exe /nologo /reference:C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.IO.Compression.FileSystem.dll -out:nodel-init.exe %TMPFOLDER%\tmp2.cs
rem if NOT ERRORLEVEL 0 goto FAILURE
rem if ERRORLEVEL 1 goto FAILURE
:TRYANYWAY
nodel-init.exe %*
if NOT ERRORLEVEL 0 goto FAILURE
if ERRORLEVEL 1 goto FAILURE
echo batch: success
goto FINISHED
:FAILURE
echo batch: failure
:FINISHED
echo batch: finished
- Example NodelInit.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
using System.Runtime.InteropServices.ComTypes;
using System.Diagnostics;
using System.Threading;
using System.Management;
using Microsoft.Win32;
// Compiles to nodel-init.exe
//
// Please adjust constants to suit environment.
//
// This program must fit in a single file.
// Some coding styles not adhered to for the sake of brevity.
//
// HISTORY
// 2020-07-21 21 JP: First time in Nodel wiki
//
namespace NodelInit
{
class Program
{
readonly int REV = 21;
static string INSTALLDOWNLOAD_PREFIX = "http://172.16.80.27:8088/nodes/Landing/";
string BOOTSTRAP_BATCH_FILE = ".\\nodel-init-bootstrap.bat";
string DOWNLOADPREFIX_7Z = INSTALLDOWNLOAD_PREFIX; // "https://www.7-zip.org/a/";
string DOWNLOADNAME_7Z = "7za920.zip";
string INSTALL_7ZIP = ".\\bin\\7-Zip";
string DOWNLOADPREFIX_JDK = INSTALLDOWNLOAD_PREFIX; // "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u242-b08/";
string DOWNLOADNAME_JDK_64 = "OpenJDK8U-jdk_x64_windows_hotspot_8u242b08.zip";
string DOWNLOADNAME_JDK_32 = "OpenJDK8U-jdk_x86-32_windows_hotspot_8u242b08.zip";
string INSTALL_JDK = ".\\bin\\jdk8u232-b09";
string JAVA_EXE_PATH; // init in Launch
string DOWNLOADPREFIX_NODELJAR = INSTALLDOWNLOAD_PREFIX; // "https://github.com/museumsvictoria/nodel/releases/download/v2.2.1.425/";
string DOWNLOADNAME_NODELJAR = "nodelhost-dev-2.2.1-rev425.jar";
string INSTALL_NODELJAR = ".\\bin";
string DOWNLOAD_COMPUTER_RECIPE_RAW = INSTALLDOWNLOAD_PREFIX + "ComputerControllerRecipe.py";
string DOWNLOAD_COMPUTER_CONTROLLER_RAW = INSTALLDOWNLOAD_PREFIX + "ComputerController.cs";
string DOWNLOAD_APP_RECIPE_RAW = INSTALLDOWNLOAD_PREFIX + "AppLauncherRecipe.py";
string DOWNLOAD_FOLDER = "downloads";
string SEVENZ_EXE;
string PROG_DATA_PATH;
string APP_DATA_PATH;
// the above are set in Launch()
[STAThread]
static void Main(string[] args)
{
// required for sites that require TLS2 for HTTPS
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
int closeIn = 15;
try
{
Program program = new Program();
program.Launch(args);
}
catch (Exception exc)
{
Log("");
Log("");
Log("errors - \"{0}\"", exc.Message);
Log("");
Log("");
Log("");
Log("");
Log(exc.ToString());
closeIn = 100;
}
Console.WriteLine("closing in {0} seconds...", closeIn);
Thread.Sleep(closeIn * 1000);
}
void Launch(string[] args)
{
string currentFolder = Directory.GetCurrentDirectory();
bool is64 = Environment.Is64BitOperatingSystem;
PROG_DATA_PATH = Environment.GetEnvironmentVariable("ProgramData"); // for Start Menu -> Start
APP_DATA_PATH = Environment.GetEnvironmentVariable("AppData"); // also Start Menu -> Start
SEVENZ_EXE = Path.Combine(PROG_DATA_PATH, "7-Zip\\7z.exe");
Log("nodel-init for ACMI rev. {0}, dotNet:{1}, currentfolder:{2} is64:{3}, args={4}", REV, Environment.Version, currentFolder, is64, string.Join(",", args));
// Download Java jdk
string jdkFilename = is64 ? DOWNLOADNAME_JDK_64 : DOWNLOADNAME_JDK_32;
String jdkFilePath = GetOrDownloadURL(DOWNLOADPREFIX_JDK + jdkFilename, filename: jdkFilename);
// ... and extract
ExtractIfMissing(jdkFilePath, INSTALL_JDK);
JAVA_EXE_PATH = FindFileOrFail("java.exe", INSTALL_JDK);
// Download 7-Zip
string sevenZipFilename = GetOrDownloadURL(DOWNLOADPREFIX_7Z + DOWNLOADNAME_7Z, filename: DOWNLOADNAME_7Z);
// ... and extract
ExtractIfMissing(sevenZipFilename, INSTALL_7ZIP);
SEVENZ_EXE = Path.Combine(INSTALL_7ZIP, "7za.exe");
// Download nodel JAR
string nodelJarFilepath = GetOrDownloadURL(DOWNLOADPREFIX_NODELJAR + DOWNLOADNAME_NODELJAR, filename: DOWNLOADNAME_NODELJAR);
// ... copy JAR
string installJarPath = Path.Combine(INSTALL_NODELJAR, DOWNLOADNAME_NODELJAR);
if (!File.Exists(installJarPath))
{
File.Copy(nodelJarFilepath, installJarPath);
}
// Update autolaunch shortcut in Start Menu/StartUp
string bootstrapBatchFilePath = Path.GetFullPath(BOOTSTRAP_BATCH_FILE);
IShellLink link = (IShellLink)new ShellLink();
link.SetDescription("Nodel Bootstrap Launcher");
link.SetPath(bootstrapBatchFilePath);
IPersistFile file = (IPersistFile)link;
// can do this for all users but this requires admin access:
// String linkPath = Path.Combine(PROG_DATA_PATH, @"Microsoft\Windows\Start Menu\Programs\StartUp\nodelLaunch-shortcut.lnk");
try {
String linkPath = Path.Combine(APP_DATA_PATH, @"Microsoft\Windows\Start Menu\Programs\Startup\nodel-init-bootstrap_shortcut.lnk"); // e.g. C:\Users\Justin\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
file.Save(linkPath, false);
} catch (Exception exc) {
Log("Could not create traditional StartMenu link, ignoring.");
}
// create a computer node if necessary
// (later versions of Nodel allow name templating)
string computerNodeName = "$HOSTNAME - Windows PC ($CPUName $MaxClockSpeed $Model by $Manufacturer)";
string computeNodePath = Path.Combine(string.Format(".\\nodes\\{0}", computerNodeName));
if (!Directory.Exists(computeNodePath))
{
string computerScriptPath = GetOrDownloadURL(DOWNLOAD_COMPUTER_RECIPE_RAW, filename: "computerScript.py");
Directory.CreateDirectory(computeNodePath);
File.Copy(computerScriptPath, Path.Combine(computeNodePath, "script.py"));
File.Copy(GetOrDownloadURL(DOWNLOAD_COMPUTER_CONTROLLER_RAW, filename: "ComputerController.cs"), Path.Combine(computeNodePath, "ComputerController.cs"));
}
// create application node if necessary
string appLauncherNodeName = "$HOSTNAME - App Launcher";
string appLauncherNodePath = Path.Combine(string.Format(".\\nodes\\{0}", appLauncherNodeName));
if (!Directory.Exists(appLauncherNodePath))
{
string appLauncherScriptPath = GetOrDownloadURL(DOWNLOAD_APP_RECIPE_RAW, filename: "appLauncherScript.py");
Directory.CreateDirectory(appLauncherNodePath);
File.Copy(appLauncherScriptPath, Path.Combine(appLauncherNodePath, "script.py"));
if (args.Length > 0)
{
string appEntryPoint = args[0];
string appWorkingDir = Path.GetDirectoryName(appEntryPoint);
string configContents = "{'remoteBindingValues': { 'actions': {}, 'events': {}}, 'paramValues': { 'AppPath': '{0}', 'AppWorkingDir': '{1}', 'PowerStateOnStart': 'On'}}"
.Replace("{0}", appEntryPoint.Replace(@"\", @"\\"))
.Replace("{1}", appWorkingDir.Replace(@"\", @"\\"));
File.WriteAllText(Path.Combine(appLauncherNodePath, "nodeConfig.json"), configContents);
}
}
// update registry if necessary // Computer\HKEY_LOCAL_MACHINE\
var regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon");
// try update it if it doesn't exist - NOTE: will require admin rights
Log("Updating 'Winlogon/shell' registry if possible");
try
{
if (!regKey.GetValue("Shell").ToString().EndsWith(BOOTSTRAP_BATCH_FILE))
{
regKey.Close();
regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon", true);
regKey.SetValue("Shell", bootstrapBatchFilePath);
}
} catch (Exception)
{
Log("!! Could not update WinLogin / Shell registry; ignoring");
}
regKey.Close();
Log("Launching Nodel and finishing...");
Log("ENSURE FIREWALL RULES HAVE BEEN ACKNOWLEDGED IF NECESSARY");
Process.Start(new ProcessStartInfo()
{
WorkingDirectory = Path.GetFullPath("."),
Arguments = String.Format("-jar \"{0}\"", installJarPath),
FileName = JAVA_EXE_PATH,
});
Log("Finished");
}
#region Convenience
// (returns root folder)
void ExtractIfMissing(string filename, string folder)
{
Log("extracting {0} into {1}", filename, folder);
if (!File.Exists(filename))
throw new FileNotFoundException();
if (Directory.Exists(folder))
{
Log("{0} already present", folder);
return;
}
// use a tmp directory to minimise half extractions on failure
string tmpDir = folder + "_tmp";
if (File.Exists(tmpDir))
Directory.Delete(tmpDir, true);
Directory.CreateDirectory(tmpDir);
ZipFile.ExtractToDirectory(filename, tmpDir);
// check if only one file is in root, use that as directory
var rootDirs = Directory.GetDirectories(tmpDir);
var rootFiles = Directory.GetFiles(tmpDir);
if (rootDirs.Length == 1 && rootFiles.Length == 0)
{
Directory.Move(rootDirs[0], folder);
// and delete left-over blank directory
Directory.Delete(tmpDir);
}
else
{
Directory.Move(tmpDir, folder);
}
}
String GetOrDownloadURL(String url, Dictionary<String, String> headers = null, String contentType = null, String post = null, String username = null, String password = null, String filename = null)
{
if (filename != null)
{
Log("downloading {0} from {1}", filename, url);
// create a download directory
Directory.CreateDirectory(DOWNLOAD_FOLDER);
}
var filepath = Path.Combine(DOWNLOAD_FOLDER, filename);
var req = WebRequest.Create(url);
// req.Timeout = 20000;
String etag = null; //etag data
string etagFilename = filepath + ".etag"; // if downloading
if (File.Exists(etagFilename))
{
etag = File.ReadAllText(etagFilename);
req.Headers["If-None-Match"] = etag;
}
if (headers != null)
{
foreach (var header in headers)
req.Headers.Add(header.Key, header.Value);
}
if (username != null)
{
String encoded = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password));
req.Headers.Add("Authorization", "Basic " + encoded);
}
if (String.IsNullOrEmpty(post))
{
req.Method = "GET";
}
else
{
req.Method = "POST";
if (contentType == null)
req.ContentType = "application/x-www-form-urlencoded";
else
req.ContentType = contentType;
var reqStream = req.GetRequestStream();
StreamWriter sw = new StreamWriter(reqStream);
sw.Write(post);
sw.Flush();
sw.Close();
}
if (filepath != null)
{
// download a file
string tmpFilename = Path.Combine(DOWNLOAD_FOLDER, "_tmpdownload_" + filename);
File.Delete(tmpFilename); // start again
try
{
var resp = (HttpWebResponse)req.GetResponse();
long contentLength = resp.ContentLength;
long totalBytesRead = 0;
long displayChunk = 0;
using (var respStream = resp.GetResponseStream())
{
using (var file = File.Create(tmpFilename))
{
byte[] buffer = new byte[65536];
for (; ; )
{
int bytesRead = respStream.Read(buffer, 0, buffer.Length);
if (bytesRead <= 0)
break;
totalBytesRead += bytesRead;
displayChunk += bytesRead;
if (displayChunk >= 1048576)
{
Log("downloaded {0} KB...", totalBytesRead / 1024);
displayChunk = 0;
}
file.Write(buffer, 0, bytesRead);
}
}
}
if (contentLength != -1 && contentLength != totalBytesRead)
throw new Exception("Bad download - expected " + contentLength + " bytes, actually got " + totalBytesRead);
if (File.Exists(filepath))
File.Delete(filepath);
// only write etag after downloading
etag = resp.Headers.Get("ETag");
if (!String.IsNullOrWhiteSpace(etag))
{
File.WriteAllText(etagFilename, etag);
}
File.Move(tmpFilename, filepath);
return filepath;
}
catch (WebException ex)
{
var exResp = ex.Response as HttpWebResponse;
if (exResp != null && exResp.StatusCode == HttpStatusCode.NotModified)
{
Log("already downloaded; no change");
return filepath;
}
// some other exception, log and ignore
Log("could not download; continuing with existing - " + ex.Message);
if (File.Exists(filepath))
return filepath;
else
throw ex;
}
}
else
{
// pull data (no download)
var resp = req.GetResponse();
StreamReader sr = new StreamReader(resp.GetResponseStream());
String data = sr.ReadToEnd();
return data;
}
}
static String FindFileOrFail(String filename, String root)
{
var result = System.IO.Directory.GetFiles(root, "java.exe", SearchOption.AllDirectories);
if (result == null || result.Length == 0)
throw new Exception("Could not find file " + filename + " within " + root);
return result[0];
}
static string GetProcessorName()
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Processor");
string procName = null;
foreach (ManagementObject moProcessor in searcher.Get())
{
if (moProcessor["name"] != null)
procName = moProcessor["name"].ToString();
}
if (string.IsNullOrWhiteSpace(procName))
throw new Exception("Could not determine processor name");
return procName.Trim();
}
static string GetManufacturer()
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_ComputerSystem");
string manuName = null;
foreach (ManagementObject moProcessor in searcher.Get())
{
if (moProcessor["Manufacturer"] != null)
manuName = moProcessor["Manufacturer"].ToString();
}
if (string.IsNullOrWhiteSpace(manuName))
throw new Exception("Could not determine manufacturer name");
return manuName.Trim();
}
#endregion
#region Windows/DLL
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
internal class ShellLink
{
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
internal interface IShellLink
{
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out IntPtr pfd, int fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
void Resolve(IntPtr hwnd, int fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
#endregion
#region Logging
static void Log(string msg)
{
Console.WriteLine("nodel-init: " + msg);
}
static void Log(string msgFormat, params object[] args)
{
Log(string.Format(msgFormat, args));
}
#endregion
}
}
Nodel: http://nodel.io/ | White Paper