OSC Install New/Latest Version Using AHK

Hi,
I wrote a script a whilst back that auto-installs the latest OSC update. I thought I'd clean it up a little (didn't really debloat it though, apologies), and share it if anyone else was interested.

This uses AHK (sorry mac), but perhaps in future, I can simply just incorporate using JS in the custom module and/or a python script for cross-platform.

Warning, this will write over the current OSC install. Meaning, if you modify any files within OSC install directory this will write it over everything.

The script does the following:

  • Has to be ran as admin to write/create directories, and unzip the latest OSC download
  • Checks the settings.ini first for a valid directory path, if not asks for one than creates it.
  • Checks for latest version using api.github latest releases
  • Checks if you already have the latest version by using the specified settings path "\open-stage-control.exe"
  • Shows the latest version and changelog if any, and ask if you'd like to install.
  • Downloads, copies file to settings path, unzips, and runs OSC.

Here is a link to the repo. Feel free to adapt to your own needs. Just wanted to share!

Example image of popup:
image

Here is the AHK code, but there are a couple includes I use and a settings.ini file to read from. The main one needed is a JSON library such as JXON or cJson. The other one is just a pretty notify popup I use from time to time.

; v1.3.12
#Requires Autohotkey v1.1.33+
#SingleInstance, Force ; Limit one running version of this script
SetBatchlines -1 ; run at maximum CPU utilization
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
#Include %A_ScriptDir%\Includes\Jxon.ahk
#Include %A_ScriptDir%\Includes\Notify.ahk
; --------------------------------------
checkAdminStatus()

global userOSCPath := checkUserOSCPath(oscPath)
global notificationSettings := {Title: ""
	, Font: "Sans Serif"
	, TitleFont: "Sans Serif"
	, Icon: A_ScriptDir "\assets\logo.png"
	, Animate: "Right, Slide"
	, ShowDelay: 100
	, IconSize: 64
	, TitleSize: 14
	, Size: 20
	, Radius: 26
	, Time: 999999
	, Background: "0x2C323A"
	, Color: "0xD8DFE9"
, TitleColor: "0xD8DFE9"}

currVersion := checkForCurrentVersion(userOSCPath)

Endpoint := "https://api.github.com/repos/jean-emmanuel/open-stage-control/releases/latest"
req := ComObjCreate("WinHttp.WinHttpRequest.5.1")
req.Open("GET", Endpoint, true)
req.Send()
req.WaitForResponse()

if (req.status != 200)
	errorTryAgain("ERROR", "There was an error with the request. Please try again later. `nError: " req.status)

res := req.ResponseBody
assets := JXON_Load(BinArr_ToString(res))
changelog := assets.body
latestVersion := SubStr(assets.tag_name, 2)

updateType := (changelog ~= "i)midi" | changelog ~= "i)electron") ? "major" : "minor"

if (currVersion == latestVersion) {
	installBetaOrPrev := upToDate(currVersion)
	if (installBetaOrPrev == false)
		ExitApp
	Endpoint := "https://api.github.com/repos/jean-emmanuel/open-stage-control/releases"
	req := ComObjCreate("WinHttp.WinHttpRequest.5.1")
	req.Open("GET", Endpoint, true)
	req.Send()
	req.WaitForResponse()

	if (req.status != 200)
		errorTryAgain("ERROR", "There was an error with the request. Please try again later. `nError: " req.status)

	res := req.ResponseBody
	assets := JXON_Load(BinArr_ToString(res))
	assets := (installBetaOrPrev == "previous") ? assets[getPreviousVersions(assets)] : assets.1

	changelog := assets.body
	latestVersion := SubStr(assets.tag_name, 2)

	updateType := "major"
}

if (!FileExist(oscPath "\open-stage-control.exe"))
	updateType := "major"

fileRegex := updateType == "major" ? "i)win32-x64.zip" : "i)node.zip"

Loop % assets.assets.Length() {
	if (assets.assets[A_Index].name ~= fileRegex) {
		latestVersion := assets.assets[A_Index].name
		latestVersionURL := assets.assets[A_Index].browser_download_url
		Break
	}
}

if (!latestVersion)
	errorTryAgain("ERROR", "couldn't find OSC latest version for Windows")

MsgBox 0x41, OSC Installing New Version, % latestVersion ": was found`, would you like to install it?`nChanges Included:`n`n" changelog
IfMsgBox OK, {

} Else IfMsgBox Cancel, {
	ExitApp
}

notificationSettings.title := "found latest version"
Notify().AddWindow("downloading update now...", notificationSettings)

UrlDownloadToFile, % latestVersionURL, % A_ScriptDir "/" latestVersion

if WinExist("ahk_exe open-stage-control.exe") ; Close OSC before installing new version
	WinClose, % "ahk_exe open-stage-control.exe"

OSC_Zip := A_ScriptDir "\" latestVersion
OSC_Folder := updateType == "minor" ? userOSCPath "\resources\app" : userOSCPath
SplitPath, OSC_Zip, vName, vDir, vEXT, vNNE, vDrive
if (vEXT != "zip")
	errorTryAgain("ERROR", "There was an error with the downloading the update. Please try again later.")

FileDelete, % OSC_Folder "\*.*"
Loop, Files, % OSC_Folder "\*.*", D
{
	Path := OSC_Folder "\" A_LoopFileName
	if (Path != OSC_Folder "\_Archive") {
		FileRemoveDir, % Path, 1
	}
}

FileMove, % OSC_Zip, % OSC_Folder "\*.*", 1
OSC_Zip_New := OSC_Folder . "\" . vName
While(!FileExist(OSC_Zip_New))
	Sleep, 10

DetectHiddenWindows, On
Unzip(OSC_Zip_New, OSC_Folder)
Sleep, 1000 ; Wait for unzip to finish
SplitPath, OSC_Zip_New, vvName, vAppDir, vvEXT, vvNNE, vvDrive
OSC_Unzip := vAppDir . "\" . vvNNE
SplitPath, OSC_Folder,,vResourcesDir

destPath := updateType == "major" ? vResourcesDir . "\Open Stage Control" : vResourcesDir "\app"
Loop, Files, % OSC_Unzip "\*.*", F
	FileMove, % A_LoopFileFullPath, % destPath, 1
Loop, Files, % OSC_Unzip "\*.*", RD
	FileMoveDir, % A_LoopFileFullPath, % destPath, 1

FileGetSize, fileSize, % OSC_Unzip
If (fileSize = 0)
	FileRemoveDir, % OSC_Unzip
If (!FileExist(OSC_Unzip))
	FileDelete, % OSC_Zip_New

notificationSettings.title := vName
Notify().AddWindow("has been Installed", notificationSettings)

Run, % userOSCPath . "\open-stage-control.exe"
DetectHiddenWindows, Off

Sleep, 2500	; Give enough time for script to finish before exiting
ExitApp

checkForCurrentVersion(userOSCPath) {
	packageJSONPath := userOSCPath "\resources\app\package.json"
	if (!FileExist(packageJSONPath)) {
		currVersion := FileGetInfo(userOSCPath "\open-stage-control.exe").FileVersion
	}else {
		FileRead, packageJSON, % packageJSONPath
		packageJSON := JXON_Load(packageJSON)
		currVersion := packageJSON.version
	}
	return currVersion
}

getPreviousVersions(assets) {
	prevVersions := []
	for i,asset in assets
		prevVersions.push(asset.tag_name)

	return installPreviousVersion(prevVersions)
}

installPreviousVersion(prevVersions) {
	Instruction := "Select which to install:"
	Content := "in 30 seconds will default to the latest previous."
	Title := "Install Previous Version"
	MainIcon := 0xFFFD
	Flags := 0x818
	CustomButtons := []
	CustomButtons.Push([101, prevVersions.2])
	CustomButtons.Push([102, prevVersions.3])
	CustomButtons.Push([103, prevVersions.4])
	cButtons := CustomButtons.Length()
	VarSetCapacity(pButtons, 4 * cButtons + A_PtrSize * cButtons, 0)
	Loop %cButtons% {
		iButtonID := CustomButtons[A_Index][1]
		iButtonText := &(b%A_Index% := CustomButtons[A_Index][2])
		NumPut(iButtonID, pButtons, (4 + A_PtrSize) * (A_Index - 1), "Int")
		NumPut(iButtonText, pButtons, (4 + A_PtrSize) * A_Index - A_PtrSize, "Ptr")
	}
	DefaultButton := 101
	ipv_Callback := RegisterCallback("ipv_Callback", "Fast")
	CBData := {}
	CBData.Timeout := 30000 ; ms

	; TASKDIALOGCONFIG structure
	x64 := A_PtrSize == 8
	NumPut(VarSetCapacity(TDC, x64 ? 160 : 96, 0), TDC, 0, "UInt") ; cbSize
	NumPut(Flags, TDC, x64 ? 20 : 12, "Int") ; dwFlags
	NumPut(&Title, TDC, x64 ? 28 : 20, "Ptr") ; pszWindowTitle
	NumPut(MainIcon, TDC, x64 ? 36 : 24, "Ptr") ; pszMainIcon
	NumPut(&Instruction, TDC, x64 ? 44 : 28, "Ptr") ; pszMainInstruction
	NumPut(&Content, TDC, x64 ? 52 : 32, "Ptr") ; pszContent
	NumPut(cButtons, TDC, x64 ? 60 : 36, "UInt") ; cButtons
	NumPut(&pButtons, TDC, x64 ? 64 : 40, "Ptr") ; pButtons
	NumPut(DefaultButton, TDC, x64 ? 72 : 44, "Int") ; nDefaultButton
	NumPut(ipv_Callback, TDC, x64 ? 140 : 84, "Ptr") ; pfCallback
	NumPut(&CBData, TDC, x64 ? 148 : 88, "Ptr") ; lpCallbackData

	DllCall("Comctl32.dll\TaskDialogIndirect", "Ptr", &TDC
		, "Int*", Button := 0
		, "Int*", Radio := 0
	, "Int*", Checked := 0)

	DllCall("Kernel32.dll\GlobalFree", "Ptr", ipv_Callback)

	If (Button == 101 || Button == 2) { ; 1
		return 2
	} Else If (Button == 102) { ; 2
		return 3
	} Else If (Button == 103) { ; 3
		return 4
	}
}

ipv_Callback(hWnd, Notification, wParam, lParam, RefData) {
	Local CBData := Object(RefData)

	If (Notification == 4 && wParam > CBData.Timeout) {
		; TDM_CLICK_BUTTON := 0x466, IDCANCEL := 2
		PostMessage 0x466, 2, 0,, ahk_id %hWnd%
	}
}

BinArr_ToString(BinArr, Encoding := "UTF-8") {
	oADO := ComObjCreate("ADODB.Stream")

	oADO.Type := 1 ; adTypeBinary
	oADO.Mode := 3 ; adModeReadWrite
	oADO.Open
	oADO.Write(BinArr)

	oADO.Position := 0
	oADO.Type := 2 ; adTypeText
	oADO.Charset := Encoding
	return oADO.ReadText, oADO.Close
}

FileGetInfo(lptstrFilename) {
	List := "Comments InternalName ProductName CompanyName LegalCopyright ProductVersion"
	. " FileDescription LegalTrademarks PrivateBuild FileVersion OriginalFilename SpecialBuild"
	dwLen := DllCall("Version.dll\GetFileVersionInfoSize", "Str", lptstrFilename, "Ptr", 0)
	dwLen := VarSetCapacity( lpData, dwLen + A_PtrSize)
	DllCall("Version.dll\GetFileVersionInfo", "Str", lptstrFilename, "UInt", 0, "UInt", dwLen, "Ptr", &lpData)
	DllCall("Version.dll\VerQueryValue", "Ptr", &lpData, "Str", "\VarFileInfo\Translation", "PtrP", lplpBuffer, "PtrP", puLen )
	sLangCP := Format("{:04X}{:04X}", NumGet(lplpBuffer+0, "UShort"), NumGet(lplpBuffer+2, "UShort"))
	i := {}
	Loop, Parse, % List, %A_Space%
		DllCall("Version.dll\VerQueryValue", "Ptr", &lpData, "Str", "\StringFileInfo\" sLangCp "\" A_LoopField, "PtrP", lplpBuffer, "PtrP", puLen )
	? i[A_LoopField] := StrGet(lplpBuffer, puLen) : ""
	return i
}

updateSettingsIni() {
	global userOSCPath
	IniWrite, % userOSCPath, % A_ScriptDir "\settings.ini", settings, oscPath
}

checkUserOSCPath(byRef path) {
	if (!FileExist(A_ScriptDir "\settings.ini"))
		IniWrite, % "", % A_ScriptDir "\settings.ini", settings, oscPath

	IniRead, path, % A_ScriptDir "\settings.ini", settings, oscPath
	pathExists := FileExist(path) == "D" ? true : false
	if (!pathExists) {
		MsgBox, 48, Error, OSC path not found. Please set a valid directory for your OSC install.
		InputBox, path, Set OSC Directory, Set a valid OSC Directory for the install
		If (ErrorLevel)
			ExitApp
		if (!FileExist(path))
			FileCreateDir, % path
		If (ErrorLevel) {
			MsgBox, 48, Error, Could not create the path you listed, try to manually create the path and restart.
			ExitApp
		}
		IniWrite, % path, % A_ScriptDir "\settings.ini", settings, oscPath
		checkUserOSCPath(path)
	}
	OnExit("updateSettingsIni")
	return path
}

Unzip(zipFullPath, outputDir) {
	FileCreateDir, %outputDir%
	psh := ComObjCreate("Shell.Application")
	psh.Namespace( outputDir ).CopyHere( psh.Namespace( zipFullPath ).items, 4|16 )
}

UpToDate(version) {
	Instruction := "You're currently up to date!"
	Content := "Your current version is: " version
	Title := "Checking for Updates"
	MainIcon := 0xFFFD
	Flags := 0x810
	CustomButtons := []
	CustomButtons.Push([101, "Check for Beta?"])
	CustomButtons.Push([102, "Install Previous Version(s)?"])
	CustomButtons.Push([103, "Exit"])
	cButtons := CustomButtons.Length()
	VarSetCapacity(pButtons, 4 * cButtons + A_PtrSize * cButtons, 0)
	Loop %cButtons% {
		iButtonID := CustomButtons[A_Index][1]
		iButtonText := &(b%A_Index% := CustomButtons[A_Index][2])
		NumPut(iButtonID, pButtons, (4 + A_PtrSize) * (A_Index - 1), "Int")
		NumPut(iButtonText, pButtons, (4 + A_PtrSize) * A_Index - A_PtrSize, "Ptr")
	}
	DefaultButton := 103
	TDCallback_UpToDate := RegisterCallback("TDCallback_UpToDate", "Fast")
	CBData := {}
	CBData.Timeout := 30000 ; ms

	; TASKDIALOGCONFIG structure
	x64 := A_PtrSize == 8
	NumPut(VarSetCapacity(TDC, x64 ? 160 : 96, 0), TDC, 0, "UInt") ; cbSize
	NumPut(Flags, TDC, x64 ? 20 : 12, "Int") ; dwFlags
	NumPut(&Title, TDC, x64 ? 28 : 20, "Ptr") ; pszWindowTitle
	NumPut(MainIcon, TDC, x64 ? 36 : 24, "Ptr") ; pszMainIcon
	NumPut(&Instruction, TDC, x64 ? 44 : 28, "Ptr") ; pszMainInstruction
	NumPut(&Content, TDC, x64 ? 52 : 32, "Ptr") ; pszContent
	NumPut(cButtons, TDC, x64 ? 60 : 36, "UInt") ; cButtons
	NumPut(&pButtons, TDC, x64 ? 64 : 40, "Ptr") ; pButtons
	NumPut(DefaultButton, TDC, x64 ? 72 : 44, "Int") ; nDefaultButton
	NumPut(TDCallback_UpToDate, TDC, x64 ? 140 : 84, "Ptr") ; pfCallback
	NumPut(&CBData, TDC, x64 ? 148 : 88, "Ptr") ; lpCallbackData

	DllCall("Comctl32.dll\TaskDialogIndirect", "Ptr", &TDC
		, "Int*", Button := 0
		, "Int*", Radio := 0
	, "Int*", Checked := 0)

	DllCall("Kernel32.dll\GlobalFree", "Ptr", TDCallback_UpToDate)

	If (Button == 101) { ; Check for Beta?
		return "beta"
	} Else If (Button == 102) { ; Install Previous Version(s)?
		return "previous"
	} Else If (Button == 103) { ; Exit
		return false
	} Else If (Button == 2) { ; Timeout
		return false
	}
}

TDCallback_UpToDate(hWnd, Notification, wParam, lParam, RefData) {
	Local CBData := Object(RefData)

	If (Notification == 4 && wParam > CBData.Timeout) {
		; TDM_CLICK_BUTTON := 0x466, IDCANCEL := 2
		PostMessage 0x466, 2, 0,, ahk_id %hWnd%
	}
}

checkAdminStatus() {
	if (!A_IsAdmin){ ;http://ahkscript.org/docs/Variables.htm#IsAdmin
		Instruction := "Restarting App..."
		Content := "You must run this script as an administrator"
		Title := "NOT ADMIN"
		MainIcon := 0xFFFC
		Flags := 0xE00
		Buttons := 0x20
		TDCallback := RegisterCallback("TDCallback", "Fast")
		CBData := {}
		CBData.Marquee := True
		CBData.Timeout := 3000 ; ms

		; TASKDIALOGCONFIG structure
		x64 := A_PtrSize == 8
		NumPut(VarSetCapacity(TDC, x64 ? 160 : 96, 0), TDC, 0, "UInt") ; cbSize
		NumPut(Flags, TDC, x64 ? 20 : 12, "Int") ; dwFlags
		NumPut(Buttons, TDC, x64 ? 24 : 16, "Int") ; dwCommonButtons
		NumPut(&Title, TDC, x64 ? 28 : 20, "Ptr") ; pszWindowTitle
		NumPut(MainIcon, TDC, x64 ? 36 : 24, "Ptr") ; pszMainIcon
		NumPut(&Instruction, TDC, x64 ? 44 : 28, "Ptr") ; pszMainInstruction
		NumPut(&Content, TDC, x64 ? 52 : 32, "Ptr") ; pszContent
		NumPut(TDCallback, TDC, x64 ? 140 : 84, "Ptr") ; pfCallback
		NumPut(&CBData, TDC, x64 ? 148 : 88, "Ptr") ; lpCallbackData

		DllCall("Comctl32.dll\TaskDialogIndirect", "Ptr", &TDC
			, "Int*", Button := 0
			, "Int*", Radio := 0
		, "Int*", Checked := 0)

		DllCall("Kernel32.dll\GlobalFree", "Ptr", TDCallback)

		If (Button == 8) { ; Close
			Run *RunAs "%A_ScriptFullPath%" ; Requires v1.0.92.01+
			ExitApp

		} Else If (Button == 2) { ; Timeout
			Run *RunAs "%A_ScriptFullPath%",, UseErrorLevel ; Requires v1.0.92.01+
			if(ErrorLevel)
				ExitApp
			ExitApp
		}
	}
}

errorTryAgain(title, message) {
	OnMessage(0x44, "OnReqStatusError")
	MsgBox 0x11, ERROR, % "There was an error with the request. Please try again later. `nError: " status
	OnMessage(0x44, "")

	IfMsgBox OK, {
		Reload
	} Else IfMsgBox Cancel, {
		ExitApp
	}
}

OnReqStatusError() {
	DetectHiddenWindows, On
	Process, Exist
	If (WinExist("ahk_class #32770 ahk_pid " . ErrorLevel)) {
		ControlSetText Button1, Try Again
		ControlSetText Button2, Exit
	}
}

TDCallback(hWnd, Notification, wParam, lParam, RefData) {
	Local CBData := Object(RefData)

	If (Notification == 4 && wParam > CBData.Timeout) {
		; TDM_CLICK_BUTTON := 0x466, IDCANCEL := 2
		PostMessage 0x466, 2, 0,, ahk_id %hWnd%
	}

	If (Notification == 0 && CBData.Marquee) {
		; TDM_SET_PROGRESS_BAR_MARQUEE
		DllCall("PostMessage", "Ptr", hWnd, "UInt", 0x46B, "UInt", 1, "UInt", 50)
	}
}

OSC-Install-New-Version-With-AHK-main-v1.3.12.zip (26.8 KB)

If the release doesn't touch the midi engine nor the electron engine (both are rarely changed), you can download the *-node.zip version and replace the files in open-stage-control/resources/app/ with its content, that would save a lot of bandwidth compared to downloading the full package everytime.

1 Like

Thank you for the tip! I wasn't aware of that and that makes way more sense to do.

I updated the script, thank you Jean!

Cheers,
DMDComposer

You might want to check the release's changelog for keywords "midi" and "electron" in order to decide whether to perform a full update or not.

1 Like

That's a brilliant idea too. Thank you Jean, with your suggestions you're making me into a real developer with this script.

I'll work on updating that now, cheers!

1 Like

Updated to v1.1.7

  • I added a major/minor variable check, per your suggestion Jean. =)
  • Removed m() include/binary, and replaced with DLL msg boxes.
  • Cleaned up some bloat, prob still some there =\

Next on the list, is when the "up to date" check happens, to add a dialogue asking to check / install beta releases.

Cheers,
DMDComposer

Updated to v1.2.10

  • Added check for beta button if up to date with current stable release
  • bug fixes and prettier task dialogues / cosmetics

Updated to v1.3.12

  • Added - when up to date, now asks if you want to check for Beta or the new option Install Previous versions, clicking Install Previous versions will show the previous 3 versions to choose from to install.
  • Bug fix: Checks current version first by the resources/app/package.json, rather than the open-stage-control.exe
    • If the install installed a "minor" version update, using the node package zip, the updater would always show a new update is available because it would be referencing the open-stage-control.exe which would still be on the previous update, since this doesn't get replaced with updating "minor" - node package zip.
  • Small bug fixes