Backup and Restore Family Historian Settings via Windows.fh_lua--[[
@Title: Backup and Restore Family Historian Settings via Windows
@Type: Standard
@Author: Mark Draper
@Version: 2.1.4
@LastUpdated: 18 Aug 2025
@Licence: This plugin is copyright (c) 2025 Mark Draper and is licensed under the MIT License which
is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Backup and restore Family Historian custom settings. This plugin does all the copying
directly within Windows rather than in the plugin. This gives much faster and more convenient
operation, as well as providing the option to implement more advanced features such as
single-click rapid backup or automated backup.
]]
--[[
Version 1.0 (May 2022)
Initial Store version
Version 1.1 (May 2022)
Emulator-compatible version
Xcopy for emulator copy
Clearer label for restore option
Fixed bug with WOW64Node in Advanced mode
Version 1.1.1 (May 2022)
Xcopy options tweaked
Does not overwrite its own settings or temporary batch files
Version 1.1.2 (May 2022)
ASCII check modified to permit FH5
Version 1.2 (Jan 2023)
Checks only one instance running before restoring to prevent potential data loss
Various minor code tidies and rationalisations
Version 1.2.1 (Feb 2023)
Corrects instance count when running under FH6 (Fh vs fh!)
Version 1.3 (Sep 2024)
Corrects restore operation in emulator and new emulator check
Now fully Unicode compatible (FSO rather than lfs)
Moves temporary script files to tmp folder and improved ini file format
Now edits Registry file in FH6 as well so fully functional
Improved user dialogs and menu button labels
Various code optimisations and minor tweaks
Version 2.0 (Dec 2024)
Simplified restore without overwrite option (always restores in full)
Simplified main menu reflecting this change
Enhanced menu pop-up prompts
Supports multi-monitor extended desktops
Checks existing backups for completeness before updating or restoring
Various corrections and enhancements to Unicode support
Backup script modified to call 64-bit Registry key specifically if running in Advanced mode
(avoids potential issues with missing backup file when called directly from Windows)
Version 2.1 (May 2025)
Modified 'three test' emulator detection
CPU architecture detection
ARM64 warning
Version 2.1.1 (Jul 2025)
Corrected ARM64 warning
Version 2.1.4 (Aug 2025)
Generalised ARM64 warning
Minor tidy-up of old code
]]
fhInitialise(5,0,0,'save_required')
require 'luacom'
require('iuplua')
if fhGetAppVersion() > 6 then
fh = require('fhUtils')
fh.setIupDefaults()
else
iup.SetGlobal('CUSTOMQUITMESSAGE','YES')
iup.SetGlobal('UTF8MODE','YES')
end
iup.SetGlobal('UTF8MODE_FILE','YES')
FSO = luacom.CreateObject('Scripting.FileSystemObject')
-- *********************************************************************
function main()
local MainForm
if not GetOptions() then return end -- get plugin options
-- set main window static controls and options
local Heading = 'This plugin backs up and restores Family Historian settings using Windows commands.\n' ..
'All languages and Windows localisations are supported automatically.\n\n'
if not gblOptions.Emulator then
Heading = Heading .. 'Technically-minded users may save the backup configuration as Windows batch ' ..
'files to\nincorporate into their own backup process (including fully automated backups).'
else
Heading = Heading .. 'Advanced mode is not applicable when running in an emulator.'
end
local lblTitle = iup.label{title = Heading, alignment = 'ALEFT', padding = '10x20'}
-- folder controls and options
local lblF = iup.label{title = gblOptions.Path or '',expand = 'HORIZONTAL'}
local btnF = iup.button{title = 'Select', padding = '10x3',
tip = 'Destination folder where scripts and backup data are written.',
action = function(self) SelectFolder(MainForm) UpdateMenu() end}
local chkB = iup.toggle{title = 'Write to dated subfolder', expand = 'YES', active = 'NO',
tip = 'Write backup to dated subfolder.'}
local fraF = iup.frame{iup.vbox{lblF, chkB, btnF; alignment = 'ACENTER'}; title = 'Backup folder'}
-- general controls and options (use similar containers for all frames for consistent appearance)
local chkM1 = iup.toggle{title = 'Normal', expand = 'HORIZONTAL',
tip = 'Run backup or restore immediately.'}
local chkM2 = iup.toggle{title = 'Advanced', expand = 'HORIZONTAL',
tip = 'Save the script files without running the backup or restore.'}
if gblOptions.Emulator then chkM2.Active = 'NO' end
local hboxM = iup.hbox{chkM1, chkM2}
local radM = iup.radio{hboxM}
local fraM = iup.frame{iup.hbox{radM}, title = 'Operating mode'}
-- button display
local btnB = iup.button{active = 'NO',
action = function(self) Backup() MainForm.BringFront = 'YES' end}
local btnR = iup.button{title = 'Restore Script', padding = '10x3',
action = function(self)
local i = Restore(MainForm)
MainForm.BringFront = 'YES'
if i == -1 then return iup.CLOSE end
end}
local btnH = iup.button{title = 'Help', tip = 'Display plugin help page.\n' ..
'(hold down either Shift key to display\nSystem Information)',
action = function(self) GetHelp(MainForm) end}
local btnX = iup.button{title = 'Close', tip = 'Close plugin.',
action = function(self) return iup.CLOSE end}
local hButtons = iup.hbox{btnB, btnR, btnH, btnX; cgap = 20, cmargin = 'x15', normalizesize = 'BOTH'}
-- enhanced tool tips
btnB.TipBalloonTitleIcon = '1'
btnF.TipBalloonTitle = 'Select Folder'
btnF.TipBalloonTitleIcon = '1'
chkB.TipBalloonTitle = 'Dated Subfolder'
chkB.TipBalloonTitleIcon = '1'
chkM1.TipBalloonTitle = 'Normal Mode'
chkM1.TipBalloonTitleIcon = '2'
chkM2.TipBalloonTitle = 'Advanced Mode'
chkM2.TipBalloonTitleIcon = '1'
btnH.TipBalloonTitle = 'Plugin Help'
btnH.TipBalloonTitleIcon = '1'
btnX.TipBalloonTitle = 'Close Plugin'
btnX.TipBalloonTitleIcon = '3'
-- menu update function
function UpdateMenu()
if gblOptions.Path then
lblF.Title = gblOptions.Path
btnB.Active = 'YES'
chkB.Active = 'YES'
end
if gblOptions.Dated then
chkB.Value = 'YES'
else
chkB.Value = 'NO'
end
if gblOptions.Advanced then
chkM2.Value = 'ON'
btnB.Title = 'Backup Script'
btnR.Title = 'Restore Script'
btnB.Tip = 'Save backup batch file to defined backup folder.'
btnB.TipBalloonTitle = 'Save Backup Script'
btnR.Tip = 'Save restore batch file to defined backup folder.'
btnR.TipBalloonTitle = 'Save Restore Script'
btnR.TipBalloonTitleIcon = '1'
else
chkM1.Value = 'ON'
btnB.Title = 'Backup'
btnB.Size = btnR.Size
btnB.Tip = 'Run backup now using defined options.'
btnB.TipBalloonTitle = 'Run Backup'
btnR.Title = 'Restore'
btnR.Size = btnB.Size
btnR.Tip = 'Run restore now using defined options.'
btnR.TipBalloonTitle = 'Run Restore'
btnR.TipBalloonTitleIcon = '2'
end
end
-- callback actions
function chkM2:valuechanged_cb()
gblOptions.Advanced = (chkM2.Value == 'ON')
UpdateMenu()
end
function chkB:valuechanged_cb()
gblOptions.Dated = (chkB.Value == 'ON')
end
-- assemble form
local vForm = iup.vbox{lblTitle, fraM, fraF, hButtons;
alignment = 'ACENTER', cgap = 5, cmargin = '10x3'}
MainForm = iup.dialog{vForm; minbox='NO', maxbox='NO',
title='Backup and Restore Family Historian Settings via Windows (2.1.4)'}
MainForm:map() -- determine form layout prior to assigning button labels and resizing
if not gblOptions.Emulator then MainForm.tipballoon = 'YES' end
UpdateMenu()
if fhGetAppVersion() < 7 then
if IsExtendedDesktop() then
local msg = 'Versions of Family Historian prior to 7 cannot determine which monitor ' ..
'the application is running in, so the main plugin menu will always open in the ' ..
'primary monitor.'
MessageBox(msg, 'OK', 'WARNING')
end
MainForm:popup(IUP_CENTER, IUP_CENTER)
else
iup.SetAttribute(MainForm, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
iup.ShowXY(MainForm, IUP_CENTERPARENT, IUP_CENTERPARENT)
local position = MainForm.ScreenPosition
local FHx, FHy = position:match('([^,]*),([^,]*)')
FHx, FHy = tonumber(FHx), tonumber(FHy)
local size = MainForm.NaturalSize
local x, y, w, h
w, h = size:match('^(%d+)x(%d+)$')
local xMid = FHx + tonumber(w) / 2
local yMid = FHy + tonumber(h) / 2
local mi = iup.GetGlobal('MONITORSINFO')
for m in mi:gmatch('[^\r\n]+') do
x, y, w, h = m:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$')
x, y, w, h = tonumber(x), tonumber(y), tonumber(w), tonumber(h)
if xMid >= x and xMid <= x + w and yMid >= y and yMid <= y + h then -- this monitor
break
end
end
if FHx >= x and FHy >= y then -- visible
MainForm:popup(FHx, FHy)
else
MainForm:popup(x, y) -- corner
end
end
SaveOptions()
end
-- *********************************************************************
function SelectFolder(MainForm)
repeat
local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path,
title = 'Select folder for scripts and backup files', parentdialog = MainForm}
filedlg:popup()
if filedlg.Status == '-1' then return end
local path = filedlg.Value
if IsASCII(path) then
gblOptions.Path = path
gblOptions.ShortPath = path
return
elseif gblOptions.Emulator then -- check emulator first as it does not support short paths
local msg = 'Due to limitations in Windows batch scripts, the backup location ' ..
'must comprise only standard English letters, numbers, and ' ..
'valid punctuation characters.\n\nPlease select an alterative location.'
if MessageBox(msg, 'OKCANCEL', 'ERROR', 'Folder Selection Error') == 2 then return end
else
if IsASCII(FSO:GetFolder(path).ShortPath) then
gblOptions.Path = path
gblOptions.ShortPath = FSO:GetFolder(path).ShortPath
return
else
local msg = 'Due to limitations in Windows batch scripts, a backup location on drives ' ..
'other than C:\\ must comprise only standard English letters, numbers, and ' ..
'valid punctuation characters.\n\nPlease select an alterative location.'
if MessageBox(msg, 'OKCANCEL', 'ERROR', 'Folder Selection Error') == 2 then return end
end
end
until false
end
-- *********************************************************************
function Backup()
-- check for existing backup
local BackupPath = gblOptions.ShortPath
if gblOptions.Dated then
BackupPath = BackupPath .. '\\fhBackup_' .. os.date('%Y-%m-%d')
end
-- check all files and folders present if existing backup
local i = 0
if FSO:FolderExists(BackupPath .. '\\fhProgramData') then i = i + 1 end
if FSO:FolderExists(BackupPath .. '\\fhAppData') then i = i + 1 end
if FSO:FileExists(BackupPath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
if FSO:FileExists(BackupPath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end
if not gblOptions.Advanced then
if i > 0 and i < 4 then
local msg = 'Existing backup in ' .. BackupPath .. ' is incomplete.\n\nIgnore error and update?'
if MessageBox(msg, 'OKCANCEL', 'WARNING', nil, 2) ~= 1 then return end
elseif i == 4 then
local stamp = FSO:GetFile(BackupPath .. '\\fhCURRENT_USER.reg').DateLastModified
local msg = os.date('Update existing backup in ' .. BackupPath ..
'?\n\nLast update: ' .. stamp)
if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Update', 2) ~= 1 then return end
end
end
local tblScript, BackupFile, ShortFile = GetBackupScript(BackupPath)
-- write script to file
local F = io.open(ShortFile or BackupFile, 'w')
if not F then
MessageBox('Cannot write backup script.', 'OK', 'ERROR', 'File Access Error')
return
end
table.insert(tblScript, '')
F:write(table.concat(tblScript, '\n'))
F:close()
-- confirm successful write
if gblOptions.Advanced then
local msg = 'Backup script written to file ' .. BackupFile .. '.'
MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
return
end
-- sync folders if running in an emulator
if gblOptions.Emulator and FSO:FolderExists(BackupPath .. '\\fhProgramData') then
SyncFolders(gblOptions.ProgramData, BackupPath .. '\\fhProgramData')
end
-- implement backup
if IsExtendedDesktop() then
MessageBox('The backup will now run in the primary monitor.', 'OK', 'INFORMATION')
end
if not fhShellExecute(ShortFile or BackupFile) then
MessageBox('Backup failed to run.', 'OK', 'ERROR', 'Script Error')
end
end
-- *********************************************************************
function GetBackupScript(BackupPath)
-- prepare backup script
local AppName = fhGetContextInfo('CI_APP_DATA_FOLDER'):match('[%w%s]+$')
local UserKey = '"HKEY_CURRENT_USER\\SOFTWARE\\Calico Pie\\' .. AppName .. '"'
local GlobalKey = '"HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Calico Pie\\' ..
AppName .. '\\2.0\\Preferences"'
if gblOptions.Emulator then -- asssumes 32-bit emulator
GlobalKey = '"HKEY_LOCAL_MACHINE\\SOFTWARE\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences"'
end
local tblT = {} -- build the script in this table
table.insert(tblT, '@echo off')
table.insert(tblT, 'echo Backup started...')
-- set path according to whether dated subfolder or not
if not gblOptions.Dated then
table.insert(tblT, 'set p=' .. gblOptions.ShortPath)
elseif gblOptions.Emulator then -- WINE does not support PowerShell Get-Date
table.insert(tblT, 'set p=' .. gblOptions.Path .. '\\fhBackup_' .. os.date('%Y-%m-%d'))
else
table.insert(tblT, 'set basepath=' .. gblOptions.ShortPath)
local cmd = 'for /f %%D in ( \'PowerShell Get-Date -Format "_yyyy-MM-dd"\' ) do set suffix=%%D'
table.insert(tblT, cmd)
table.insert(tblT, 'set p=%basepath%\\fhBackup%suffix%')
end
-- define commands to be used for copying ProgramData folders
if gblOptions.Emulator then
local colFolders = luacom.GetEnumerator(FSO:GetFolder(gblOptions.ProgramData).SubFolders)
local objFolder = colFolders:Next()
while objFolder do
if objFolder.Name ~= 'Map' then
table.insert(tblT, 'xcopy "' .. gblOptions.ProgramData .. '\\' .. objFolder.Name .. '\\" ' ..
'"%p%\\fhProgramData\\' .. objFolder.Name .. '\\" /d /e /y')
end
objFolder = colFolders:Next()
end
else
table.insert(tblT, 'robocopy "' .. gblOptions.ProgramData ..
'" "%p%\\fhProgramData" /MIR /XD Map /NJH')
end
-- define AppData command
if gblOptions.Emulator then
table.insert(tblT, 'xcopy "' .. gblOptions.AppDataPath .. '\\" "%p%\\fhAppData\\" /d /e /y')
else
table.insert(tblT, 'robocopy "' .. gblOptions.AppDataPath .. '" "%p%\\fhAppData" /MIR /NJH')
end
-- define Registry commands
table.insert(tblT, 'reg export ' .. UserKey .. ' "%p%\\fhCURRENT_USER.reg" /y')
table.insert(tblT, 'reg export ' .. GlobalKey .. ' "%p%\\fhLOCAL_MACHINE.reg" /y')
-- determine name of file used to save command script and self-delete command if immediate backup
local File, ShortFile
if gblOptions.Advanced then
ShortFile = gblOptions.ShortPath .. '\\fhBackup.bat'
File = gblOptions.Path .. '\\fhBackup.bat'
else
File = gblOptions.Temp .. '\\~fhBackup.bat'
table.insert(tblT, 'pause')
table.insert(tblT, 'del "' .. File .. '"')
end
return tblT, File, ShortFile
end
-- *********************************************************************
function Restore(MainForm)
-- check for Advanced mode
local RestoreFolder, ShortFolder, RestoreFile, ShortFile, legacy
if gblOptions.Advanced then
RestoreFolder = gblOptions.Path
RestoreFile = gblOptions.Path .. '\\fhRestore.bat'
ShortFolder = gblOptions.ShortPath
ShortFile = gblOptions.ShortPath .. '\\fhRestore.bat'
else
RestoreFolder, ShortFolder, legacy = SelectRestoreFolder(MainForm)
if not RestoreFolder then return end
RestoreFile = gblOptions.Temp .. '\\~fhRestore.bat'
ShortFile = gblOptions.Temp .. '\\~fhRestore.bat'
end
local tblScript = GetRestoreScript(RestoreFolder, ShortFolder, legacy)
-- write script to file (path cannot be Unicode, so simple method ok)
local F = io.open(ShortFile, 'w')
if not F then
MessageBox('Cannot write restore script.', 'OK', 'ERROR', 'File Access Error')
return
end
table.insert(tblScript, '')
F:write(table.concat(tblScript, '\n'))
F:close()
-- confirm successful write
if gblOptions.Advanced then
local msg = 'Restore script written to file ' .. RestoreFile .. '.'
MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
return
end
-- remove default files and folders from Registry file before restoring (if not old format)
if not legacy then RemoveDefaults(RestoreFolder) end
-- compare folders if running in an emulator
if gblOptions.Emulator then
SyncFolders(RestoreFolder .. '\\fhProgramData\\', gblOptions.ProgramData .. '\\')
end
-- warn the user what is about to happen and request approval
if not gblOptions.Emulator then
local msg = 'Family Historian will now be closed in order to complete the restore operation.\n\n' ..
'Are you sure that you want to do this?'
if MessageBox(msg, 'YESNO', 'WARNING', 'Confirm Automatic Restore - CAUTION', 2) ~= 1 then return end
else
local msg = 'When the "Press any key to continue..." message appears in the command window, ' ..
'please close Family Historian manually.\n\n'..
'Once it is fully closed, select the command window and press any key to complete ' ..
'the restore.'
if MessageBox(msg, 'OKCANCEL', 'WARNING', 'Confirm Restore - CAUTION', 2) ~= 1 then return end
end
-- generate auto-close script
local CloseFile = gblOptions.Temp .. '\\' .. '~fhAutoClose.bat'
local tblT = {}
table.insert(tblT, '@echo off')
if not gblOptions.Emulator then
table.insert(tblT, 'echo Restore started...')
table.insert(tblT, 'powershell start-sleep -s 1')
table.insert(tblT, 'cls')
table.insert(tblT, 'taskkill /IM fh.exe /f')
table.insert(tblT, 'powershell start-sleep -s 1')
else
table.insert(tblT, 'echo Please close Family Historian before proceeding.')
table.insert(tblT, 'pause')
end
table.insert(tblT, 'call "' .. RestoreFile .. '"')
table.insert(tblT, 'del "' .. RestoreFile .. '"')
table.insert(tblT, 'del "' .. CloseFile .. '"')
table.insert(tblT, '')
local F = io.open(CloseFile, 'w')
if not F then
MessageBox('Cannot write auto-close script.', 'OK', 'ERROR', 'File Access Error')
return
end
F:write(table.concat(tblT, '\n'))
F:close()
-- check that only one instance of FH is running (not valid in emulator)
if not gblOptions.Emulator and CountInstances() > 1 then
local msg = 'There may be more than one instance of Family Historian open.\n\n' ..
'Proceeding with the restore operation could cause data loss. Please close ' ..
'any additional copies and re-run the plugin.'
MessageBox(msg, 'OK', 'ERROR', 'Multiple Instances Detected')
os.remove(RestoreFile)
os.remove(CloseFile)
return
end
-- execute the kamikaze close and bale out...
fhShellExecute(CloseFile)
return -1 -- trigger value to close plugin (DOES NOT HAPPEN - CHECK)
end
-- *********************************************************************
function SelectRestoreFolder(MainForm)
-- select backup to restore
local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path, parentdialog = MainForm,
title = 'Select folder that contains the backup you wish to restore'}
filedlg:popup()
if filedlg.Status == '-1' then return end
local RestorePath, ShortPath = filedlg.Value, filedlg.Value
if not gblOptions.Emulator then ShortPath = FSO:GetFolder(RestorePath).ShortPath end
-- check folder contains an identifiable backup
local BackupVersion, B1, B2, B3, legacy
-- is there a valid new format backup in this folder?
local KeyFile = RestorePath .. '\\Family Historian\\Version.data'
if not FSO:FileExists(KeyFile) then
local i = 0
if FSO:FolderExists(RestorePath .. '\\fhProgramData') then i = i + 1 end
if FSO:FolderExists(RestorePath .. '\\fhAppData') then i = i + 1 end
if FSO:FileExists(RestorePath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
if FSO:FileExists(RestorePath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end
if i == 4 then
KeyFile = RestorePath .. '\\fhProgramData\\Plugin Data\\' ..
'Backup and Restore Family Historian Settings via Windows.ini'
local ShortKey = KeyFile
if not gblOptions.Emulator then ShortKey = FSO:GetFile(KeyFile).ShortPath end
for Line in io.lines(ShortKey) do
local k, v = Line:match('^(%w+)=([%w%p%s]+)$')
if k == 'Version' then
BackupVersion = v
B1, B2, B3 = BackupVersion:match('(%d+).(%d+).(%d+)')
end
end
end
elseif not (B1 and B2 and B3) then -- new version not found
KeyFile = RestorePath .. '\\Family Historian\\Version.data'
local ShortKey = KeyFile
if not gblOptions.Emulator then ShortKey = FSO:GetFolder(KeyFile).ShortPath end
F = io.open(ShortKey)
if F then
BackupVersion = F:read('*a')
F:close()
B1, B2, B3 = BackupVersion:match('(%d+).(%d+).(%d+)')
legacy = true
end
end
if not (B1 and B2 and B3) then
local msg = 'Selected folder does not appear to contain a backup.'
MessageBox(msg, 'OK', 'ERROR', 'Folder Selection Error')
return
end
-- check backup version matches
local V1, V2, V3 = fhGetAppVersion()
B1, B2, B3 = tonumber(B1), tonumber(B2), tonumber(B3)
if V1 ~= B1 then
local msg = 'Cannot restore a Version ' .. B1 .. ' backup into Version ' .. V1 .. '.'
MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
return
elseif B2 > V2 or (B2 == V2 and B3 > V3) then
local msg = 'Cannot restore a Version ' .. BackupVersion .. ' backup into Version ' ..
gblOptions.Version .. '.\n\n' ..
'You should update your present copy of Family Historian before restoring the backup.'
MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
return
elseif B2 < V2 or (B2 == V2 and B3 < V3) then
local msg = 'Restoring a Version ' .. BackupVersion .. ' backup into Version ' ..
gblOptions.Version .. ' is potentially risky.\n\nAre you sure that you want to do this?'
if MessageBox(msg, 'YESNO', 'WARNING', 'Potential Version Conflict', 2) ~= 1 then return end
end
-- get backup creation date and time
local stamp = FSO:GetFile(RestorePath .. '\\fhCURRENT_USER.reg').DateLastModified
if legacy then
stamp = FSO:GetFile(RestorePath .. '\\Family Historian\\Version.data').DateLastModified
end
local msg = 'Backup in ' .. RestorePath .. ' was created ' .. stamp ..
'. Is this the correct backup?'
if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Backup Selection', 2) ~= 1 then return end
return RestorePath, ShortPath, legacy
end
-- *********************************************************************
function GetRestoreScript(RestoreFolder, ShortFolder, legacy)
local tblT = {} -- build the script in this table
table.insert(tblT, '@echo off')
if not gblOptions.Emulator then
local Switch = '/MIR /NJH'
if not legacy then
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhProgramData" "' ..
gblOptions.ProgramData .. '" ' .. Switch)
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhAppData" "' ..
gblOptions.AppDataPath .. '" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhCURRENT_USER.reg"')
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhLOCAL_MACHINE.reg"')
else
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\Family Historian\\Program Data" "' ..
gblOptions.ProgramData .. '" ' .. Switch)
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\Family Historian\\Application" "' ..
AppDataPath .. '" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\Family Historian\\Registry.keys"')
end
else
local Switch = '/e /y'
if not legacy then
table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\fhProgramData\\" "' ..
gblOptions.ProgramData .. '\\" ' .. Switch)
table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\fhAppData\\" "' ..
gblOptions.AppDataPath .. '" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhCURRENT_USER.reg"')
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhLOCAL_MACHINE.reg"')
else
table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\Family Historian\\Program Data\\" "' ..
gblOptions.ProgramData .. '\\" ' .. Switch)
table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\Family Historian\\Application\\" "' ..
AppDataPath .. '\\" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\Family Historian\\Registry.keys"')
end
end
if not gblOptions.Advanced then
table.insert(tblT, 'pause')
end
return tblT
end
-- *********************************************************************
function RemoveDefaults(RestoreFolder)
-- remove defaults that may not be valid in new installation
local FileName = RestoreFolder .. '\\fhLOCAL_MACHINE.reg'
if not FSO:FileExists(FileName) then
MessageBox('Restore folder is missing a required Registry file.', 'OK', 'ERROR')
return
end
local TextStream = FSO:OpenTextFile(FileName, 1, false, -1)
local Data = TextStream:ReadAll()
TextStream:Close()
local tblT = {}
Data = Data:gsub('\r\n\r\n', '\r\n \r\n') -- simple workaround to include blank lines
for Line in Data:gmatch('[^\r\n]+') do
if Line == ' ' then Line = '' end
local T1 = Line:match('^"Backup Directory"=')
local T2 = Line:match('^"Default File"=')
local T3 = Line:match('^"FH Root Folder"=')
local T4 = Line:match('^"Emulator%-Compat%-Mode"=')
if not (T1 or T2 or T3 or T4) then table.insert(tblT, Line) end
end
table.insert(tblT, '')
TextStream = FSO:OpenTextFile(FileName, 2, false, -1)
TextStream:Write(table.concat(tblT, '\n'))
TextStream:Close()
end
-- *********************************************************************
function SyncFolders(PrimaryFolder, ReplicaFolder)
local tblT = {}
-- set tblT to true for all files in replica folder
GetFiles(ReplicaFolder, tblT, true)
-- clear value for all corresponding files in primary folder
GetFiles(PrimaryFolder, tblT, nil)
-- delete all files in replica but not primary
for File, _ in pairs(tblT) do
FSO:DeleteFile(ReplicaFolder .. File)
end
end
-- *********************************************************************
function GetFiles(Folder, tblT, switch)
-- pass folder recursively as text name rather than folder object as latter
-- not supported in WINE
local function GetFolderContents(Folder, I)
local colFiles = luacom.GetEnumerator(FSO:GetFolder(Folder).Files)
local objFile = colFiles:Next()
while objFile do
tblT[objFile.Path:sub(I+1)] = switch
objFile = colFiles:Next()
end
local colSubFolders = luacom.GetEnumerator(FSO:GetFolder(Folder).SubFolders)
local objSubFolder = colSubFolders:Next()
while objSubFolder do
GetFolderContents(objSubFolder.Path, I)
objSubFolder = colSubFolders:Next()
end
end
local I = Folder:len()
GetFolderContents(Folder, I)
end
-- *********************************************************************
function GetOptions()
-- get system data
gblOptions = {}
local tblO = {}
local pc = iup.GetGlobal('COMPUTERNAME')
-- "3-test" version of emulator detection
local check1 = FSO:FolderExists('Z:\\bin') and FSO:FolderExists('Z:\\etc')
local check2 = not FSO:FolderExists('C:\\Windows\\SoftwareDistribution')
local check3 = os.getenv('WINEPREFIX')
if check1 and check2 and check3 then
gblOptions.Emulator = true
elseif not check1 and not check2 and not check3 then
gblOptions.Emulator = false
else
local msg = 'Emulator detection has given an ambiguous result.\n\n'
msg = msg .. 'Z:\\ method: ' .. tostring(check1) .. '\n'
msg = msg .. 'C:\\Windows\\SoftwareDistribution method: ' .. tostring(check2) .. '\n'
msg = msg .. 'WINEPREFIX method: ' .. tostring(check3) .. '\n\n'
msg = msg .. 'Please report on FHUG for investigation.'
MessageBox(msg, 'OK', 'ERROR')
return
end
-- get system level options
gblOptions.Temp = os.getenv('TEMP')
if not FSO:FolderExists(gblOptions.Temp) then
MessageBox('Cannot locate Temp folder.', 'OK', 'ERROR')
return
end
gblOptions.ProgramData = fhGetContextInfo('CI_APP_DATA_FOLDER')
gblOptions.AppData = os.getenv('APPDATA')
if not FSO:FolderExists(gblOptions.AppData) then -- Unicode user name
local path = fhGetPluginDataFileName('CURRENT_USER')
local i = path:find('\\Calico Pie\\Family Historian\\Plugin Data\\')
gblOptions.AppData = path:sub(1, i-1)
end
gblOptions.AppData = gblOptions.AppData .. '\\Calico Pie\\Family Historian'
if gblOptions.Emulator then
gblOptions.AppDataPath = gblOptions.AppData
else
gblOptions.AppDataPath = FSO:GetFolder(gblOptions.AppData).ShortPath
end
-- get saved options
local File = gblOptions.ProgramData .. '\\Plugin Data\\' ..
'Backup and Restore Family Historian Settings via Windows.ini'
if FSO:FileExists(File) then
for line in io.lines(File) do
local i = line:find('=')
if i then tblO[line:sub(1, i-1)] = line:sub(i+1) end
end
if tblO.Path and tblO.ShortPath and FSO:FolderExists(tblO.Path) then
gblOptions.Path = tblO.Path
gblOptions.ShortPath = tblO.ShortPath
end
if not gblOptions.Emulator and tblO.Advanced == '1' or tblO.Advanced == 'ON' then
gblOptions.Advanced = true
end
if tblO.Dated == '1' or tblO.Dated == 'ON' then gblOptions.Dated = true end
for k, _ in pairs(tblO) do -- ensure ARM settings are retained
if k:match('ARM_Msg_') then
gblOptions[k] = true
end
end
end
-- get processor architecture
local key = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\' ..
'PROCESSOR_ARCHITECTURE'
gblOptions.CPU = GetRegKey(key)
if gblOptions.CPU == 'ARM64' and not gblOptions['ARM_Msg_' .. pc] then
local msg = 'Your PC has a CPU with ARM infrastructure. ' ..
'Issues have been reported by Mac users running FH inside a VM when backing up to a ' ..
'location on the host Operating System. If this applies to you, you are recommended ' ..
'to select a backup location on either an external drive or network location.\n\n' ..
'See the plugin help file for more details of this topic.\n\n' ..
'Do you wish to hide this message on future runs?'
if MessageBox(msg, 'YESNO', 'WARNING') == 1 then
gblOptions['ARM_Msg_' .. pc] = true
end
elseif gblOptions.CPU ~= 'ARM64' and gblOptions['ARM_Msg_' .. pc] then
gblOptions['ARM_Msg_' .. pc] = false
end
-- set version option and save back to file to ensure saved on backup
local V1, V2, V3 = fhGetAppVersion()
gblOptions.Version = V1 .. '.' .. V2 .. '.' .. V3
SaveOptions()
return true
end
-- *********************************************************************
function SaveOptions()
-- save options to file
local OptionsFolder = gblOptions.ProgramData .. '\\Plugin Data'
if not FSO:FolderExists(OptionsFolder) then
FSO:CreateFolder(OptionsFolder)
end
local pc = iup.GetGlobal('COMPUTERNAME')
local OptionsFile = OptionsFolder .. '\\Backup and Restore Family Historian Settings via Windows.ini'
local F = io.open(OptionsFile, 'w')
if not F then
MessageBox('Cannot open options file.', 'OK', 'ERROR', 'File Access Error')
return
end
F:write('[Options]\n')
F:write('Version=' .. gblOptions.Version .. '\n')
if gblOptions.Path then F:write('Path=' .. gblOptions.Path .. '\n') end
if gblOptions.ShortPath then F:write('ShortPath=' .. gblOptions.ShortPath .. '\n') end
if gblOptions.Advanced then F:write('Advanced=1\n') end
if gblOptions.Dated then F:write('Dated=1\n') end
if gblOptions['ARM_Msg_' .. pc] then
F:write('ARM_Msg_' .. pc .. '=1\n')
end
for k, _ in pairs(gblOptions) do -- ensure ARM settings relating to other PCs are retained
if k:match('ARM_Msg_') and k ~= 'ARM_Msg_' .. pc then
F:write(k .. '=1\n')
end
end
F:close()
end
-- *********************************************************************
function GetHelp(MainForm)
-- hidden diagnostic window
if iup.GetGlobal('SHIFTKEY') == 'OFF' then
fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/' ..
'backup-and-restore-family-historian-settings-via-windows')
else
local CPU = GetRegKey('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\' ..
'PROCESSOR_ARCHITECTURE')
local tblT = {'Monitors Count = ' .. (iup.GetGlobal('MONITORSCOUNT') or 'n/a'),
'Monitors Info = ' .. (iup.GetGlobal('MONITORSINFO') or 'n/a'),
'Screen Size = ' .. (iup.GetGlobal('SCREENSIZE') or 'n/a'),
'Full Size = ' .. (iup.GetGlobal('FULLSIZE') or 'n/a'),
'Virtual Screen = ' .. (iup.GetGlobal('VIRTUALSCREEN') or 'n/a'),
'Menu Size = ' .. (MainForm.NaturalSize or 'n/a'),
'Menu Position = ' .. (MainForm.ScreenPosition or 'n/a'),
'',
'Processor Architecture = ' .. gblOptions.CPU or 'FALSE',
'Emulator = ' .. tostring(gblOptions.Emulator),
'',
'Family Historian Version = ' .. gblOptions.Version
--[[ options required only for detailed debugging
'System = ' .. (iup.GetGlobal('SYSTEM') or ''),
'System Version = ' .. (iup.GetGlobal('SYSTEMVERSION') or ''),
'System Language = ' .. (iup.GetGlobal('SYSTEMLANGUAGE') or ''),
'System Locale = ' .. (iup.GetGlobal('SYSTEMLOCALE') or ''),
'Computer Name = ' .. (iup.GetGlobal('COMPUTERNAME') or ''),
'',
'User Name (IUP) = ' .. (iup.GetGlobal('USERNAME') or ''),
'Temp Folder = ' .. gblOptions.Temp,
'ProgramData Folder = ' .. gblOptions.ProgramData:gsub(' ', ' '),
'AppData Folder = ' .. gblOptions.AppData:gsub(' ', ' '),
]]
}
MessageBox(table.concat(tblT, '\n'), 'OK', 'INFORMATION', 'System Information')
end
end
-- *********************************************************************
function IsExtendedDesktop()
-- determines if system has an extended desktop
local xd = iup.GetGlobal('VIRTUALSCREEN')
local x, y, width, height = xd:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$')
local fs = iup.GetGlobal('FULLSIZE')
local ExtendedDesktop = fs ~= width .. 'x' .. height
return ExtendedDesktop
end
-- *********************************************************************
function IsASCII(S)
-- checks whether the supplied string comprises just ASCII characters
for i = 1, S:len() do
if S:byte(i) < 32 or S:byte(i) > 127 then return false end
end
return true
end
-- *********************************************************************
function CountInstances()
-- checks just a single instance of FH is running
local F = io.popen('powershell get-process *fh*')
if not F then
local msg = 'Cannot determine number of Family Historian copies running.'
MessageBox(msg, 'OK', 'ERROR', 'File Handle Access Error')
return
end
local S = F:read('*a')
F:close()
local Instances = 0
for line in S:gmatch('[^\r\n]+') do
if line:lower():match('%sfh%s+$') then
Instances = Instances + 1
end
end
return Instances
end
-- *********************************************************************
function GetRegKey(key)
local sh = luacom.CreateObject 'WScript.Shell'
local ans
if pcall(function () ans = sh:RegRead(key) end) then
return ans
else
return nil,true
end
end
-- *********************************************************************
function MessageBox(Message, Buttons, Icon, Title, Default)
-- replaces built-in function with custom version containing more options
local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
title = Title or fhGetContextInfo('CI_PLUGIN_NAME'), buttondefault = Default,
parentdialog = MainForm}
msgdlg:popup()
return tonumber(msgdlg.ButtonResponse)
end
-- *********************************************************************
main()
--[[
@Title: Backup and Restore Family Historian Settings via Windows
@Type: Standard
@Author: Mark Draper
@Version: 2.1.4
@LastUpdated: 18 Aug 2025
@Licence: This plugin is copyright (c) 2025 Mark Draper and is licensed under the MIT License which
is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Backup and restore Family Historian custom settings. This plugin does all the copying
directly within Windows rather than in the plugin. This gives much faster and more convenient
operation, as well as providing the option to implement more advanced features such as
single-click rapid backup or automated backup.
]]
--[[
Version 1.0 (May 2022)
Initial Store version
Version 1.1 (May 2022)
Emulator-compatible version
Xcopy for emulator copy
Clearer label for restore option
Fixed bug with WOW64Node in Advanced mode
Version 1.1.1 (May 2022)
Xcopy options tweaked
Does not overwrite its own settings or temporary batch files
Version 1.1.2 (May 2022)
ASCII check modified to permit FH5
Version 1.2 (Jan 2023)
Checks only one instance running before restoring to prevent potential data loss
Various minor code tidies and rationalisations
Version 1.2.1 (Feb 2023)
Corrects instance count when running under FH6 (Fh vs fh!)
Version 1.3 (Sep 2024)
Corrects restore operation in emulator and new emulator check
Now fully Unicode compatible (FSO rather than lfs)
Moves temporary script files to tmp folder and improved ini file format
Now edits Registry file in FH6 as well so fully functional
Improved user dialogs and menu button labels
Various code optimisations and minor tweaks
Version 2.0 (Dec 2024)
Simplified restore without overwrite option (always restores in full)
Simplified main menu reflecting this change
Enhanced menu pop-up prompts
Supports multi-monitor extended desktops
Checks existing backups for completeness before updating or restoring
Various corrections and enhancements to Unicode support
Backup script modified to call 64-bit Registry key specifically if running in Advanced mode
(avoids potential issues with missing backup file when called directly from Windows)
Version 2.1 (May 2025)
Modified 'three test' emulator detection
CPU architecture detection
ARM64 warning
Version 2.1.1 (Jul 2025)
Corrected ARM64 warning
Version 2.1.4 (Aug 2025)
Generalised ARM64 warning
Minor tidy-up of old code
]]
fhInitialise(5,0,0,'save_required')
require 'luacom'
require('iuplua')
if fhGetAppVersion() > 6 then
fh = require('fhUtils')
fh.setIupDefaults()
else
iup.SetGlobal('CUSTOMQUITMESSAGE','YES')
iup.SetGlobal('UTF8MODE','YES')
end
iup.SetGlobal('UTF8MODE_FILE','YES')
FSO = luacom.CreateObject('Scripting.FileSystemObject')
-- *********************************************************************
function main()
local MainForm
if not GetOptions() then return end -- get plugin options
-- set main window static controls and options
local Heading = 'This plugin backs up and restores Family Historian settings using Windows commands.\n' ..
'All languages and Windows localisations are supported automatically.\n\n'
if not gblOptions.Emulator then
Heading = Heading .. 'Technically-minded users may save the backup configuration as Windows batch ' ..
'files to\nincorporate into their own backup process (including fully automated backups).'
else
Heading = Heading .. 'Advanced mode is not applicable when running in an emulator.'
end
local lblTitle = iup.label{title = Heading, alignment = 'ALEFT', padding = '10x20'}
-- folder controls and options
local lblF = iup.label{title = gblOptions.Path or '',expand = 'HORIZONTAL'}
local btnF = iup.button{title = 'Select', padding = '10x3',
tip = 'Destination folder where scripts and backup data are written.',
action = function(self) SelectFolder(MainForm) UpdateMenu() end}
local chkB = iup.toggle{title = 'Write to dated subfolder', expand = 'YES', active = 'NO',
tip = 'Write backup to dated subfolder.'}
local fraF = iup.frame{iup.vbox{lblF, chkB, btnF; alignment = 'ACENTER'}; title = 'Backup folder'}
-- general controls and options (use similar containers for all frames for consistent appearance)
local chkM1 = iup.toggle{title = 'Normal', expand = 'HORIZONTAL',
tip = 'Run backup or restore immediately.'}
local chkM2 = iup.toggle{title = 'Advanced', expand = 'HORIZONTAL',
tip = 'Save the script files without running the backup or restore.'}
if gblOptions.Emulator then chkM2.Active = 'NO' end
local hboxM = iup.hbox{chkM1, chkM2}
local radM = iup.radio{hboxM}
local fraM = iup.frame{iup.hbox{radM}, title = 'Operating mode'}
-- button display
local btnB = iup.button{active = 'NO',
action = function(self) Backup() MainForm.BringFront = 'YES' end}
local btnR = iup.button{title = 'Restore Script', padding = '10x3',
action = function(self)
local i = Restore(MainForm)
MainForm.BringFront = 'YES'
if i == -1 then return iup.CLOSE end
end}
local btnH = iup.button{title = 'Help', tip = 'Display plugin help page.\n' ..
'(hold down either Shift key to display\nSystem Information)',
action = function(self) GetHelp(MainForm) end}
local btnX = iup.button{title = 'Close', tip = 'Close plugin.',
action = function(self) return iup.CLOSE end}
local hButtons = iup.hbox{btnB, btnR, btnH, btnX; cgap = 20, cmargin = 'x15', normalizesize = 'BOTH'}
-- enhanced tool tips
btnB.TipBalloonTitleIcon = '1'
btnF.TipBalloonTitle = 'Select Folder'
btnF.TipBalloonTitleIcon = '1'
chkB.TipBalloonTitle = 'Dated Subfolder'
chkB.TipBalloonTitleIcon = '1'
chkM1.TipBalloonTitle = 'Normal Mode'
chkM1.TipBalloonTitleIcon = '2'
chkM2.TipBalloonTitle = 'Advanced Mode'
chkM2.TipBalloonTitleIcon = '1'
btnH.TipBalloonTitle = 'Plugin Help'
btnH.TipBalloonTitleIcon = '1'
btnX.TipBalloonTitle = 'Close Plugin'
btnX.TipBalloonTitleIcon = '3'
-- menu update function
function UpdateMenu()
if gblOptions.Path then
lblF.Title = gblOptions.Path
btnB.Active = 'YES'
chkB.Active = 'YES'
end
if gblOptions.Dated then
chkB.Value = 'YES'
else
chkB.Value = 'NO'
end
if gblOptions.Advanced then
chkM2.Value = 'ON'
btnB.Title = 'Backup Script'
btnR.Title = 'Restore Script'
btnB.Tip = 'Save backup batch file to defined backup folder.'
btnB.TipBalloonTitle = 'Save Backup Script'
btnR.Tip = 'Save restore batch file to defined backup folder.'
btnR.TipBalloonTitle = 'Save Restore Script'
btnR.TipBalloonTitleIcon = '1'
else
chkM1.Value = 'ON'
btnB.Title = 'Backup'
btnB.Size = btnR.Size
btnB.Tip = 'Run backup now using defined options.'
btnB.TipBalloonTitle = 'Run Backup'
btnR.Title = 'Restore'
btnR.Size = btnB.Size
btnR.Tip = 'Run restore now using defined options.'
btnR.TipBalloonTitle = 'Run Restore'
btnR.TipBalloonTitleIcon = '2'
end
end
-- callback actions
function chkM2:valuechanged_cb()
gblOptions.Advanced = (chkM2.Value == 'ON')
UpdateMenu()
end
function chkB:valuechanged_cb()
gblOptions.Dated = (chkB.Value == 'ON')
end
-- assemble form
local vForm = iup.vbox{lblTitle, fraM, fraF, hButtons;
alignment = 'ACENTER', cgap = 5, cmargin = '10x3'}
MainForm = iup.dialog{vForm; minbox='NO', maxbox='NO',
title='Backup and Restore Family Historian Settings via Windows (2.1.4)'}
MainForm:map() -- determine form layout prior to assigning button labels and resizing
if not gblOptions.Emulator then MainForm.tipballoon = 'YES' end
UpdateMenu()
if fhGetAppVersion() < 7 then
if IsExtendedDesktop() then
local msg = 'Versions of Family Historian prior to 7 cannot determine which monitor ' ..
'the application is running in, so the main plugin menu will always open in the ' ..
'primary monitor.'
MessageBox(msg, 'OK', 'WARNING')
end
MainForm:popup(IUP_CENTER, IUP_CENTER)
else
iup.SetAttribute(MainForm, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
iup.ShowXY(MainForm, IUP_CENTERPARENT, IUP_CENTERPARENT)
local position = MainForm.ScreenPosition
local FHx, FHy = position:match('([^,]*),([^,]*)')
FHx, FHy = tonumber(FHx), tonumber(FHy)
local size = MainForm.NaturalSize
local x, y, w, h
w, h = size:match('^(%d+)x(%d+)$')
local xMid = FHx + tonumber(w) / 2
local yMid = FHy + tonumber(h) / 2
local mi = iup.GetGlobal('MONITORSINFO')
for m in mi:gmatch('[^\r\n]+') do
x, y, w, h = m:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$')
x, y, w, h = tonumber(x), tonumber(y), tonumber(w), tonumber(h)
if xMid >= x and xMid <= x + w and yMid >= y and yMid <= y + h then -- this monitor
break
end
end
if FHx >= x and FHy >= y then -- visible
MainForm:popup(FHx, FHy)
else
MainForm:popup(x, y) -- corner
end
end
SaveOptions()
end
-- *********************************************************************
function SelectFolder(MainForm)
repeat
local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path,
title = 'Select folder for scripts and backup files', parentdialog = MainForm}
filedlg:popup()
if filedlg.Status == '-1' then return end
local path = filedlg.Value
if IsASCII(path) then
gblOptions.Path = path
gblOptions.ShortPath = path
return
elseif gblOptions.Emulator then -- check emulator first as it does not support short paths
local msg = 'Due to limitations in Windows batch scripts, the backup location ' ..
'must comprise only standard English letters, numbers, and ' ..
'valid punctuation characters.\n\nPlease select an alterative location.'
if MessageBox(msg, 'OKCANCEL', 'ERROR', 'Folder Selection Error') == 2 then return end
else
if IsASCII(FSO:GetFolder(path).ShortPath) then
gblOptions.Path = path
gblOptions.ShortPath = FSO:GetFolder(path).ShortPath
return
else
local msg = 'Due to limitations in Windows batch scripts, a backup location on drives ' ..
'other than C:\\ must comprise only standard English letters, numbers, and ' ..
'valid punctuation characters.\n\nPlease select an alterative location.'
if MessageBox(msg, 'OKCANCEL', 'ERROR', 'Folder Selection Error') == 2 then return end
end
end
until false
end
-- *********************************************************************
function Backup()
-- check for existing backup
local BackupPath = gblOptions.ShortPath
if gblOptions.Dated then
BackupPath = BackupPath .. '\\fhBackup_' .. os.date('%Y-%m-%d')
end
-- check all files and folders present if existing backup
local i = 0
if FSO:FolderExists(BackupPath .. '\\fhProgramData') then i = i + 1 end
if FSO:FolderExists(BackupPath .. '\\fhAppData') then i = i + 1 end
if FSO:FileExists(BackupPath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
if FSO:FileExists(BackupPath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end
if not gblOptions.Advanced then
if i > 0 and i < 4 then
local msg = 'Existing backup in ' .. BackupPath .. ' is incomplete.\n\nIgnore error and update?'
if MessageBox(msg, 'OKCANCEL', 'WARNING', nil, 2) ~= 1 then return end
elseif i == 4 then
local stamp = FSO:GetFile(BackupPath .. '\\fhCURRENT_USER.reg').DateLastModified
local msg = os.date('Update existing backup in ' .. BackupPath ..
'?\n\nLast update: ' .. stamp)
if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Update', 2) ~= 1 then return end
end
end
local tblScript, BackupFile, ShortFile = GetBackupScript(BackupPath)
-- write script to file
local F = io.open(ShortFile or BackupFile, 'w')
if not F then
MessageBox('Cannot write backup script.', 'OK', 'ERROR', 'File Access Error')
return
end
table.insert(tblScript, '')
F:write(table.concat(tblScript, '\n'))
F:close()
-- confirm successful write
if gblOptions.Advanced then
local msg = 'Backup script written to file ' .. BackupFile .. '.'
MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
return
end
-- sync folders if running in an emulator
if gblOptions.Emulator and FSO:FolderExists(BackupPath .. '\\fhProgramData') then
SyncFolders(gblOptions.ProgramData, BackupPath .. '\\fhProgramData')
end
-- implement backup
if IsExtendedDesktop() then
MessageBox('The backup will now run in the primary monitor.', 'OK', 'INFORMATION')
end
if not fhShellExecute(ShortFile or BackupFile) then
MessageBox('Backup failed to run.', 'OK', 'ERROR', 'Script Error')
end
end
-- *********************************************************************
function GetBackupScript(BackupPath)
-- prepare backup script
local AppName = fhGetContextInfo('CI_APP_DATA_FOLDER'):match('[%w%s]+$')
local UserKey = '"HKEY_CURRENT_USER\\SOFTWARE\\Calico Pie\\' .. AppName .. '"'
local GlobalKey = '"HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Calico Pie\\' ..
AppName .. '\\2.0\\Preferences"'
if gblOptions.Emulator then -- asssumes 32-bit emulator
GlobalKey = '"HKEY_LOCAL_MACHINE\\SOFTWARE\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences"'
end
local tblT = {} -- build the script in this table
table.insert(tblT, '@echo off')
table.insert(tblT, 'echo Backup started...')
-- set path according to whether dated subfolder or not
if not gblOptions.Dated then
table.insert(tblT, 'set p=' .. gblOptions.ShortPath)
elseif gblOptions.Emulator then -- WINE does not support PowerShell Get-Date
table.insert(tblT, 'set p=' .. gblOptions.Path .. '\\fhBackup_' .. os.date('%Y-%m-%d'))
else
table.insert(tblT, 'set basepath=' .. gblOptions.ShortPath)
local cmd = 'for /f %%D in ( \'PowerShell Get-Date -Format "_yyyy-MM-dd"\' ) do set suffix=%%D'
table.insert(tblT, cmd)
table.insert(tblT, 'set p=%basepath%\\fhBackup%suffix%')
end
-- define commands to be used for copying ProgramData folders
if gblOptions.Emulator then
local colFolders = luacom.GetEnumerator(FSO:GetFolder(gblOptions.ProgramData).SubFolders)
local objFolder = colFolders:Next()
while objFolder do
if objFolder.Name ~= 'Map' then
table.insert(tblT, 'xcopy "' .. gblOptions.ProgramData .. '\\' .. objFolder.Name .. '\\" ' ..
'"%p%\\fhProgramData\\' .. objFolder.Name .. '\\" /d /e /y')
end
objFolder = colFolders:Next()
end
else
table.insert(tblT, 'robocopy "' .. gblOptions.ProgramData ..
'" "%p%\\fhProgramData" /MIR /XD Map /NJH')
end
-- define AppData command
if gblOptions.Emulator then
table.insert(tblT, 'xcopy "' .. gblOptions.AppDataPath .. '\\" "%p%\\fhAppData\\" /d /e /y')
else
table.insert(tblT, 'robocopy "' .. gblOptions.AppDataPath .. '" "%p%\\fhAppData" /MIR /NJH')
end
-- define Registry commands
table.insert(tblT, 'reg export ' .. UserKey .. ' "%p%\\fhCURRENT_USER.reg" /y')
table.insert(tblT, 'reg export ' .. GlobalKey .. ' "%p%\\fhLOCAL_MACHINE.reg" /y')
-- determine name of file used to save command script and self-delete command if immediate backup
local File, ShortFile
if gblOptions.Advanced then
ShortFile = gblOptions.ShortPath .. '\\fhBackup.bat'
File = gblOptions.Path .. '\\fhBackup.bat'
else
File = gblOptions.Temp .. '\\~fhBackup.bat'
table.insert(tblT, 'pause')
table.insert(tblT, 'del "' .. File .. '"')
end
return tblT, File, ShortFile
end
-- *********************************************************************
function Restore(MainForm)
-- check for Advanced mode
local RestoreFolder, ShortFolder, RestoreFile, ShortFile, legacy
if gblOptions.Advanced then
RestoreFolder = gblOptions.Path
RestoreFile = gblOptions.Path .. '\\fhRestore.bat'
ShortFolder = gblOptions.ShortPath
ShortFile = gblOptions.ShortPath .. '\\fhRestore.bat'
else
RestoreFolder, ShortFolder, legacy = SelectRestoreFolder(MainForm)
if not RestoreFolder then return end
RestoreFile = gblOptions.Temp .. '\\~fhRestore.bat'
ShortFile = gblOptions.Temp .. '\\~fhRestore.bat'
end
local tblScript = GetRestoreScript(RestoreFolder, ShortFolder, legacy)
-- write script to file (path cannot be Unicode, so simple method ok)
local F = io.open(ShortFile, 'w')
if not F then
MessageBox('Cannot write restore script.', 'OK', 'ERROR', 'File Access Error')
return
end
table.insert(tblScript, '')
F:write(table.concat(tblScript, '\n'))
F:close()
-- confirm successful write
if gblOptions.Advanced then
local msg = 'Restore script written to file ' .. RestoreFile .. '.'
MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
return
end
-- remove default files and folders from Registry file before restoring (if not old format)
if not legacy then RemoveDefaults(RestoreFolder) end
-- compare folders if running in an emulator
if gblOptions.Emulator then
SyncFolders(RestoreFolder .. '\\fhProgramData\\', gblOptions.ProgramData .. '\\')
end
-- warn the user what is about to happen and request approval
if not gblOptions.Emulator then
local msg = 'Family Historian will now be closed in order to complete the restore operation.\n\n' ..
'Are you sure that you want to do this?'
if MessageBox(msg, 'YESNO', 'WARNING', 'Confirm Automatic Restore - CAUTION', 2) ~= 1 then return end
else
local msg = 'When the "Press any key to continue..." message appears in the command window, ' ..
'please close Family Historian manually.\n\n'..
'Once it is fully closed, select the command window and press any key to complete ' ..
'the restore.'
if MessageBox(msg, 'OKCANCEL', 'WARNING', 'Confirm Restore - CAUTION', 2) ~= 1 then return end
end
-- generate auto-close script
local CloseFile = gblOptions.Temp .. '\\' .. '~fhAutoClose.bat'
local tblT = {}
table.insert(tblT, '@echo off')
if not gblOptions.Emulator then
table.insert(tblT, 'echo Restore started...')
table.insert(tblT, 'powershell start-sleep -s 1')
table.insert(tblT, 'cls')
table.insert(tblT, 'taskkill /IM fh.exe /f')
table.insert(tblT, 'powershell start-sleep -s 1')
else
table.insert(tblT, 'echo Please close Family Historian before proceeding.')
table.insert(tblT, 'pause')
end
table.insert(tblT, 'call "' .. RestoreFile .. '"')
table.insert(tblT, 'del "' .. RestoreFile .. '"')
table.insert(tblT, 'del "' .. CloseFile .. '"')
table.insert(tblT, '')
local F = io.open(CloseFile, 'w')
if not F then
MessageBox('Cannot write auto-close script.', 'OK', 'ERROR', 'File Access Error')
return
end
F:write(table.concat(tblT, '\n'))
F:close()
-- check that only one instance of FH is running (not valid in emulator)
if not gblOptions.Emulator and CountInstances() > 1 then
local msg = 'There may be more than one instance of Family Historian open.\n\n' ..
'Proceeding with the restore operation could cause data loss. Please close ' ..
'any additional copies and re-run the plugin.'
MessageBox(msg, 'OK', 'ERROR', 'Multiple Instances Detected')
os.remove(RestoreFile)
os.remove(CloseFile)
return
end
-- execute the kamikaze close and bale out...
fhShellExecute(CloseFile)
return -1 -- trigger value to close plugin (DOES NOT HAPPEN - CHECK)
end
-- *********************************************************************
function SelectRestoreFolder(MainForm)
-- select backup to restore
local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path, parentdialog = MainForm,
title = 'Select folder that contains the backup you wish to restore'}
filedlg:popup()
if filedlg.Status == '-1' then return end
local RestorePath, ShortPath = filedlg.Value, filedlg.Value
if not gblOptions.Emulator then ShortPath = FSO:GetFolder(RestorePath).ShortPath end
-- check folder contains an identifiable backup
local BackupVersion, B1, B2, B3, legacy
-- is there a valid new format backup in this folder?
local KeyFile = RestorePath .. '\\Family Historian\\Version.data'
if not FSO:FileExists(KeyFile) then
local i = 0
if FSO:FolderExists(RestorePath .. '\\fhProgramData') then i = i + 1 end
if FSO:FolderExists(RestorePath .. '\\fhAppData') then i = i + 1 end
if FSO:FileExists(RestorePath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
if FSO:FileExists(RestorePath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end
if i == 4 then
KeyFile = RestorePath .. '\\fhProgramData\\Plugin Data\\' ..
'Backup and Restore Family Historian Settings via Windows.ini'
local ShortKey = KeyFile
if not gblOptions.Emulator then ShortKey = FSO:GetFile(KeyFile).ShortPath end
for Line in io.lines(ShortKey) do
local k, v = Line:match('^(%w+)=([%w%p%s]+)$')
if k == 'Version' then
BackupVersion = v
B1, B2, B3 = BackupVersion:match('(%d+).(%d+).(%d+)')
end
end
end
elseif not (B1 and B2 and B3) then -- new version not found
KeyFile = RestorePath .. '\\Family Historian\\Version.data'
local ShortKey = KeyFile
if not gblOptions.Emulator then ShortKey = FSO:GetFolder(KeyFile).ShortPath end
F = io.open(ShortKey)
if F then
BackupVersion = F:read('*a')
F:close()
B1, B2, B3 = BackupVersion:match('(%d+).(%d+).(%d+)')
legacy = true
end
end
if not (B1 and B2 and B3) then
local msg = 'Selected folder does not appear to contain a backup.'
MessageBox(msg, 'OK', 'ERROR', 'Folder Selection Error')
return
end
-- check backup version matches
local V1, V2, V3 = fhGetAppVersion()
B1, B2, B3 = tonumber(B1), tonumber(B2), tonumber(B3)
if V1 ~= B1 then
local msg = 'Cannot restore a Version ' .. B1 .. ' backup into Version ' .. V1 .. '.'
MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
return
elseif B2 > V2 or (B2 == V2 and B3 > V3) then
local msg = 'Cannot restore a Version ' .. BackupVersion .. ' backup into Version ' ..
gblOptions.Version .. '.\n\n' ..
'You should update your present copy of Family Historian before restoring the backup.'
MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
return
elseif B2 < V2 or (B2 == V2 and B3 < V3) then
local msg = 'Restoring a Version ' .. BackupVersion .. ' backup into Version ' ..
gblOptions.Version .. ' is potentially risky.\n\nAre you sure that you want to do this?'
if MessageBox(msg, 'YESNO', 'WARNING', 'Potential Version Conflict', 2) ~= 1 then return end
end
-- get backup creation date and time
local stamp = FSO:GetFile(RestorePath .. '\\fhCURRENT_USER.reg').DateLastModified
if legacy then
stamp = FSO:GetFile(RestorePath .. '\\Family Historian\\Version.data').DateLastModified
end
local msg = 'Backup in ' .. RestorePath .. ' was created ' .. stamp ..
'. Is this the correct backup?'
if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Backup Selection', 2) ~= 1 then return end
return RestorePath, ShortPath, legacy
end
-- *********************************************************************
function GetRestoreScript(RestoreFolder, ShortFolder, legacy)
local tblT = {} -- build the script in this table
table.insert(tblT, '@echo off')
if not gblOptions.Emulator then
local Switch = '/MIR /NJH'
if not legacy then
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhProgramData" "' ..
gblOptions.ProgramData .. '" ' .. Switch)
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhAppData" "' ..
gblOptions.AppDataPath .. '" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhCURRENT_USER.reg"')
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhLOCAL_MACHINE.reg"')
else
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\Family Historian\\Program Data" "' ..
gblOptions.ProgramData .. '" ' .. Switch)
table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\Family Historian\\Application" "' ..
AppDataPath .. '" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\Family Historian\\Registry.keys"')
end
else
local Switch = '/e /y'
if not legacy then
table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\fhProgramData\\" "' ..
gblOptions.ProgramData .. '\\" ' .. Switch)
table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\fhAppData\\" "' ..
gblOptions.AppDataPath .. '" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhCURRENT_USER.reg"')
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhLOCAL_MACHINE.reg"')
else
table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\Family Historian\\Program Data\\" "' ..
gblOptions.ProgramData .. '\\" ' .. Switch)
table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\Family Historian\\Application\\" "' ..
AppDataPath .. '\\" ' .. Switch)
table.insert(tblT, 'reg import "' .. ShortFolder .. '\\Family Historian\\Registry.keys"')
end
end
if not gblOptions.Advanced then
table.insert(tblT, 'pause')
end
return tblT
end
-- *********************************************************************
function RemoveDefaults(RestoreFolder)
-- remove defaults that may not be valid in new installation
local FileName = RestoreFolder .. '\\fhLOCAL_MACHINE.reg'
if not FSO:FileExists(FileName) then
MessageBox('Restore folder is missing a required Registry file.', 'OK', 'ERROR')
return
end
local TextStream = FSO:OpenTextFile(FileName, 1, false, -1)
local Data = TextStream:ReadAll()
TextStream:Close()
local tblT = {}
Data = Data:gsub('\r\n\r\n', '\r\n \r\n') -- simple workaround to include blank lines
for Line in Data:gmatch('[^\r\n]+') do
if Line == ' ' then Line = '' end
local T1 = Line:match('^"Backup Directory"=')
local T2 = Line:match('^"Default File"=')
local T3 = Line:match('^"FH Root Folder"=')
local T4 = Line:match('^"Emulator%-Compat%-Mode"=')
if not (T1 or T2 or T3 or T4) then table.insert(tblT, Line) end
end
table.insert(tblT, '')
TextStream = FSO:OpenTextFile(FileName, 2, false, -1)
TextStream:Write(table.concat(tblT, '\n'))
TextStream:Close()
end
-- *********************************************************************
function SyncFolders(PrimaryFolder, ReplicaFolder)
local tblT = {}
-- set tblT to true for all files in replica folder
GetFiles(ReplicaFolder, tblT, true)
-- clear value for all corresponding files in primary folder
GetFiles(PrimaryFolder, tblT, nil)
-- delete all files in replica but not primary
for File, _ in pairs(tblT) do
FSO:DeleteFile(ReplicaFolder .. File)
end
end
-- *********************************************************************
function GetFiles(Folder, tblT, switch)
-- pass folder recursively as text name rather than folder object as latter
-- not supported in WINE
local function GetFolderContents(Folder, I)
local colFiles = luacom.GetEnumerator(FSO:GetFolder(Folder).Files)
local objFile = colFiles:Next()
while objFile do
tblT[objFile.Path:sub(I+1)] = switch
objFile = colFiles:Next()
end
local colSubFolders = luacom.GetEnumerator(FSO:GetFolder(Folder).SubFolders)
local objSubFolder = colSubFolders:Next()
while objSubFolder do
GetFolderContents(objSubFolder.Path, I)
objSubFolder = colSubFolders:Next()
end
end
local I = Folder:len()
GetFolderContents(Folder, I)
end
-- *********************************************************************
function GetOptions()
-- get system data
gblOptions = {}
local tblO = {}
local pc = iup.GetGlobal('COMPUTERNAME')
-- "3-test" version of emulator detection
local check1 = FSO:FolderExists('Z:\\bin') and FSO:FolderExists('Z:\\etc')
local check2 = not FSO:FolderExists('C:\\Windows\\SoftwareDistribution')
local check3 = os.getenv('WINEPREFIX')
if check1 and check2 and check3 then
gblOptions.Emulator = true
elseif not check1 and not check2 and not check3 then
gblOptions.Emulator = false
else
local msg = 'Emulator detection has given an ambiguous result.\n\n'
msg = msg .. 'Z:\\ method: ' .. tostring(check1) .. '\n'
msg = msg .. 'C:\\Windows\\SoftwareDistribution method: ' .. tostring(check2) .. '\n'
msg = msg .. 'WINEPREFIX method: ' .. tostring(check3) .. '\n\n'
msg = msg .. 'Please report on FHUG for investigation.'
MessageBox(msg, 'OK', 'ERROR')
return
end
-- get system level options
gblOptions.Temp = os.getenv('TEMP')
if not FSO:FolderExists(gblOptions.Temp) then
MessageBox('Cannot locate Temp folder.', 'OK', 'ERROR')
return
end
gblOptions.ProgramData = fhGetContextInfo('CI_APP_DATA_FOLDER')
gblOptions.AppData = os.getenv('APPDATA')
if not FSO:FolderExists(gblOptions.AppData) then -- Unicode user name
local path = fhGetPluginDataFileName('CURRENT_USER')
local i = path:find('\\Calico Pie\\Family Historian\\Plugin Data\\')
gblOptions.AppData = path:sub(1, i-1)
end
gblOptions.AppData = gblOptions.AppData .. '\\Calico Pie\\Family Historian'
if gblOptions.Emulator then
gblOptions.AppDataPath = gblOptions.AppData
else
gblOptions.AppDataPath = FSO:GetFolder(gblOptions.AppData).ShortPath
end
-- get saved options
local File = gblOptions.ProgramData .. '\\Plugin Data\\' ..
'Backup and Restore Family Historian Settings via Windows.ini'
if FSO:FileExists(File) then
for line in io.lines(File) do
local i = line:find('=')
if i then tblO[line:sub(1, i-1)] = line:sub(i+1) end
end
if tblO.Path and tblO.ShortPath and FSO:FolderExists(tblO.Path) then
gblOptions.Path = tblO.Path
gblOptions.ShortPath = tblO.ShortPath
end
if not gblOptions.Emulator and tblO.Advanced == '1' or tblO.Advanced == 'ON' then
gblOptions.Advanced = true
end
if tblO.Dated == '1' or tblO.Dated == 'ON' then gblOptions.Dated = true end
for k, _ in pairs(tblO) do -- ensure ARM settings are retained
if k:match('ARM_Msg_') then
gblOptions[k] = true
end
end
end
-- get processor architecture
local key = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\' ..
'PROCESSOR_ARCHITECTURE'
gblOptions.CPU = GetRegKey(key)
if gblOptions.CPU == 'ARM64' and not gblOptions['ARM_Msg_' .. pc] then
local msg = 'Your PC has a CPU with ARM infrastructure. ' ..
'Issues have been reported by Mac users running FH inside a VM when backing up to a ' ..
'location on the host Operating System. If this applies to you, you are recommended ' ..
'to select a backup location on either an external drive or network location.\n\n' ..
'See the plugin help file for more details of this topic.\n\n' ..
'Do you wish to hide this message on future runs?'
if MessageBox(msg, 'YESNO', 'WARNING') == 1 then
gblOptions['ARM_Msg_' .. pc] = true
end
elseif gblOptions.CPU ~= 'ARM64' and gblOptions['ARM_Msg_' .. pc] then
gblOptions['ARM_Msg_' .. pc] = false
end
-- set version option and save back to file to ensure saved on backup
local V1, V2, V3 = fhGetAppVersion()
gblOptions.Version = V1 .. '.' .. V2 .. '.' .. V3
SaveOptions()
return true
end
-- *********************************************************************
function SaveOptions()
-- save options to file
local OptionsFolder = gblOptions.ProgramData .. '\\Plugin Data'
if not FSO:FolderExists(OptionsFolder) then
FSO:CreateFolder(OptionsFolder)
end
local pc = iup.GetGlobal('COMPUTERNAME')
local OptionsFile = OptionsFolder .. '\\Backup and Restore Family Historian Settings via Windows.ini'
local F = io.open(OptionsFile, 'w')
if not F then
MessageBox('Cannot open options file.', 'OK', 'ERROR', 'File Access Error')
return
end
F:write('[Options]\n')
F:write('Version=' .. gblOptions.Version .. '\n')
if gblOptions.Path then F:write('Path=' .. gblOptions.Path .. '\n') end
if gblOptions.ShortPath then F:write('ShortPath=' .. gblOptions.ShortPath .. '\n') end
if gblOptions.Advanced then F:write('Advanced=1\n') end
if gblOptions.Dated then F:write('Dated=1\n') end
if gblOptions['ARM_Msg_' .. pc] then
F:write('ARM_Msg_' .. pc .. '=1\n')
end
for k, _ in pairs(gblOptions) do -- ensure ARM settings relating to other PCs are retained
if k:match('ARM_Msg_') and k ~= 'ARM_Msg_' .. pc then
F:write(k .. '=1\n')
end
end
F:close()
end
-- *********************************************************************
function GetHelp(MainForm)
-- hidden diagnostic window
if iup.GetGlobal('SHIFTKEY') == 'OFF' then
fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/' ..
'backup-and-restore-family-historian-settings-via-windows')
else
local CPU = GetRegKey('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\' ..
'PROCESSOR_ARCHITECTURE')
local tblT = {'Monitors Count = ' .. (iup.GetGlobal('MONITORSCOUNT') or 'n/a'),
'Monitors Info = ' .. (iup.GetGlobal('MONITORSINFO') or 'n/a'),
'Screen Size = ' .. (iup.GetGlobal('SCREENSIZE') or 'n/a'),
'Full Size = ' .. (iup.GetGlobal('FULLSIZE') or 'n/a'),
'Virtual Screen = ' .. (iup.GetGlobal('VIRTUALSCREEN') or 'n/a'),
'Menu Size = ' .. (MainForm.NaturalSize or 'n/a'),
'Menu Position = ' .. (MainForm.ScreenPosition or 'n/a'),
'',
'Processor Architecture = ' .. gblOptions.CPU or 'FALSE',
'Emulator = ' .. tostring(gblOptions.Emulator),
'',
'Family Historian Version = ' .. gblOptions.Version
--[[ options required only for detailed debugging
'System = ' .. (iup.GetGlobal('SYSTEM') or ''),
'System Version = ' .. (iup.GetGlobal('SYSTEMVERSION') or ''),
'System Language = ' .. (iup.GetGlobal('SYSTEMLANGUAGE') or ''),
'System Locale = ' .. (iup.GetGlobal('SYSTEMLOCALE') or ''),
'Computer Name = ' .. (iup.GetGlobal('COMPUTERNAME') or ''),
'',
'User Name (IUP) = ' .. (iup.GetGlobal('USERNAME') or ''),
'Temp Folder = ' .. gblOptions.Temp,
'ProgramData Folder = ' .. gblOptions.ProgramData:gsub(' ', ' '),
'AppData Folder = ' .. gblOptions.AppData:gsub(' ', ' '),
]]
}
MessageBox(table.concat(tblT, '\n'), 'OK', 'INFORMATION', 'System Information')
end
end
-- *********************************************************************
function IsExtendedDesktop()
-- determines if system has an extended desktop
local xd = iup.GetGlobal('VIRTUALSCREEN')
local x, y, width, height = xd:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$')
local fs = iup.GetGlobal('FULLSIZE')
local ExtendedDesktop = fs ~= width .. 'x' .. height
return ExtendedDesktop
end
-- *********************************************************************
function IsASCII(S)
-- checks whether the supplied string comprises just ASCII characters
for i = 1, S:len() do
if S:byte(i) < 32 or S:byte(i) > 127 then return false end
end
return true
end
-- *********************************************************************
function CountInstances()
-- checks just a single instance of FH is running
local F = io.popen('powershell get-process *fh*')
if not F then
local msg = 'Cannot determine number of Family Historian copies running.'
MessageBox(msg, 'OK', 'ERROR', 'File Handle Access Error')
return
end
local S = F:read('*a')
F:close()
local Instances = 0
for line in S:gmatch('[^\r\n]+') do
if line:lower():match('%sfh%s+$') then
Instances = Instances + 1
end
end
return Instances
end
-- *********************************************************************
function GetRegKey(key)
local sh = luacom.CreateObject 'WScript.Shell'
local ans
if pcall(function () ans = sh:RegRead(key) end) then
return ans
else
return nil,true
end
end
-- *********************************************************************
function MessageBox(Message, Buttons, Icon, Title, Default)
-- replaces built-in function with custom version containing more options
local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
title = Title or fhGetContextInfo('CI_PLUGIN_NAME'), buttondefault = Default,
parentdialog = MainForm}
msgdlg:popup()
return tonumber(msgdlg.ButtonResponse)
end
-- *********************************************************************
main()
Source:Backup-and-Restore-Family-Historian-Settings-via-Windows-8.fh_lua