I have the following problem:
When a user logs on over citrix, the logon script writes to a network file
Once the file has been written to, notify the local machine so that it can perform some operations
Once this is done, restart the session (I know how to do this part, my difficulty is with monitoring a network file from a locally stored FileWatcher and then running a local exe)
I plan to achieve this by:
Registering an ActiveEventScriptConsumer on the machine, which will monitor a file and then run an executable once it is modified
The first actions the exe takes is to sleep for 5 minutes, and then perform the actions I need it to do
Constraints
The issue I have is that ActiveEventScriptConsuemrs can't use the Wscript object, meaning I can't run an exe with:
objShell.Run("C:\MyProgram.exe")
Is there some other way of running an executable from vbscript which can be done from an EventConsumer? Alternatively, am I able to register this subscription where the script run uses something other than VBScript?
I would include the Wait in my logons script, but I can't delay the end of this because this causes my group policies to run 5 minutes after logon!
I tried having a look at running a batch file from the VBS, but this also uses WScript.
The executable is checking for something which occurs when a (Citrix) session begins, so I can't use regular windows logon as a trigger because sometimes the user will log onto a session even though the machine itself has been logged on for some time.
Any help is much appreciated!
Event Subscription Code:
$Computername = $env:COMPUTERNAME
$query = #"
SELECT * FROM __InstanceModificationEvent WITHIN 1 WHERE TargetInstance ISA 'CIM_DataFile' AND TargetInstance.Name='C:\\test\\filewatching\\tester.txt'
"#
$instanceFilter = ([WMICLASS]"\\$Computername\root\subscription:__EventFilter").CreateInstance()
$instanceFilter.QueryLanguage = 'WQL'
$instanceFilter.Query = $query
$instanceFilter.Name = 'EventFilterNameHere'
$instanceFilter.EventNameSpace = 'root/CIMV2'
$result = $instanceFilter.Put()
$script =
#"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("c:\test\filewatching\Log.log", 8, True)
objFile.WriteLine "1"
Dim objShell
Set objShell = WScript.CreateObject( "WScript.Shell" )
objShell.Run("C:\MyProgran.exe")
Set objShell = Nothing
objFile.WriteLine "2"
objFile.Close
"#
$instanceConsumer = ([wmiclass]"\\$Computername\root\subscription:ActiveScriptEventConsumer").CreateInstance()
$instanceConsumer.Name = 'ConsumerNameHere'
$instanceConsumer.ScriptingEngine = 'VBScript'
$instanceConsumer.ScriptFilename = ''
$instanceConsumer.ScriptText = $script
$instanceConsumer.Put()
[object]$Filter = (Get-WMIObject -Computername $Computername -Namespace root\Subscription -Class __EventFilter | Sort Name)
[object]$Consumer = (Get-WMIObject -Computername $Computername -Namespace root\Subscription -Class __EventConsumer | Sort Name)
$instanceBinding = ([wmiclass]"\\$Computername\root\subscription:__FilterToConsumerBinding").CreateInstance()
$instanceBinding.Filter = $Filter
$instanceBinding.Consumer = $Consumer
$instanceBinding.Put()
This is not possible using this type of event consumer. This is because the ActiveScriptEventConsumer doesn't allow any screen interaction except for basic message boxes:
Docs
When WMI is run as a service, scripts run by ActiveScriptEventConsumer do not generate screen output. Scripts that use MsgBox do run, but they do not display information on the screen. Running the WMI service as an executable file is not supported, but WMI allows scripts that use the MsgBox function to display output or accept user input. None of the methods provided by the WScript object can be used because ActiveScriptEventConsumer does not use Windows Script Host (WSH).
Another option would be to use CSCript, which only uses the command line, but this also doesn't support visual activity (only command-line), so for my purposes this will not work.
Related
I am simply trying to connect to a different SharePoint site. This code works perfectly with a specific site but when using a different site with the correct permissions to the site and site list, I receive the following error:
format-default: The collection has not been initialized. It has not been requested or the request has not been executed. It may need to be explicitly requested.
If the user has full control on the SharePoint list as well as the SharePoint site, why isn't the connection initialized?
I have stepped through the code line by line to see what is being stored to each variable and find that the code throws the error at the following line:
$sharepointlistitems = Get-ListItems -Context $Context -ListTitle $ListName
Here is the code from start to finish for accessing the site. The commented out site works perfectly with the code as is:
[Reflection.Assembly]::LoadFile(([System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client").location))
[Reflection.Assembly]::LoadFile(([System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.runtime").location))
$admincreds = Get-Credential
$SPCredentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($admincreds.UserName, $admincreds.Password)
######### Create Sharepoint List Functions ######################################################################
#Function to get items that are not processed
Function Get-ListItems([Microsoft.SharePoint.Client.ClientContext]$Context, [String]$ListTitle) {
$list = $Context.Web.Lists.GetByTitle($ListTitle)
$qry = [Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery()
#In future query by status to avoid slowing down of program
$items = $list.GetItems($qry)
$Context.Load($items)
$Context.ExecuteQuery()
return $items
}
#Function to update item status
Function Set-ListItemByID([Microsoft.SharePoint.Client.ClientContext]$Context, [String]$ListTitle, [int]$ItemId, [string]$Status) {
$list = $Context.Web.Lists.GetByTitle($ListTitle)
$listitem = $list.GetItemById($ItemId)
$listitem["AutoAccountCreationStatus"] = $Status
$listitem.Update();
$Context.ExecuteQuery()
}
########################## Step 1: Read from SharePoint List and add to an array ###########################################
#Sets Site, sets list of interest, creates array
#"https://<sitename>.sharepoint.com/sites/Home/TeamSite"
$SiteUrl = "https://<sitename>.sharepoint.com/sites/InfTech"
$ListName = "New Users"
#Setup the context
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl)
$Context.Credentials = $SPCredentials
#Getting items from list
$sharepointlistitems = Get-ListItems -Context $Context -ListTitle $ListName
$sharepointlistitems = $sharepointlistitems | where {$_['AutoAccountCreationStatus'] -eq 'Received'}```
I use Power GUI Script editor to debug PowerShell script usually, we could monitor the variables, it's helpful for troubleshooting.
Or, you could use PnP PowerShell instead,demo
I'm trying to get a process ID by means of the process's execution-path. For that I'm executing the below Powershell command which runs perfectly in Powershell's console:
(Get-Process | Where-Object {$_.Path -eq 'C:\WINDOWS\system32\winlogon.exe'}).Id
But executing the same through C# is giving no results. below is the code snippet I'm following:
string cmd = "(Get-Process | Where-Object {{$_.Path -eq '{0}'}}).Id";
string path = #"C:\WINDOWS\system32\winlogon.exe";
string finalCmd = string.Format(cmd, System.IO.Path.GetFullPath(path));
powershell.Runspace = runspace;
powershell.AddScript(finalCmd);
var result = powershell.Invoke();
I'm using double-culry-braces for escape sequence. But still powershell.Invoke() returns nothing but null. Is there any other way to get the Process Id with its executable path?
My ultimate goal is that I should be able to push an application (MSI installer) to all the PCs in network through Active Directory(irrespective of x86/x64) and I should get the Process Ids for the given executable path. Thanks for the suggestions but in my case I need a generic solution which should work seamlessly for both x86 and x64.
Doesn't seem like you need to use Powershell here. .NET code can query processes directly.
Something like:
Process.GetProcesses().Where(p=>p.MainModule.FileName==path)
should return you an enumerable of all matching processes, from which you can easily retrieve their IDs. And decide what to do if you find more than one!
since some time now I try to figure out how to correctly setup this new UWF (Unified Write Filter). Unfortunately it seems there is only documentation for Win 8.1 industry (here), not for Win 10. I hope there were no relevant changes since.
I also asked this on the WindowsDevCenter but got no response so far.
Here is my problem:
With the WMI providers I got UWF enabled by now (UWF_Filter.Enable()), but I cannot protect any volume.
Also the volume list looks very strange: There are 4 entrys, everyone is with CurrentSession=True.
The first is for an volume with no drive letter, only a volume id.
The second is for C:
and then there are 2 identical for D: .
Should'nt there normally be 2 entrys per volume, one where CurrentSession is true and one where its false, meaning its the setting applied after reboot?
If I try to execute Protect on the ManagementObject with DriveLetter=C: I get an Access denied exception, I assume because its the object for the current session.
Also if I try uwfmgr.exe Volume Protect C: on the console it simply hangs: no reaction, no error, only a forever blinking cursor. EDIT: it turned out this was a problem caused by another installed software. See also below.
Do I have to enable or disable or do anything else before I can protect volumes?
Thanks in advance,
Sebastian
My system:
Windows 10 IOT Enterprise 2016 LTSB x64
1 SSD 250GB with Boot, C: and D:
Edit:
Here I asked a follow up question with some other details and a workaround. If I use uwfmgr.exe volume protect c: for example, it works and UWF_Volume now suddenly has (the correct) 2 entries for C:, one for the current and one for the next session.
However I want to avoid this, because IMHO it should be solveable by WMI only.
Edit 2: #sommmen
The partition layout is as following: One disk with 4 partitions.
Boot, 500MB
C:/ , 45GB
unknown, 500MB (Boot-Backup I think)
D:/ , ~200GB
PS:
Please could anyone create the tags uwf and uwfmgr? Would be nice :-)
Missing UWF_Volume instances often appeared after reboot in my tests. But if not, you can create them directly using ManagementClass.CreateInstance().
The problem here is that the official docs are not exactly correct. The description of the UWF_Volume.VolumeName property is:
The unique identifier of the volume on the current system. The
VolumeName is the same as the DeviceID property of the Win32_Volume
class for the volume.
from: https://learn.microsoft.com/en-us/windows-hardware/customize/enterprise/uwf-volume#properties
In fact, the DeviceID needs a slight modification, before using it as value for UWF_Volume.VolumeName:
DeviceID.Substring(4).TrimEnd('\\')
So, after removing prefix \\?\ and removing any trailing slashes you can create instances with CurrentSession=false for the specified device.
This also works in Windows 10 Pro without any uwfmgr.exe. Though, officially not recommended/supported.
Also, I was not able to delete instances, yet. So be sure to add only correct values.
Full Example:
// example value
var DeviceId_From_Win32_Volume = #"\\?\Volume{c2eac053-27e3-4f94-b28c-c2c53d5f4fe1}\";
// example value
var myDriveLetter = "C:";
var myDeviceId = DeviceId_From_Win32_Volume.Substring(4).TrimEnd('\\');
var wmiNamespace = "root\\standardcimv2\\embedded";
var className = "UWF_Volume";
var mgmtScope = new ManagementScope {Path = {NamespacePath = wmiNamespace}};
var mgmtPath = new ManagementPath(className);
var mgmtClass = new ManagementClass(mgmtScope, mgmtPath, null);
// prepare the new object
var newObj = mgmtClass.CreateInstance();
newObj.SetPropertyValue("DriveLetter", myDriveLetter);
newObj.SetPropertyValue("VolumeName", myDeviceId);
newObj.SetPropertyValue("CurrentSession", false);
newObj.SetPropertyValue("CommitPending", false);
newObj.SetPropertyValue("BindByDriveLetter", false);
// create the WMI instance
newObj.Put(new PutOptions {Type = PutType.CreateOnly});
I experience the similar issue in that I could not query the UWF_Volume with CurrentSession=False. However, there's one thing I did that seems to "generate" the UWF_Volume management object with CurrentSession=False. I ran "uwfmgr volume protect c:". Unfortunately, in your case running this causes it to hang.
Could you try running uwfmgr in cmd in admin? Also, if you run "uwfmgr get-config", would you be able to get the current setting of the write filter?
Another thing from your description: you said there are two identical volumes for D:, but if you looks closely at the properties, one would be CurrentSession=True, and the other one is CurrentSession=False. According to the documentation, if you want to make change, you must select the management object (UWF_Volume) with CurrentSession=False.
https://learn.microsoft.com/en-us/windows-hardware/customize/enterprise/uwf-volume
(scroll down to powershell script code sample section)
First of all a volume may have several partitions. They will show up as having the same drive label.
e.g.
C:/ //?/{some guid here}
C:/ //?/{some other guid here}
Now this is common for the %systemDrive% because this has the boot partition.
You can use the commands
mountvol
and
Diskpart
List volume
To figure out the right guid for your need (or you can protect both the boot partition and the system partition). Also using wmi you can look at Win32_volume under namespace cimv2 to get some more insight.
The command line util UWFmgr seems to create an UWF_VOLUME wmi instance once you run the protect command. The docs also hint that you need to create an object yourself.
function Set-ProtectVolume($driveLetter, [bool] $enabled) {
# Each volume has two entries in UWF_Volume, one for the current session and one for the next session after a restart
# You can only change the protection status of a drive for the next session
$nextConfig = Get-WMIObject -class UWF_Volume #CommonParams |
where {
$_.DriveLetter -eq "$driveLetter" -and $_.CurrentSession -eq $false
};
# If a volume entry is found for the drive letter, enable or disable protection based on the $enabled parameter
if ($nextConfig) {
Write-Host "Setting drive protection on $driveLetter to $enabled"
if ($Enabled -eq $true) {
$nextConfig.Protect() | Out-Null;
} else {
$nextConfig.Unprotect() | Out-Null;
}
}
=======> (!) im talking about this comment
# If the drive letter does not match a volume, create a new UWF_volume instance
else {
Write-Host "Error: Could not find $driveLetter. Protection is not enabled."
}
}
The docs however do not provide a method of doing this. For now it seems we need to use the command line util till someone has an example using the WMI provider.
To answer my own question: So far I have only a workaround but no real solution.
It is to check if there is an entry with CurrentSession=False and if not invoke the command directly:
ManagementObjectSearcher ms = new ManagementObjectSearcher(_Scope, new ObjectQuery("select * from UWF_Volume where VolumeName = \"" + volId + "\" AND CurrentSession=\"False\""));
ManagementObjectCollection c = ms.Get();
UInt32 res = 1;
foreach (ManagementObject mo in c)
{
// entry found: do it with WMI
res = (UInt32)mo.InvokeMethod(newState ? "Protect" : "Unprotect", new object[] { });
}
if (c.Count == 1 && res == 0)
// message: success
if (c.Count == 0)
{
// no entry found: invoke cmd
ProcessStartInfo info = new ProcessStartInfo("uwfmgr.exe", "volume " + (newState ? "Protect" : "Unprotect") + #" \\?\" + volId);
Process process = new Process();
info.Verb = "runas"; //needs admin
process.StartInfo = info;
process.Start();
process.WaitForExit();
}
This has the side effect that for a split second a command line window will pop up, but nevertheless it works well.
I need to be able to execute a PS1 script that resides on a remote machine against another remote machine through a C# runspace.
To be clear what I mean by this: The service I'm creating resides on server A. It creates a remote runspace to server B using the method below. Through the runspace I'm trying to call a script residing on server C against server B. If it helps, currently server A IS server C, but it's not guaranteed that will always be the case.
Here's the method I'm using to make the remote call:
internal Collection<PSObject> RunRemoteScript(string remoteScript, string remoteServer, string scriptName, out bool scriptSuccessful)
{
bool isLocal = (remoteServer == "localhost" || remoteServer == "127.0.0.1" || remoteServer == Environment.MachineName);
WSManConnectionInfo connectionInfo = null;
if (!isLocal)
{
connectionInfo = new WSManConnectionInfo(new Uri("http://" + remoteServer + ":5985"));
}
PsHostImplementation myHost = new PsHostImplementation(scriptName);
using (Runspace remoteRunspace = (isLocal ? RunspaceFactory.CreateRunspace(myHost) : RunspaceFactory.CreateRunspace(myHost, connectionInfo)))
{
remoteRunspace.Open();
using (PowerShell powershell = PowerShell.Create())
{
powershell.Runspace = remoteRunspace;
Pipeline pipeline = remoteRunspace.CreatePipeline();
pipeline.Commands.AddScript(remoteScript);
Collection<PSObject> results = pipeline.Invoke();
remoteRunspace.Close();
scriptSuccessful = myHost.ScriptSuccessful;
return results;
}
}
}
"remoteScript" is set to the Powershell script I want to run. For example:
"& \"\\\\remoteserveraddress\\PathToScript\\Install.ps1\" -Parameter;Import-Module Modulename;CustomCommand-FromModule -parameter(s) -ErrorAction stop"
If I'm on the remote machine that I want to run the script on, in the powershell console I can just give the following command:
& "\\remoteserverC\PathToScript\Install.ps1" -Parameter
However this simply refuses to work for me if I try to run it through the c# runspace.
If I send in the following as a parameter to "remoteScript":
"& \"\\\\remoteserverC\\PathToScript\\Install.ps1\" -Parameter"
I get the following error:
The term '\remoteserverC\PathToScript\Install.ps1' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
I've tried with and without '&' and with and without the parameter. I can already call a script that resides directly on the remote machine "c:\...\Install.ps1" instead of "\\remoteserver\...\Install.ps1", but it would be greatly beneficial to be able to call the remote script directly.
I've searched many many pages in google and here on stackoverflow, but I haven't been able to find anything that helps to overcome this issue. Any help would be appreciated! Thanks!
I never did get this to work directly and seems to be a security "feature" that you can't access a third machine using a UNC address while working remotely. I was however able to find a workaround that worked great for me.
Instead of trying to call directly to a \\server\share address, I dynamically map a network drive on the machine I'm trying to run the script against to a share on the machine that has the script. (Running remotely from A, map a drive on B to a share on C). Then I call my scripts through that drive and it works like a charm. This string is what I pass in to the RunRemoteScript method above:
"$net = new-object -ComObject WScript.Network;" +
"if(!($net.EnumNetworkDrives() -contains \"S:\"))" +
"{Write-Host \"S: Drive Not Currently Mapped. Mapping to \\\\" + RemoteServerC + "\\Share.\";" +
"$net.MapNetWorkDrive(\"S:\",\"\\\\" + RemoteServerC + "\\Share\",$false,\"username\",\"password\")};" +
"Get-PSDrive | Write-Verbose;" +
"& \"S:\\PathToScript\\Install.ps1\" -noPrompt;"
RemoteServerC is a variable I pass in that is defined in a user config file.
Here is the same code as just powershell script if anyone needs it (replacing RemoteServerC with a powershell variable you'd need to set before or just hardcode:
$net = new-object -ComObject WScript.Network
if(!($net.EnumNetworkDrives() -contains "S:"))
{ Write-Host "S: Drive Not Currently Mapped. Mapping to \\remoteserverC\Share."
$net.MapNetWorkDrive("S:","\\remoteserverC\Share",$false,"username","password")
}
Get-PSDrive | Write-Verbose
& "S:\\PathToScript\\Install.ps1\" -noPrompt;"
First I set up the object to be able to map the drive. I then check if there is already an "s" drive mapped on the remote machine. If it hasn't I then map the share to the "s" drive on the remote machine using a name and password we set up in active directory to run this service.
I included the Get-PSDrive command because it appears to force powershell to reload the list of available drives. This seems to only matter the very first time you try to run this in a powershell session (and through c sharp I don't know if it is truly necessary or not, I included it to be safe). Apparently powershell does not recognize a drive addition in the same session it was created if you use MapNetworkDrive.
We have a company application for automating certain tasks (it doesn't matter what actually). In our software we have the abbility to build a script based on our own commands, were also able to run a VBScript script within the same environment. We have a function built into the software, so we can get and set variables from a VBScript script within our own script.
In our manual we have this description: "The VB script feature provides a new VB object "AppName". One of the functions are: AppName.GetStringVariable("variable"). So by just using it like this in a VBScript script it's possible to set or get this variable. You would write it like this inside the VBScript script:
stringInput = AppName.GetStringVariable("variable")
The VBscript engine is running in a different process than the C#.NET application. So this VBscript object is running in a different process, which I would like to access in my C#.NET application.
Are there any possibility to get this variable within a C# .NET environment?
I have tried:
System.Environment.GetEnvironmentVariable("AppName.GetStringVariable(string1)");
System.Environment.GetEnvironmentVariable("AppName.GetStringVariable('string1')");
System.Environment.GetEnvironmentVariable("AppName.GetStringVariable(\"string1\")");
In the vbScript monitor a file that is updated by c# and set the property:
comp = "."
dir = "."
select = "SELECT * FROM __InstanceModificationEvent WITHIN 10 "
where = "WHERE Targetinstance ISA 'CIM_DirectoryContainsFile' and TargetInstance.GroupComponent= 'Win32_Directory.Name=" & dir & "'"
Set wmi = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & comp & "\root\cimv2")
Set events = wmi.ExecNotificationQuery(select & where)
Do
Set event = events.NextEvent
Wscript.Echo event.TargetInstance.PartComponent
Loop
Then from c# setup a FileSystemWatcher to another file (use json, it's easy to deserialize) and get the vbScript set property.