// Windows sometimes needs to defer the replacement of in-use files until the // next system start. It does this by keeping a list of (new file, old file) // pairs in a REG_MULTI_SZ value at HKLM\SYSTEM\CurrentControlSet\Control\ // Session Manager\PendingFileRenameOperations, checking that list at startup // and moving (renaming) the new files over the old ones. See // http://ss64.com/nt/mv.html // // Unfortunately such moves will fail if the new and old files are not on // the same drive, and this can cause some app updaters to break if e.g. // %APPDATA% and %ProgramFiles% have been set up on different volumes. // // This script is designed to be run during system shutdown (e.g. from a // Group Policy shutdown script). It scans the pending rename list for doomed // cross-drive renames, and attempts to fix those by copying the new files // onto the same drives as those they are to replace. // // Windows Home editions without the ability to use shutdown scripts can run // this every few minutes from a scheduled task instead. Provided it gets to // run at least once after creation of a broken rename list, it will fix it. // // Stephen Thomas // Last updated 3-Nov-2010 // // This is free software. Do whatever you like with it except // hold me accountable for any grief it causes you. // Don't issue diagnostic spew when run from wscript var host = WScript.Fullname; var cscript = host.substr(host.length - 12).toLowerCase() == "\\cscript.exe"; // For FileSystemObject documentation, see // http://msdn.microsoft.com/en-us/library/6kxy1a51 fso = new ActiveXObject("Scripting.FileSystemObject"); // Getting at the registry from jscript requires bizarre contortions: see // http://msdn.microsoft.com/en-us/library/aa392722 // http://msdn.microsoft.com/en-us/library/aa389294 // http://msdn.microsoft.com/en-us/library/aa392730 // http://msdn.microsoft.com/en-us/library/aa394616 var registry = GetObject("winmgmts:{impersonationLevel=impersonate}!root/default:StdRegProv"); var HKCR = 0x80000000; var HKCU = 0x80000001; var HKLM = 0x80000002; var HKU = 0x80000003; var HKCC = 0x80000005; function ReadRegMultiSz(address) { var method = registry.Methods_.Item("GetMultiStringValue"); var inParam = method.InParameters.SpawnInstance_(); inParam.hDefKey = address[0]; inParam.sSubKeyName = address[1]; inParam.sValueName = address[2]; var outParam = registry.ExecMethod_(method.Name, inParam); if (outParam.ReturnValue == 0) { return outParam.sValue.toArray(); } else return []; } function WriteRegMultiSz(address, data) { // If the array passed as "data" contains any property other // than the actual array values, jscript will consider it // a sparse array, and passing it directly will make the // SetMultiStringValue method call fail with a Type Mismatch // error. Even reading the data array's length will cause // a .length property to spring into existence and make the // array sparse. Create a pristine fixed-size copy using // only numeric indices to work around this. var values = new Array(data.length); for (var i = 0; i < data.length; i++) { values[i] = data[i]; } var method = registry.Methods_.Item("SetMultiStringValue"); var inParam = method.InParameters.SpawnInstance_(); inParam.hDefKey = address[0]; inParam.sSubKeyName = address[1]; inParam.sValueName = address[2]; inParam.sValue = values; var outParam = registry.ExecMethod_(method.Name, inParam); return outParam.ReturnValue; } function Drive(path) { var drive = ""; if (path) { drive = fso.GetDriveName(fso.GetAbsolutePathName(path)).toUpperCase(); } return drive; } function RandomHex(bits) { var result = "0x"; for (var i = 0; i < bits; i += 4) { result += "0123456789abcdef".charAt(16 * Math.random()); } return result; } function Log(message) { if (cscript) { if (message instanceof Array) { WScript.Echo("["); for (var i = 0; i < message.length; i++) { WScript.Echo(" " + i + ": " + message[i]); } WScript.Echo("]"); } else { WScript.Echo(message); } } } // Get the list of files to be renamed at next startup. var renameListAddress = [HKLM, "SYSTEM\\CurrentControlSet\\Control\\Session Manager", "PendingFileRenameOperations"]; var renameList = ReadRegMultiSz(renameListAddress); Log(renameList); var changed = false; var copied = new Array; var tempFolders = new Array; // Make a first pass over the rename list to find and fix doomed renames. for (var i = 0; i < renameList.length; i += 2) { var source = renameList[i]; var destn = renameList[i + 1]; if (source.substr(0, 4) == "\\??\\" && destn.substr(0, 5) == "!\\??\\") { source = source.substr(4); destn = destn.substr(5); var sourceDrive = Drive(source); var destnDrive = Drive(destn); if (sourceDrive && destnDrive && sourceDrive != destnDrive) { // Rename won't work across drives, so the file needs // copying to a temp folder on the destination drive. // First, find or make the temp folder. var tempFolder; if (tempFolders[destnDrive]) { tempFolder = tempFolders[destnDrive]; } else { // Drive has no existing temp folder, so make a // new one and schedule it for later deletion. tempFolder = destnDrive + "\\" + RandomHex(96); tempFolders[destnDrive] = tempFolder; fso.CreateFolder(tempFolder); renameList.push("\\??\\" + tempFolder, ""); changed = true; } // Copy the file to the destination drive. // If the copy worked, remember it. try { newSource = tempFolder + "\\" + RandomHex(96); Log("\r\n" + source + "\r\n -> " + newSource); fso.CopyFile(source, newSource, false); copied["\\??\\" + source] = "\\??\\" + newSource; } catch (ex) { Log(ex.message); } } } } // Second pass edits all references to files that got copied, // making them refer to the copies instead of the originals. for (var i = 0; i < renameList.length; i += 2) { var source = renameList[i]; if (copied[source]) { renameList[i] = copied[source]; } } // Finish by prepending deletion requests for the originals of all // copied files. Originals should't simply be deleted as we go, as // app updaters might be testing for their continued existence. for (var source in copied) { renameList.unshift(source, ""); } // If the rename list was changed, write it back. if (changed) { WriteRegMultiSz(renameListAddress, renameList) Log(renameList); }