We have a need to remotely start/stop IIS websites and app pools, so we can remotely deploy a website.
I have a Websocket app that starts a PowerShell script to complete these activities. I built Powershell scripts to complete that tasks and they work perfectly in the Powershell prompt. However, when I try to run these scripts from the websocket, the scripts run (I have Write-Outputs in the scripts), but nothing happens the site and pool do not change. I don't see anything that says it failed, either. I would appreciate any help that can be given.
Below is an excerpt from the code:
using (PowerShell ps = PowerShell.Create())
{
string scriptContent = string.Empty;
string pathToReadScriptFile = string.Empty;
// add a script that creates a new instance of an object from the caller's namespace
if (r.StartStop.ToLower() == "stop")
{
pathToReadScriptFile = Path.Combine(scriptsPath, "StopPoolAndSite.ps1");
}
else
{
pathToReadScriptFile = Path.Combine(scriptsPath, "StartPoolAndSite.ps1");
}
using (StreamReader sr = new StreamReader(pathToReadScriptFile))
{
scriptContent = sr.ReadToEnd();
sr.Close();
}
ps.AddScript(scriptContent);
ps.AddParameter("siteName", r.SiteName);
ps.AddParameter("poolName", r.PoolName);
// invoke execution on the pipeline (collecting output)
Collection<PSObject> PSOutput = ps.Invoke();
// loop through each output object item
foreach (PSObject outputItem in PSOutput)
{
if (outputItem != null)
{
await SendMessageToAllAsync($"{outputItem.ToString()}");
}
}
}
Here is one of the powershell script code:
Param(
[Parameter(Mandatory=$True)]
[string]$siteName ,
[string]$poolName
)
if (-Not $poolName)
{
$poolName = $siteName
Write-Output "PoolName not supplied. Using $siteName as default. "
}
Import-Module WebAdministration
Write-Output "Preparing to Start AppPool: $poolName"
Write-Output "(OutPut)Preparing to Start AppPool: $poolName"
Start-WebAppPool $poolName
Write-Output "Preparing to Start Site: $siteName"
Start-WebSite $siteName
Get-WebSite $siteName
Actually i would suggest not to reinvent the wheel there is a project for that check these out :
https://github.com/microsoft/iis.administration
https://manage.iis.net/get
Related
I am using .NET with powershell trying to retrieve result of Get-Acl command of specific AD object. Unfortunately when I run the code from C# code I get 0 result. Also the ThrowIfError is not throwing any error.
Command test01 = new Command("import-module");
test01.Parameters.Add("name", "activedirectory");
session.Commands.AddCommand(test01);
Command test0 = new Command("Set-Location");
test0.Parameters.Add("Path", "AD:");
session.Commands.AddCommand(test0);
Command test1 = new Command("Get-Acl");
test1.Parameters.Add("Path", identity);
session.Commands.AddCommand(test1);
session.AddCommand("select-object");
session.AddParameter("Property", "Access");
var tempResults1 = session.Invoke();
ThrowIfError();
private void ThrowIfError()
{
var errors = session.Streams.Error;
if (errors.Count > 0)
{
var ex = errors[0].Exception;
session.Streams.ClearStreams();
// Never close session to dispose already running scripts.
throw ex;
}
}
This code running on server in powershell is working correctly:
PS AD:\> Import-Module -Name activedirectory
PS AD:\> set-location ad:
PS AD:\> get-acl -path <distinguishedNameOfADObject>
Question
How to get the same result like I get from Powershell? I should get atleast something not a zero result.
Little background:
I am trying to get Send-As rights not using Get-ADPermission cmdlet because its taking too long time when I need to search for rights within thousands of mailboxes. Using this article link I am trying another approach to get the rights. I have already the slower version working using C# code:
Command command = new Command("Get-ADPermission");
command.Parameters.Add("Identity", identity);
session.Commands.AddCommand(command);
session.AddCommand("where-object");
ScriptBlock filter = ScriptBlock.Create("$_.ExtendedRights -eq 'send-as'");
session.AddParameter("FilterScript", filter);
session.AddCommand("select-object");
session.AddParameter("Property", "User");
tempResults = session.Invoke();
The better way is to define a powershell-script instead of multiple commands to get the values you need. Example with your powershell-code:
using System.Collections.ObjectModel;
using System.DirectoryServices;
using System.Management.Automation;
namespace GetAclPowershellTest
{
class Program
{
static void Main(string[] args)
{
/****Create Powershell-Environment****/
PowerShell PSI = PowerShell.Create();
/****Insert PowershellScript****/
string Content = "param($object); Import-Module ActiveDirectory; Set-Location AD:; Get-ACL -Path $object"; //Add Scrip
PSI.AddScript(Content);
PSI.AddParameter("object", "<distinguishedNameOfADObject>");
/****Run your Script with PSI.Invoke()***/
Collection<PSObject> PSIResults = PSI.Invoke();
/****All Errors****/
Collection<ErrorRecord> Errors = PSI.Streams.Error.ReadAll();
/****needed, because garbagecollector ignores PSI otherwise****/
PSI.Dispose();
/**** Your ACL-Object ****/
ActiveDirectorySecurity MyACL = (ActiveDirectorySecurity)PSIResults[0].BaseObject;
/*insert your code here*/
}
}
}
This example works for me.
You have to set a reference to the Powershell-Assembly (Usually you can find it at "C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll")
Benefit of this solution is, you could read a .ps1-File you got from someone, fill the parameters with the objects you have and the script runs like in a standard powershell-session. The only requirement to set parameters is the param-part in the Script.
More Infos about param: https://technet.microsoft.com/en-us/library/jj554301.aspx
Hope, this helps...
Greetings, Ronny
Update:
string Content = "param($object); Import-Module ActiveDirectory; Set-Location AD:; (Get-ACL -Path $object).Access | Where-Object{($_.ActiveDirectoryRights -eq 'ExtendedRight') -and ($_.objectType -eq 'ab721a54-1e2f-11d0-9819-00aa0040529b')}";
And the loop at the end looks like this now:
foreach (PSObject o in PSIResults)
{
ActiveDirectoryAccessRule AccessRule = (ActiveDirectoryAccessRule)o.BaseObject;
/**do something with the AccessRule here**/
}
This script works when running in PowerShell ISE (it sets the given user's Remote Desktop Services Profile settings in Active Directory):
Get-ADUser FirstName.LastName | ForEach-Object {
$User = [ADSI]"LDAP://$($_.DistinguishedName)"
$User.psbase.invokeset("TerminalServicesProfilePath","\\Server\Share\HomeDir\Profile")
$User.psbase.invokeset("TerminalServicesHomeDrive","H:")
$User.psbase.invokeset("TerminalServicesHomeDirectory","\\Server\Share\HomeDir")
$User.setinfo()
}
But when I try running it from a C# application I get an error for each invokeset that I call:
Exception calling "InvokeSet" with "2" argument(s):
"Unknown name. (Exception from HRESULT: 0x80020006 (DISP_E_UNKNOWNNAME))"
Here is the code, which is inside my PowerShell class:
public static List<PSObject> Execute(string args)
{
var returnList = new List<PSObject>();
using (var powerShellInstance = PowerShell.Create())
{
powerShellInstance.AddScript(args);
var psOutput = powerShellInstance.Invoke();
if (powerShellInstance.Streams.Error.Count > 0)
{
foreach (var error in powerShellInstance.Streams.Error)
{
Console.WriteLine(error);
}
}
foreach (var outputItem in psOutput)
{
if (outputItem != null)
{
returnList.Add(outputItem);
}
}
}
return returnList;
}
And I call it like this:
var script = $#"
Get-ADUser {newStarter.DotName} | ForEach-Object {{
$User = [ADSI]""LDAP://$($_.DistinguishedName)""
$User.psbase.invokeset(""TerminalServicesProfilePath"",""\\file\tsprofiles$\{newStarter.DotName}"")
$User.psbase.invokeset(""TerminalServicesHomeDrive"",""H:"")
$User.psbase.invokeset(""TerminalServicesHomeDirectory"",""\\file\home$\{newStarter.DotName}"")
$User.setinfo()
}}";
PowerShell.Execute(script);
Where newStarter.DotName contains the (already existing) AD user's account name.
I tried including Import-Module ActveDirectory at the top of the C# script, but with no effect. I also called $PSVersionTable.PSVersion in both the script running normally and the C# script and both return that version 3 is being used.
After updating the property names to
msTSProfilePath
msTSHomeDrive
msTSHomeDirectory
msTSAllowLogon
I am getting this error in C#:
Exception calling "setinfo" with "0" argument(s): "The attribute syntax specified to the directory service is invalid.
And querying those properties in PowerShell nothing (no error but also no output)
Does anyone happen to know what could cause this?
Many thanks
Updated answer: It seems that these attributes don't exist in 2008+. Try these ones instead:
msTSAllowLogon
msTSHomeDirectory
msTSHomeDrive
msTSProfilePath
See the answer in this thread for the full explanation.
Original Answer:
The comment from Abhijith pk is probably the answer. You need to run Import-Module ActiveDirectory, just like you need to do in the command line PowerShell.
If you've ever run Import-Module ActiveDirectory in the PowerShell command line, you'll know it takes a while to load. It will be the same when run in C#. So if you will be running several AD commands in your application, you would be better off keeping a Runspace object alive as a static object and reuse it, which means you only load the ActiveDirectory module once.
There is details here about how to do that in C#:
https://blogs.msdn.microsoft.com/syamp/2011/02/24/how-to-run-an-active-directory-ad-cmdlet-from-net-c/
Particularly, this is the code:
InitialSessionState iss = InitialSessionState.CreateDefault();
iss.ImportPSModule(new string[] { "activedirectory" });
Runspace myRunSpace = RunspaceFactory.CreateRunspace(iss);
myRunSpace.Open();
I am running a PowerShell script using C#. The build is not able to determine the different file path written in the script but if I run script from command line it is working fine.
Here is my code for the running script:
private const string ScriptPath = "F:\\";
private const string SubPath = "build\\Build.ps1";
public Collection<PSObject> ExecuteBuildScript(BuildParams buildParams)
{
string executablePath = String.Empty;
string[] subdirectories = Directory.GetDirectories(ScriptPath);
Collection<PSObject> psOutput = null;
//Get path of appropriate branch
foreach (var subdirectory in subdirectories)
{
if (subdirectory.Contains(buildParams.Branch))
{
executablePath = subdirectory;
break;
}
}
if (!String.IsNullOrEmpty(executablePath))
{
using (PowerShell ps = PowerShell.Create())
{
//Enable the powershell execution on the system
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
RunspaceInvoke runSpaceInvoker = new RunspaceInvoke(runspace);
runSpaceInvoker.Invoke("Set-ExecutionPolicy Unrestricted");
ps.AddScript(Path.Combine(executablePath, SubPath));
ps.AddParameter("kit", !string.IsNullOrEmpty(buildParams.Kit) ? buildParams.Kit : "3CLogic");
ps.AddParameter("config", !string.IsNullOrEmpty(buildParams.Config) ? buildParams.Config : "Release");
ps.AddParameter("version", !string.IsNullOrEmpty(buildParams.ClientVersion) ? buildParams.ClientVersion : "latest");
ps.AddParameter("revision", !string.IsNullOrEmpty(buildParams.ClientRevision) ? buildParams.ClientRevision : "latest");
ps.AddParameter("serviceversion", !string.IsNullOrEmpty(buildParams.ServiceVersion) ? buildParams.ServiceVersion : "latest");
psOutput = ps.Invoke();
// check the other output streams (for example, the error stream)
if (ps.Streams.Error.Count > 0)
{
Console.WriteLine(ps.Streams.Error[0]);
// error records were written to the error stream.
// do something with the items found.
}
}
}
return psOutput;
}
Let say I want to import another script from called script it just failed to get path. Example importing include.ps1 from build.ps1 just not working and also Get-Location pick location of IIS server location.
. build\include.ps1
Use the $MyInvocation variable to determine the current script directory and combine the path using the Join-Path cmdlet:
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
. (Join-Path $scriptPath 'build\include.ps1')
I've recently started to explore powershell scripting as well as powershell hosting.
From a powershell command prompt the following line executes as I would expect.
"fee" | &{ process { $_; $args } } "fi" "fo" "fum"
This script outputs the "fee" that's in the pipeline ($_) plus the "fi", "fo", and "fum" that are passed in as arguments to the scriptBlock ($args).
I am trying to achieve this same result by using the System.Management.Automation.Powershell class.
When I try this:
using (var ps = PowerShell.Create())
{
var result = ps
.AddScript(#"&{ process { $_; $args} }")
.AddArgument("fi")
.AddArgument("fo")
.AddArgument("fum")
.Invoke(Enumerable.Repeat("fee", 1));
}
my result array contains a single null element (not sure what's going on there).
If I try this:
using (var ps = PowerShell.Create())
{
var result = ps
.AddCommand("Invoke-Command")
.AddParameter("ScriptBlock", ScriptBlock.Create(#"&{ process { $_; $args } }"))
.AddParameter("ArgumentList", new string[] { "fi", "fo", "fum"})
.Invoke(Enumerable.Repeat("fee", 1));
}
only "fee" is output. The $args array is empty.
Finally, when I try this:
using (var ps = PowerShell.Create())
{
var result = ps
.AddCommand("Invoke-Command")
.AddParameter("ScriptBlock", ScriptBlock.Create(#"&{ process { $_; $args } } ""fi"" ""fo"" ""fum"""))
.Invoke(Enumerable.Repeat("fee", 1));
}
I get closest. The pipelined "fee", and the "fi", "fo", and "fum" arguments are all output. The problem with this, however, is that I ideally would prefer to dynamically pass my arguments to the scriptBlock rather than concatenating them into the script block expression.
My question is how can I create a script block in a hosted powershell environment that can both access data in the pipeline from which it is invoked as well as accept parameters? I've only been able to achieve one or the other. I am using version 4.5 of the .Net framework and version 3 of the System.Management.Automation assembly.
It's because a wrong script is used for AddScript, &{} should not be used because it is actually done by Invoke() in the C# code. Here is the PowerShell analogue of C# code that works:
$ps = [powershell]::Create()
$ps.AddScript('process { $_; $args}').
AddArgument('fi').
AddArgument('fo').
AddArgument('fum').
Invoke(#('fee'))
Output:
fee
fi
fo
fum
as expected.
I have the following sample Powershell script that is embedded in my C# application.
Powershell Code
$MeasureProps = "AssociatedItemCount", "ItemCount", "TotalItemSize"
$Databases = Get-MailboxDatabase -Status
foreach($Database in $Databases) {
$AllMBStats = Get-MailboxStatistics -Database $Database.Name
$MBItemAssocCount = $AllMBStats | %{$_.AssociatedItemCount.value} | Measure-Object -Average -Sum
$MBItemCount = $AllMBStats | %{$_.ItemCount.value} | Measure-Object -Average -Sum
New-Object PSObject -Property #{
Server = $Database.Server.Name
DatabaseName = $Database.Name
ItemCount = $MBItemCount.Sum
}
}
Visual Studio offers me the following embedding options:
Every PowerShell sample I've seen (MSDN on Exchange, and MSFT Dev Center) required me to chop up the Powershell command into "bits" and send it through a parser.
I don't want to leave lots of PS1 files with my application, I need to have a single binary with no other "supporting" PS1 file.
How can I make it so myapp.exe is the only thing that my customer sees?
Many customers are averse to moving away from a restricted execution policy because they don't really understand it. It's not a security boundary - it's just an extra hoop to jump through so you don't shoot yourself in the foot. If you want to run ps1 scripts in your own application, simply use your own runspace and use the base authorization manager which pays no heed to system execution policy:
InitialSessionState initial = InitialSessionState.CreateDefault();
// Replace PSAuthorizationManager with a null manager which ignores execution policy
initial.AuthorizationManager = new
System.Management.Automation.AuthorizationManager("MyShellId");
// Extract psm1 from resource, save locally
// ...
// load my extracted module with my commands
initial.ImportPSModule(new[] { <path_to_psm1> });
// open runspace
Runspace runspace = RunspaceFactory.CreateRunspace(initial);
runspace.Open();
RunspaceInvoke invoker = new RunspaceInvoke(runspace);
// execute a command from my module
Collection<PSObject> results = invoker.Invoke("my-command");
// or run a ps1 script
Collection<PSObject> results = invoker.Invoke("c:\temp\extracted\my.ps1");
By using a null authorization manager, execution policy is completed ignored. Remember - this is not some "hack" because execution policy is something for protecting users against themselves. It's not for protecting against malicious third parties.
http://www.nivot.org/nivot2/post/2012/02/10/Bypassing-Restricted-Execution-Policy-in-Code-or-in-Script.aspx
First of all you should try removing your customer's aversion To scripts. Read up about script signing, execution policy etc.
Having said that, you can have the script as a multiline string in C# code itself and execute it.Since you have only one simple script, this is the easiest approach.
You can use the AddScript ,ethos which takes the script as a string ( not script path)
http://msdn.microsoft.com/en-us/library/dd182436(v=vs.85).aspx
You can embed it as a resource and retrieve it via reflection at runtime. Here's a link from MSDN. The article is retrieving embedded images, but the principle is the same.
You sort of hovered the answer out yourself. By adding it as content, you can get access to it at runtime (see Application.GetResourceStream). Then you can either store that as a temp file and execute, or figure out a way to invoke powershell without the use of files.
Store your POSH scripts as embedded resources then run them as needed using something like the code from this MSDN thread:
public static Collection<PSObject> RunScript(string strScript)
{
HttpContext.Current.Session["ScriptError"] = "";
System.Uri serverUri = new Uri(String.Format("http://exchangsserver.contoso.com/powershell?serializationLevel=Full"));
RunspaceConfiguration rc = RunspaceConfiguration.Create();
WSManConnectionInfo wsManInfo = new WSManConnectionInfo(serverUri, SHELL_URI, (PSCredential)null);
using (Runspace runSpace = RunspaceFactory.CreateRunspace(wsManInfo))
{
runSpace.Open();
RunspaceInvoke scriptInvoker = new RunspaceInvoke(runspace);
scriptInvoker.Invoke("Set-ExecutionPolicy Unrestricted");
PowerShell posh = PowerShell.Create();
posh.Runspace = runSpace;
posh.AddScript(strScript);
Collection<PSObject> results = posh.Invoke();
if (posh.Streams.Error.Count > 0)
{
bool blTesting = false;
string strType = HttpContext.Current.Session["Type"].ToString();
ErrorRecord err = posh.Streams.Error[0];
if (err.CategoryInfo.Reason == "ManagementObjectNotFoundException")
{
HttpContext.Current.Session["ScriptError"] = "Management Object Not Found Exception Error " + err + " running command " + strScript;
runSpace.Close();
return null;
}
else if (err.Exception.Message.ToString().ToLower().Contains("is of type usermailbox.") && (strType.ToLower() == "mailbox"))
{
HttpContext.Current.Session["ScriptError"] = "Mailbox already exists.";
runSpace.Close();
return null;
}
else
{
HttpContext.Current.Session["ScriptError"] = "Error " + err + "<br />Running command " + strScript;
fnWriteLog(HttpContext.Current.Session["ScriptError"].ToString(), "error", strType, blTesting);
runSpace.Close();
return null;
}
}
runSpace.Close();
runSpace.Dispose();
posh.Dispose();
posh = null;
rc = null;
if (results.Count != 0)
{
return results;
}
else
{
return null;
}
}
}
The customer just can't see the PowerShell script in what you deploy, right? You can do whatever you want at runtime. So write it to a temporary directory--even try a named pipe, if you want to get fancy and avoid files--and simply start the PowerShell process on that.
You could even try piping it directly to stdin. That's probably what I'd try first, actually. Then you don't have any record of it being anywhere on the computer. The Process class is versatile enough to do stuff like that without touching the Windows API directly.