I am trying to change the Windows Console Mode for output (CONOUT$) using the Windows API and SetConsoleMode calls. So I modified a PowerShell script, based on ConinMode.ps1 (and which works for input), to do this. Reading works fine with both the ConoutMode.exe and my script, and returns:
# .\ConoutMode.exe
mode: 0x3
ENABLE_PROCESSED_OUTPUT 0x0001 ON
ENABLE_WRAP_AT_EOL_OUTPUT 0x0002 ON
ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004 off <====
DISABLE_NEWLINE_AUTO_RETURN 0x0008 off
ENABLE_LVB_GRID_WORLDWIDE 0x0010 off
# .\ConoutMode.ps1
OK! GetConsoleWindow handle : 0x1F06D6
Console Input Mode (CONIN) : 0x21f
Console Output Mode (CONOUT) : 0x3
However, both my script and the exe fails in writing the mode. (Possibly because it thinks that my output handle is not pointing to a console?)
In PowerShell:
# .\ConoutMode.exe set 0x000f
error: SetConsoleMode failed (is stdout a console?)
# .\ConoutMode.ps1 -Mode 7
OK! GetConsoleWindow handle : 0x1F06D6
old (out) mode: 0x3
SetConsoleMode (out) failed (is stdout a console?)
At C:\cygwin64\home\emix\win_esc_test\ConoutMode.ps1:112 char:9
+ throw "SetConsoleMode (out) failed (is stdout a console?)"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (SetConsoleMode ...out a console?):String) [], RuntimeException
+ FullyQualifiedErrorId : SetConsoleMode (out) failed (is stdout a console?)
Here is my ConoutMode.ps1 script in it's entirety:
param (
[int] $Mode
)
$signature = #'
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
public const int STD_INPUT_HANDLE = -10;
public const int STD_OUTPUT_HANDLE = -11;
public const int ENABLE_PROCESSED_OUTPUT = 0x0001;
public const int ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002;
public const int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
public const int DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
public const int ENABLE_LVB_GRID_WORLDWIDE = 0x0010;
'#
#----------------------------------------------------------
$WinAPI = Add-Type -MemberDefinition $signature -Name WinAPI -Namespace ConoutMode -PassThru
#$WinAPI = Add-Type -MemberDefinition $signature -Name WinAPI -Namespace IdentifyConsoleWindow -PassThru
$hwnd = $WinAPI::GetConsoleWindow()
if ($hwnd -eq 0) {
throw "Error: GetConsoleWindow returned NULL."
}
echo "OK! GetConsoleWindow handle : 0x$($hwnd.ToString("X"))"
function GetConIn {
$ret = $WinAPI::GetStdHandle($WinAPI::STD_INPUT_HANDLE)
if ($ret -eq -1) {
throw "Error: cannot get stdin handle"
}
return $ret
}
function GetConOut {
$ret = $WinAPI::GetStdHandle($WinAPI::STD_OUTPUT_HANDLE)
if ($ret -eq -1) {
throw "Error: cannot get stdout handle"
}
return $ret
}
function GetConInMode { #GetCOnsoleMode
$conin = GetConIn
$mode = 0
$ret = $WinAPI::GetConsoleMode($conin, [ref]$mode)
if ($ret -eq 0) {
throw "GetConsoleMode (in) failed (is stdin a console?)"
}
return $mode
}
function GetConOutMode {
$conout = GetConOut
$mode = 0
$ret = $WinAPI::GetConsoleMode($conout, [ref]$mode)
if ($ret -eq 0) {
throw "GetConsoleMode (out) failed (is stdout a console?)"
}
return $mode
}
function SetConInMode($mode) {
$conin = GetConIn
$ret = $WinAPI::SetConsoleMode($conin, $mode)
if ($ret -eq 0) {
throw "SetConsoleMode (in) failed (is stdin a console?)"
}
}
function SetConOutMode($mode) {
#$conout = GetConOut
# Different tack:
$conout = $hwnd
$ret = $WinAPI::SetConsoleMode($conout, $mode)
if ($ret -eq 0) {
throw "SetConsoleMode (out) failed (is stdout a console?)"
}
}
#----------------------------------------------------------
# MAIN
#----------------------------------------------------------
$oldInMode = GetConInMode
$oldOutMode = GetConOutMode
$newMode = $oldOutMode
$doit = $false
if ($PSBoundParameters.ContainsKey("Mode")) {
$newMode = $Mode
$doit = $true
}
if ($doit) {
echo "old (out) mode: 0x$($oldOutMode.ToString("x"))"
SetConOutMode $newMode
$newMode = GetConOutMode
echo "new (out) mode: 0x$($newMode.ToString("x"))"
} else {
echo "Console Input Mode (CONIN) : 0x$($oldInMode.ToString("x"))"
echo "Console Output Mode (CONOUT) : 0x$($oldOutMode.ToString("x"))"
}
So at the end of the day, I want to enable ENABLE_VIRTUAL_TERMINAL_PROCESSING for the Console.
The system I am using for this is:
---------------------------------------------------------
PowerShell Version : 6.1.1
OS Name : Microsoft Windows 8.1 (64-bit)
OS Version : 6.3.9600 [2018-11-16 00:50:01]
OS BuildLabEx : 9600.19179
OS HAL : 6.3.9600.18969
OS Kernel : 6.3.9600.18217
OS UBR : 19182
-------------------------------------------------------
with Privilege : Administrator
-------------------------------------------------------
ExecutionPolicy :
MachinePolicy : Undefined
UserPolicy : Undefined
Process : Undefined
CurrentUser : Undefined
LocalMachine : Bypass
Console Settings:
Type : ConsoleHost
OutputEncoding : Unicode (UTF-8)
Color Capability : 23
Registry VT Level : 1
CodePage (input) : 437
CodePage (output) : 437
I have also enabled the registry item: HKCU:\Console\VirtualTerminalLevel, as instructed elsewhere.
Q: How can I enable this bit/flag using PowerShell?
Some things I can think of that may be wrong:
I surely have the wrong understanding of how this works...
I might have the wrong permissions to write to console? (How to set?)
I might not write to the correct output buffer? (How to find?)
Note that setting the output mode of one screen buffer does not affect the output mode of other screen buffers.
When executing a script, perhaps a new output buffer is created? (What to do?)
I might have to create a new buffer?
I might have to create a new Console?
Related Questions and Links:
ENABLE_VIRTUAL_TERMINAL_PROCESSING and DISABLE_NEWLINE_AUTO_RETURN failing
MSDN: Inside the Windows Console
MSDN: Introducing the Windows Pseudo Console - ConPty
The code is using "GetStdHandle" to get stdin or stdout. And those might be console handles - but it's dependent on how the process is created. If you're launched from a console, and the stdin/stdout aren't redirected, then you'd probably get a console handle. But it's not guaranteed.
Rather than asking for stdin/stdout, just open the console handles directly - you can "CreateFile" for CONIN$ and CONOUT$. The documentation for CreateFile has a whole section on Consoles.
Update: I made a Gist to demonstrate how to use CreateFile to open CONIN$ and CONOUT$ from PowerShell.
I have been experimenting with this myself for a few days and have come to realize a few things. Apparently PowerShell enables Virtual Terminal Processing when it starts up. It disables it again, however, when it launches an executable and then reenables it when the executable exits. This means that whatever executable needs Virtual Terminal Processing must enable it itself or be called through a wrapper program that enables it and then pipes stdin/stdout/stderr through to the terminal. As for how would even go about writing such a program, I don't know.
Related
I'm trying to determine if an HTA is the foreground window. The following PowerShell will normally identify the foreground window:
Add-Type #"
using System;
using System.Runtime.InteropServices;
public class UserWindows {
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
}
"#
$a = [UserWindows]::GetForegroundWindow()
get-process | ? { $_.mainwindowhandle -eq $a }
But, if the HTA is in the foreground, no process is returned. (I.E. there is a MainWindowHandle, but there is no process!?)
The MSHTA process has an entirely different MainWindowHandle, but no window.
Process Hacker identifies the HTA window (frame or contents) as the mshta process.
How can I match up the HTA window and mshta.exe via script? Alternatively, how can I get the MainWindowHandle of the HTA window without knowing if it is in front?
The Foregroundwindow returned is not a mainwindowhandle of any process but just a window handle of mshta, you'll have to check all window handles.
I did manually with the tool cmdow.exe (had to convert the handle to hex) and got this for my example The HTA helpomatic :
> cmdow 0x14E0F46
Handle Lev Pid -Window status- Image Caption
0x14E0F46 1 153048 Res Ina Ena Vis mshta The HTA Helpomatic -- Presented by t
There should be better/more PowerShellish ways to enumerate the window handles but this changed script will use the mentioned cmdow.exe
## Get-ForegrounWindow.ps1
Add-Type #"
using System;
using System.Runtime.InteropServices;
public class UserWindows {
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
}
"#
while ($true -eq 'true') {
$ForeGroundWin = [UserWindows]::GetForegroundWindow()
$handle = "0x{0:x}" -f $($ForeGroundWin.ToInt64())
cmdow.exe $Handle /B /F
Sleep -sec 5
}
Sample output:
> .\Get-ForegrounWindow.ps1
0x15C04A2 1 127148 Res Act Ena Vis powershell Windows PowerShell
0x7F0DE4 1 135416 Res Act Ena Vis TextPad TextPad - Q:\Test\2017-06\09\Get-ForegrounWindow.ps1
0x16205D0 1 121732 Res Act Ena Vis bash usernamet#computer: ~
0x14E0F46 1 153048 Res Act Ena Vis mshta The HTA Helpomatic -- Presented by the Microsoft Scripting Guys
Modified the answer from here to get this which does what I need:
Add-Type #"
using System;
using System.Runtime.InteropServices;
using System.Text;
public class UserWindows {
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetWindowText(IntPtr hwnd,StringBuilder lpString, int cch);
[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern Int32 GetWindowTextLength(IntPtr hWnd);
}
"#
while(1) {
$ForgroundWindow = [UserWindows]::GetForegroundWindow()
$FGWTitleLength = [UserWindows]::GetWindowTextLength($ForgroundWindow)
$StringBuilder = New-Object text.stringbuilder -ArgumentList ($FGWTitleLength + 1)
$null = [UserWindows]::GetWindowText($ForgroundWindow,$StringBuilder,$StringBuilder.Capacity)
if ($StringBuilder.ToString() -match $HTAWindowTitleRegEx) {
# Put further scripting here for when the HTA window is in front
}
Start-Sleep -Seconds 1
}
Hope that helps someone.
The following code does not return "Y" as expected. Only in the next session (another new window) it works? I would expect it to be available immediately?
[Environment]::SetEnvironmentVariable("X", "Y", "Machine")
Write-Host $env:X
You must do this since the process gets env vars on start, not while running (i.e. you would have to restart shell for this to work your way):
[Environment]::SetEnvironmentVariable("X", "Y", "Machine")
$Env:X = "Y"
There is also a way to broadcast this to other windows using WM_SETTINGCHANGE
To effect a change in the environment variables for the system or the
user, broadcast this message with lParam set to the string
"Environment".)
# Notify system of change via WM_SETTINGCHANGE
if (! ("Win32.NativeMethods" -as [Type]))
{
Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition #"
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout( IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
"#
}
$HWND_BROADCAST = [IntPtr] 0xffff; $WM_SETTINGCHANGE = 0x1a; $result = [UIntPtr]::Zero
[Win32.Nativemethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", 2, 5000, [ref] $result) | out-null
}
As far as I know, a process loads the environment variables only once (at start). But you can change it using:
[Environment]::SetEnvironmentVariable("X", "Y", "Process") # for the current session
Note: You probably want to set both:
[Environment]::SetEnvironmentVariable("X", "Y", "Machine")
[Environment]::SetEnvironmentVariable("X", "Y", "Process")
I am developing a Qt application that, among other things, converts an Excel spreadsheet in a text delimited with tab file. This is done by running a Windows Powershell script.
My problem is that the finished() signal from the QProcess is never emitted, although the conversion is done successfully. And yes, I receive stateChanged() signal.
Powershell script (ps_excel.ps1)
(adapted from this question)
param ([string]$ent = $null, [string]$sal = $null)
$xlCSV = -4158 #value for tab delimited file
$Excel = New-Object -Com Excel.Application
$Excel.visible = $False
$Excel.displayalerts=$False
$b = $Excel.Workbooks.Open($ent)
$b.SaveAs($sal,$xlCSV)
$Excel.quit()
exit 0 #tested without exit, with exit and with exit 0
Qt app header
(For testing, the minimal case is a QDialog without other widgets.)
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
#include <QProcess>
#include <QDebug>
namespace Ui {
class Dialog;
}
class Dialog : public QDialog
{
Q_OBJECT
public:
explicit Dialog(QWidget *parent = 0);
~Dialog();
QProcess *proces;
private:
Ui::Dialog *ui;
private slots:
void procFinish(int estat);
void procState(QProcess::ProcessState estat);
};
#endif // DIALOG_H
Qt app C++
#include "dialog.h"
#include "ui_dialog.h"
Dialog::Dialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::Dialog)
{
ui->setupUi(this);
QString program = "C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
QStringList params;
params << "-File" << "C:/Garsineu/ps_excel.ps1" << "-ent" << "C:/Garsineu/PROVAGALATEA.xls" << "-sal" << "C:/Garsineu/PROVAGALATEA.tab";
proces = new QProcess();
connect(proces, SIGNAL(finished(int)), this, SLOT(procFinish(int)));
connect(proces, SIGNAL(stateChanged(QProcess::ProcessState)), this, SLOT(procState(QProcess::ProcessState)));
proces->start(program, params);
}
Dialog::~Dialog()
{
delete ui;
}
void Dialog::procFinish(int estat)
{
qDebug() << "Finished";
}
void Dialog::procState(QProcess::ProcessState estat)
{
qDebug() << estat;
}
Although the conversion is successful and the states 1 (Started) and 2 (Running) are shown, the message "Finished" is never displayed.
I also tried to do it synchronously, by waitForFinished () method, which is always timed out.
Environment
Windows 7 Professional 64 bit
Powershell v1.0 x86
Qt 5.4.0 with minGW491_32
I appreciate your help. Thank you.
I found the solution. Apparently Powershell does not run in the same way from the console or when is called from another program (noninteractive). If I add at end of ps_excel.ps1 script:
Get-Process Powershell | Stop-Process
instead of
exit 0
I Stop really Powershell and get finished signal, with QProcess::ExitStatus = 0.
Any ideas and suggestions on why this works when run from w/in PS, but not when run from a shortcut defined as:
%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -File "C:\Users\bin\ChangeDesktop.ps1"
Contents of ChangeDesktop.ps1:
set-itemproperty -path "HKCU:Control Panel\Desktop" -name WallPaper -value ""
rundll32.exe user32.dll, UpdatePerUserSystemParameters
If I am in the PS "command prompt" environment the desktop background is automatically removed and refreshed, outside of that I have to manually refresh the desktop to effect the change.
System is Windows Server 2008 R2 - fresh install. Script executionpolicy is set to RemoteSigned, and I don't see any PS errors. I Just don't see the desktop refresh automatically when running from a desktop shortcut.
scratches head
rundll32.exe user32.dll, UpdatePerUserSystemParameters didn't actually change the wallpaper for me on a 2008 x64 box. This does did though... It calls the Win32 API to invoke changing the wallpaper. If you save this as your ChangeDesktop.ps1 script it should work. As it is below it will remove any desktop wallpaper. However if you do want to set one you can edit the last line with the path of a supported image file like this:
[Wallpaper.Setter]::SetWallpaper( 'C:\Wallpaper.bmp', 0 )
The second argument is for the styling:
0: Tile
1: Center
2: Stretch
3: No Change
The script:
Add-Type #"
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;
namespace Wallpaper
{
public enum Style : int
{
Tile, Center, Stretch, NoChange
}
public class Setter {
public const int SetDesktopWallpaper = 20;
public const int UpdateIniFile = 0x01;
public const int SendWinIniChange = 0x02;
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int SystemParametersInfo (int uAction, int uParam, string lpvParam, int fuWinIni);
public static void SetWallpaper ( string path, Wallpaper.Style style ) {
SystemParametersInfo( SetDesktopWallpaper, 0, path, UpdateIniFile | SendWinIniChange );
RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", true);
switch( style )
{
case Style.Stretch :
key.SetValue(#"WallpaperStyle", "2") ;
key.SetValue(#"TileWallpaper", "0") ;
break;
case Style.Center :
key.SetValue(#"WallpaperStyle", "1") ;
key.SetValue(#"TileWallpaper", "0") ;
break;
case Style.Tile :
key.SetValue(#"WallpaperStyle", "1") ;
key.SetValue(#"TileWallpaper", "1") ;
break;
case Style.NoChange :
break;
}
key.Close();
}
}
}
"#
[Wallpaper.Setter]::SetWallpaper( '', 0 )
Originally from PoshCode: http://poshcode.org/491
This might sounf weird, but what worked for me was using single quotes instead of double. So it would look like this:
Set-ItemProperty -path "HKCU:Control Panel\Desktop" -name 'wallpaper' -value 'some value'
rundll32.exe user32.dll, UpdatePerUserSystemParameters
This script works wonders. For a domain deployment, we didn't want it to continuously change the background each time a user logs in.
I made the following changes so that it checks to see if the background exists on the computer in the desired location, if it does exist then to exit, if it does not to go ahead with the file copy and set the background.
it first maps the hidden share, copies the file to the desired directory, sets the wallpaper and then disconnects the hidden share. if "X" is already used by your company insert another drive letter. :D
$strFileName="C:\Users\Public\Pictures\background.jpg"
If (Test-Path $strFileName){
# // File exists
Exit-PSSession
}Else{
# // File does not exist
New-PSDrive -Name X -PSProvider Filesystem -Root \\hiddenfileshare\wallpapers
Copy-Item X:\background.jpg C:\Users\Public\Pictures
[Wallpaper.Setter]::SetWallpaper( 'C:\Users\Public\Pictures\background.jpg', 0 )
Remove-PSDrive X
}
The script provided by Andy Arismendi is awesome!
I've used it to make a fun project - set a random wallpaper scraped from the net.
I'm posting it here for anyone interested. Before using it you need to change a few constants at the top of the script source. You also need to download the HtmlAgilityPack.dll library (there are instructions in the script comments).
Enjoy!
P.S. If the wallpaper site I'm using goes down or changes its layout the scraping in the script will go to hell, but nevertheless with my script as an example I bet you'll be able to build another wallpaper scraper.
############## CONSTANTS ##############
# add the library for parsing html - HtmlAgilityPack - download it with nuget from https://www.nuget.org/packages/HtmlAgilityPack
# download nuget command line from https://dist.nuget.org/index.html and install HtmlAgilityPack with "nuget install HtmlAgilityPack" from the cmd
# enter the path to HtmlAgilityPack.dll library used for html parsing
$html_parser_path = "C:\Users\username\Documents\htmlagilitypack\HtmlAgilityPack.1.4.9.5\lib\Net20\HtmlAgilityPack.dll"
# choose where your wallpapers will be downloaded
$wallpaper_dir_path = "C:\Users\username\Pictures\"
# get random wallpaper category from wallpaperscraft.com - the ones below are my favourite categories, edit it if you want to get other categories
<#
you can choose your favorite wallpaper categories from the list below
3D
Abstract
Animals
Anime
Brands
Cars
City
Fantasy
Flowers
Food
Games
Girls
Hi-Tech
Holidays
Macro
Men
Movies
Music
Nature
Other
Space
Sport
Textures
TV Series
Vector
#>
$categories = #("animals","flowers","macro","nature","space")
# I download my wallpapers from the site below - real quality wallpapers
# don't forget to change your resolution - I'm using a 1920x1080 monitor
<#
A list of resolutions to choose from:
1600x1200
1400x1050
1280x1024
1280x960
1152x864
1024x768
3840x2400
3840x2160
3840x1200
2560x1600
2560x1440
2560x1080
2560x1024
2048x1152
1920x1200
1920x1080
1680x1050
1600x900
1440x900
1280x800
1280x720
2160x3840
1440x2560
1366x768
1080x1920
1024x600
960x544
800x1280
800x600
720x1280
540x960
480x854
480x800
400x480
360x640
320x480
320x240
240x400
240x320
2732x2732
2048x2048
1080x1920
1024x1024
750x1334
640x1136
640x960
320x480
1366x768
1920x1080
360x640
1024x768
1600x900
1280x900
1440x900
1280x1024
800x600
1680x1050
2560x1440
320x480
1920x1200
480x800
720x1280
#>
$resolution = "1920x1080" # default resolution
$url = "https://wallpaperscraft.com/catalog/category/$resolution" # category is a placeholder
############## END OF CONSTANT DECLARATIONS ##############
Add-Type #"
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;
namespace Wallpaper
{
public enum Style : int
{
Tile, Center, Stretch, NoChange
}
public class Setter {
public const int SetDesktopWallpaper = 20;
public const int UpdateIniFile = 0x01;
public const int SendWinIniChange = 0x02;
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int SystemParametersInfo (int uAction, int uParam, string lpvParam, int fuWinIni);
public static void SetWallpaper ( string path, Wallpaper.Style style ) {
SystemParametersInfo( SetDesktopWallpaper, 0, path, UpdateIniFile | SendWinIniChange );
RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", true);
switch( style )
{
case Style.Stretch :
key.SetValue(#"WallpaperStyle", "2") ;
key.SetValue(#"TileWallpaper", "0") ;
break;
case Style.Center :
key.SetValue(#"WallpaperStyle", "1") ;
key.SetValue(#"TileWallpaper", "0") ;
break;
case Style.Tile :
key.SetValue(#"WallpaperStyle", "1") ;
key.SetValue(#"TileWallpaper", "1") ;
break;
case Style.NoChange :
break;
}
key.Close();
}
}
}
"#
Add-Type -Path $html_parser_path
$rand_index = Get-Random -minimum 0 -maximum $categories.Length
$random_category = $categories[$rand_index]
# replace the placeholder "category" with the random category chosen above
$url = $url -replace "category", $random_category
$doc = New-Object HtmlAgilityPack.HtmlDocument
$doc.LoadHtml((New-Object System.Net.WebClient).DownloadString($url))
# NOTE: the html parser I'm using locates elements by XPath only
$page_links = $doc.DocumentNode.SelectSingleNode("//div[contains(#class, 'pages')]").SelectNodes("a")
# get last page link
$last_page_link = $page_links[$page_links.Count - 1].GetAttributeValue("href", "")
# get last page number
$last_page_number = [regex]::match($last_page_link,'.*page(\d+)').Groups[1].Value
$random_page_number = Get-Random -minimum 0 -maximum $last_page_number
$random_page_addr = ""
# page 1 doesn't add anything to the url
if ($random_page_number -gt 0){
$random_page_addr = "/page$random_page_number"
}
$doc.LoadHtml((New-Object System.Net.WebClient).DownloadString("$url$random_page_addr"))
# get wallpaper divs
$wallpaper_divs = $doc.DocumentNode.SelectNodes("//div[contains(#class, 'wallpaper_pre')]")
$random_wallpaper_div = Get-Random -minimum 0 -maximum 15 # there are 15 wallpapers on a page
# get a sample wallpaper link which has to be substituted later
$sample_wallpaper_link = $wallpaper_divs[$random_wallpaper_div].SelectNodes("a")[0].GetAttributeValue("href", "")
# substitute the above link to get the image link itself
$sample_wallpaper_link = $sample_wallpaper_link -replace "download", "image"
$sample_wallpaper_link = $sample_wallpaper_link -replace "/$resolution", "_$resolution.jpg"
$sample_wallpaper_link = $sample_wallpaper_link -replace "//", "https://"
$wallpaper_image_name = [regex]::match($sample_wallpaper_link,'.*image/(\w+)').Groups[1].Value
$wallpaper_image_name = "$wallpaper_image_name.jpg"
$wc = New-Object System.Net.WebClient
$save_location = "$wallpaper_dir_path$wallpaper_image_name"
$wc.DownloadFile($sample_wallpaper_link, "$save_location")
[Wallpaper.Setter]::SetWallpaper($save_location, 1 )
I'm working on a PoSh project that generates CSharp code, and then Add-Types it into memory.
The new types use existing types in an on disk DLL, which is loaded via Add-Type.
All is well and good untill I actualy try to invoke methods on the new types. Here's an example of what I'm doing:
$PWD = "."
rm -Force $PWD\TestClassOne*
$code = "
namespace TEST{
public class TestClassOne
{
public int DoNothing()
{
return 1;
}
}
}"
$code | Out-File tcone.cs
Add-Type -OutputAssembly $PWD\TestClassOne.dll -OutputType Library -Path $PWD\tcone.cs
Add-Type -Path $PWD\TestClassOne.dll
$a = New-Object TEST.TestClassOne
"Using TestClassOne"
$a.DoNothing()
"Compiling TestClassTwo"
Add-Type -Language CSharpVersion3 -TypeDefinition "
namespace TEST{
public class TestClassTwo
{
public int CallTestClassOne()
{
var a = new TEST.TestClassOne();
return a.DoNothing();
}
}
}" -ReferencedAssemblies $PWD\TestClassOne.dll
"OK"
$b = New-Object TEST.TestClassTwo
"Using TestClassTwo"
$b.CallTestClassOne()
Running the above script gives the following error on the last line:
Exception calling "CallTestClassOne" with "0" argument(s):
"Could not load file or assembly 'TestClassOne,...'
or one of its dependencies. The system cannot find the file specified."
At AddTypeTest.ps1:39 char:20
+ $b.CallTestClassOne <<<< ()
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
What am I doing wrong?
This happens because any assemblies are looked for by the CLR loader in the application's (PowerShell's) base directory. Of course, it doesn't find your assembly there. The best way to solve this is to hook the AssemblyResolve event as stej mentions but use it to tell the CLR where the assembly is. You can't do this with PowerShell 2.0's Register-ObjectEvent because it doesn't work with events that require a return value (ie the assembly). In this case, let's use more C# via Add-Type to do this work for us. This snippet of code works:
ri .\TestClassOne.dll -for -ea 0
$resolver = #'
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Utils
{
public static class AssemblyResolver
{
private static Dictionary<string, string> _assemblies;
static AssemblyResolver()
{
var comparer = StringComparer.CurrentCultureIgnoreCase;
_assemblies = new Dictionary<string,string>(comparer);
AppDomain.CurrentDomain.AssemblyResolve += ResolveHandler;
}
public static void AddAssemblyLocation(string path)
{
// This should be made threadsafe for production use
string name = Path.GetFileNameWithoutExtension(path);
_assemblies.Add(name, path);
}
private static Assembly ResolveHandler(object sender,
ResolveEventArgs args)
{
var assemblyName = new AssemblyName(args.Name);
if (_assemblies.ContainsKey(assemblyName.Name))
{
return Assembly.LoadFrom(_assemblies[assemblyName.Name]);
}
return null;
}
}
}
'#
Add-Type -TypeDefinition $resolver -Language CSharpVersion3
$code = #'
namespace TEST {
public class TestClassOne {
public int DoNothing() {
return 1;
}
}
}
'#
$code | Out-File tcone.cs
Add-Type -OutputAssembly TestClassOne.dll -OutputType Library -Path tcone.cs
# This is the key, register this assembly's location with our resolver utility
[Utils.AssemblyResolver]::AddAssemblyLocation("$pwd\TestClassOne.dll")
Add-Type -Language CSharpVersion3 `
-ReferencedAssemblies "$pwd\TestClassOne.dll" `
-TypeDefinition #'
namespace TEST {
public class TestClassTwo {
public int CallTestClassOne() {
var a = new TEST.TestClassOne();
return a.DoNothing();
}
}
}
'#
$b = new-object Test.TestClassTwo
$b.CallTestClassOne()
When you output the TestClassTwo to a dll (in the same directory as TestClassOne) and Add-Type it, it works. Or at least at my machine ;) So that's the ugly workaround.
When calling $b.CallTestClassOne() PowerShell tries (from some reason I don't know) to find assembly TestClassOne.dll at these locations:
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne.DLL
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne/TestClassOne.DLL
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne.EXE
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne/TestClassOne.EXE
This is output from fuslogvw tool. It might be useful for you. The same list of paths can bee seen live using ProcessMonitor.
You might also try this (before calling CallTestClassOne()
[appdomain]::CurrentDomain.add_assemblyResolve({
$global:x = $args
})
$b.CallTestClassOne()
$x | fl
This will show you what assembly failed and some more info.
I agree that it should work as you expect. So that's why this looks somewhat buggy.