I just got the Anniversary Update for Windows 10, and it took me a while to track down all the steps to actually enable this.. I had to hit several web pages, and some guesses, so I wanted to put a slightly more concise set of instructions on how to do this.

  1. Go into Add/Remove Programs in the Control Panel
    1. This can be opened with a quick command at the command line – appwiz.cpl
      appwiz
  2. Select Turn Windows features on or off
    TurnOnWindowsFeatures
  3. Select Windows Subsystem for Linux (beta)
    EnableLinux
  4. The installation will take a little time, and will probably require a reboot.
  5. After the reboot, log back in and click the Start button
  6. Type in “developer” and select the For developer settings
    DeveloperSettings
  7. Select Developer mode
    SelectDeveloperMode
  8. You can optionally select some other settings and click Apply in each section
    1. [Windows Explorer] Change policy to show Run as different user in Start
    2. [Remote Desktop] Change settings to allow remote connections to this computer
  9. Open up the command prompt, right click on the top left icon and select Properties
    CMDProperties
  10. Remove the checkbox for Use legacy console (requires relaunch)
    DisableLegacyMode
  11. Click OK, close the command prompt and relaunch it.
  12. Type in bash and hit enter.  You will be prompted to install the subsystem (it will take a few minutes.  I’ll update the screenshot for this later.

At this point, you’ll have bash available, and the system drive will be mounted under /mnt/c with full access to your filesystem.

David F.

I don’t normally post this kind of stuff.. but this was a little too good to pass up.

Microsoft is offering a huge list of free eBooks in various formats (pdf, mobi, docx, zip, ppt, ePub, xps) and the Microsoft Deployment Toolkits in both x86 & x64 formats.  A good number of them come in multiple formats so expect to have a lot of duplicates..

You can read about it here:
https://blogs.msdn.microsoft.com/mssmallbiz/2016/07/10/free-thats-right-im-giving-away-millions-of-free-microsoft-ebooks-again-including-windows-10-office-365-office-2016-power-bi-azure-windows-8-1-office-2013-sharepoint-2016-sha/

The list of links to get each item is here, although I’m not sure which one is which. I’ve made a few attempts to try and script something that would tell me which one was which (none off them worked) and I did manage to sort of write a script that would open up IE to each of the links sequentially for download purposes.  My goal was to try and control the number of instances using powershell jobs, but that mostly failed. I got them to open up in batches, but they just kept opening & opening & opening etc..  I was able to click through and save everything after the massive numbers of IE windows opened.

List of Links
http://ligman.me/29ngkYn http://ligman.me/29jL5wW http://ligman.me/29afIRV
http://ligman.me/29ngkYn http://ligman.me/29pyHgR http://ligman.me/29dmbfC
http://ligman.me/29ollRF http://ligman.me/29pyHgR http://ligman.me/29gvv67
http://ligman.me/29gvv67 http://ligman.me/29pzkHg http://ligman.me/29CWQ20
http://ligman.me/29CWQ20 http://ligman.me/1G0Cm7T http://ligman.me/1G0Cm7T
http://ligman.me/29dmbfC http://ligman.me/29ollRF http://ligman.me/29vDVrw
http://ligman.me/29CW4SV http://ligman.me/29CW4SV http://ligman.me/29G1z1N
http://ligman.me/29yWAAO http://ligman.me/29DHWHq http://ligman.me/29DHWHq
http://ligman.me/1HGwMgm http://ligman.me/1HGwMgm http://ligman.me/29yUVeH
http://ligman.me/29yUVeH http://ligman.me/29ErxWs http://ligman.me/29ErxWs
http://ligman.me/29EsHkr http://ligman.me/29EsHkr http://ligman.me/29v5zCF
http://ligman.me/29v5zCF http://ligman.me/1H32nUT http://ligman.me/1H32nUT
http://ligman.me/29yixk9 http://ligman.me/29yixk9 http://ligman.me/29vq6d2
http://ligman.me/29vq6d2 http://ligman.me/29cvrjG http://ligman.me/29pzjTR
http://ligman.me/29rPOf6 http://ligman.me/29cvrjG http://ligman.me/29ctW9z
http://ligman.me/29gvoHt http://ligman.me/29gvfEd http://ligman.me/29ctW9z
http://ligman.me/29LvcBg http://ligman.me/29DS3gJ http://ligman.me/1q9L65I
http://ligman.me/1q9L65I http://ligman.me/29pufLO http://ligman.me/29pufLO
http://ligman.me/29wODdA http://ligman.me/29wODdA http://ligman.me/29vch9q
http://ligman.me/29vch9q http://ligman.me/29EmfZc http://ligman.me/29EmfZc
http://ligman.me/29LXmMt http://ligman.me/29LXmMt http://ligman.me/29qwKfQ
http://ligman.me/29qwKfQ http://ligman.me/29sp3nZ http://ligman.me/29sp3nZ
http://ligman.me/29yWFFf http://ligman.me/29yWFFf http://ligman.me/29ZNaMN
http://ligman.me/29ZNaMN http://ligman.me/29gYy5h http://ligman.me/29gYy5h
http://ligman.me/29NhudA http://ligman.me/29jDRJf http://ligman.me/29conIf
http://ligman.me/29cnLlL http://ligman.me/29jDRJf http://ligman.me/29fNYRE
http://ligman.me/29cq9cw http://ligman.me/29acqhj http://ligman.me/29fJJCS
http://ligman.me/29nbxWK http://ligman.me/29fO9MV http://ligman.me/29fJJCS
http://ligman.me/29pr2x1 http://ligman.me/29pr2x1 http://ligman.me/29oa0kH
http://ligman.me/29i6ntm http://ligman.me/29n7JVF http://ligman.me/29fQsQ4
http://ligman.me/29fQo2J http://ligman.me/29idz8T http://ligman.me/29fQsQ4
http://ligman.me/29dfwlu http://ligman.me/29fLliP http://ligman.me/29i83TI
http://ligman.me/29diypX http://ligman.me/29fNpHO http://ligman.me/29oefNf
http://ligman.me/29diypX http://ligman.me/29jGTgC http://ligman.me/29crPhL
http://ligman.me/29FUJIk http://ligman.me/29jGTgC http://ligman.me/1HGwMgm
http://ligman.me/1HGwMgm http://ligman.me/1G2oDNG http://ligman.me/1G2oDNG
http://ligman.me/29u5uMT http://ligman.me/29u5uMT http://ligman.me/29rlRt3
http://ligman.me/29rlRt3 http://ligman.me/29Yd9V0 http://ligman.me/29Yd9V0
http://ligman.me/29s4EQ1 http://ligman.me/29s4EQ1 http://ligman.me/29vkv6E
http://ligman.me/29vkv6E http://ligman.me/29A3q8Z http://ligman.me/29A3q8Z
http://ligman.me/29H7ovO http://ligman.me/29H7ovO http://ligman.me/1dHMOui
http://ligman.me/1dHMOui http://ligman.me/1G2pw8X http://ligman.me/1G2pw8X
http://ligman.me/29gnaPK http://ligman.me/29d7GZH http://ligman.me/29i2Mvf
http://ligman.me/29rbpkY http://ligman.me/29rbpkY http://ligman.me/29e6cTO
http://ligman.me/29e6cTO http://ligman.me/29hJU1c http://ligman.me/29bcJtd
http://ligman.me/29bcJtd http://ligman.me/29bcRZO http://ligman.me/29d5mS2
http://ligman.me/29d5mS2 http://ligman.me/29cz6OD http://ligman.me/29fDult
http://ligman.me/29fDult http://ligman.me/29pEdjN http://ligman.me/29rwoY0
http://ligman.me/29rwoY0 http://ligman.me/29fTQrx http://ligman.me/29gfrkN
http://ligman.me/29gfrkN http://ligman.me/29rwoHw http://ligman.me/29rwoHw
http://ligman.me/29d5AbX http://ligman.me/29d5AbX http://ligman.me/29fudHc
http://ligman.me/29fudHc http://ligman.me/29FzGG5 http://ligman.me/29FzGG5
http://ligman.me/29osNwd http://ligman.me/29a22WR http://ligman.me/29a22WR
http://ligman.me/29alDpK http://ligman.me/29fDylj http://ligman.me/29fDylj
http://ligman.me/29jPTCx http://ligman.me/29d5XTw http://ligman.me/29d5XTw
http://ligman.me/29rUIci http://ligman.me/29hVdVa http://ligman.me/29hVdVa
http://ligman.me/29ijDhm http://ligman.me/29pn5e2 http://ligman.me/29pn5e2
http://ligman.me/29ckVx7 http://ligman.me/29ckVx7 http://ligman.me/29cfePX
http://ligman.me/29cfePX http://ligman.me/29vGIvY http://ligman.me/29vGIvY
http://ligman.me/29hfnPR http://ligman.me/29hfnPR http://ligman.me/29fCVFe
http://ligman.me/29fCVFe http://ligman.me/29a6YLu http://ligman.me/29a6YLu
http://ligman.me/29n4GwJ http://ligman.me/29n4GwJ http://ligman.me/29jAS3v
http://ligman.me/29jAS3v http://ligman.me/29ci56U http://ligman.me/29ci56U
http://ligman.me/29nW9L3 http://ligman.me/29nW9L3 http://ligman.me/29hV6ZM
http://ligman.me/29hV6ZM http://ligman.me/29d1qAV http://ligman.me/29d1qAV
http://ligman.me/29pfpbI http://ligman.me/29pfpbI http://ligman.me/29v8nwX
http://ligman.me/29v8nwX http://ligman.me/29uq452 http://ligman.me/29uq452
http://ligman.me/29d1wc3 http://ligman.me/29d1wc3 http://ligman.me/29Y1O7a
http://ligman.me/29Y1O7a http://ligman.me/29a7wRA http://ligman.me/29pnEEG
http://ligman.me/1FYtDD8 http://ligman.me/1FYtDD8 http://ligman.me/1HByNKS
http://ligman.me/1HByNKS http://ligman.me/1NCfcKC http://ligman.me/1NCfcKC
http://ligman.me/1HCDxl9 http://ligman.me/1HCDxl9 http://ligman.me/1HCCCRP
http://ligman.me/1HCCCRP http://ligman.me/1H4Q0e5 http://ligman.me/1H4Q0e5
http://ligman.me/1JI6V77 http://ligman.me/1JI6V77 http://ligman.me/1CSMobd
http://ligman.me/1CSMobd http://ligman.me/1jWMJA2 http://ligman.me/1jWMJA2
http://ligman.me/1m6xucg http://ligman.me/1m6xucg http://ligman.me/1onTg9n
http://ligman.me/1onTg9n http://ligman.me/1n49kzj http://ligman.me/1n49kzj
http://ligman.me/1sgBtn4 http://ligman.me/1sgBtn4 http://ligman.me/1qZlnOJ
http://ligman.me/1qZlnOJ http://ligman.me/TWa2Dg http://ligman.me/TWa2Dg
http://ligman.me/1vM9mwt http://ligman.me/1vM9mwt http://ligman.me/1qzON6Q
http://ligman.me/1qzON6Q http://ligman.me/1rB8nl1 http://ligman.me/1rB8nl1
http://ligman.me/TL3pn1 http://ligman.me/TL3pn1 http://ligman.me/1vM9H2d
http://ligman.me/1vM9H2d http://ligman.me/29odbJ6 http://ligman.me/1LSKTC0
http://ligman.me/1LSKTC0 http://ligman.me/1qC1pu4 http://ligman.me/1qC1pu4
http://ligman.me/1dHSpRh http://ligman.me/1dHSpRh http://ligman.me/1LO5k1Y
http://ligman.me/1LO5k1Y http://ligman.me/1M7Xr5v http://ligman.me/1M7Xr5v
http://ligman.me/29jLNtX http://ligman.me/29jLNtX http://ligman.me/29agpuw
http://ligman.me/29cv8FE http://ligman.me/29cv8FE http://ligman.me/29ieXIo
http://ligman.me/29dmCXi http://ligman.me/29jLjnM http://ligman.me/29jLjnM
http://ligman.me/29agkqx http://ligman.me/29gvTBJ http://ligman.me/29pztuz
http://ligman.me/29pztuz http://ligman.me/29dmTJT http://ligman.me/29ieSEI
http://ligman.me/29ieSEI http://ligman.me/29hB9CQ http://ligman.me/29fOWdV
http://ligman.me/1JPNIAt http://ligman.me/1JPNIAt http://ligman.me/29Xnqk8
http://ligman.me/29Xnqk8 http://ligman.me/29oRWlp http://ligman.me/29csX4C
http://ligman.me/29jH8bx http://ligman.me/29pcQFA http://ligman.me/29pcQFA
http://ligman.me/29FkNr3 http://ligman.me/29FkNr3 http://ligman.me/12FIapt
http://ligman.me/12FIapt http://ligman.me/13WvGXa http://ligman.me/1bPPb6C
http://ligman.me/12FIZ1I http://ligman.me/12FIZ1I http://ligman.me/16CaDM1
http://ligman.me/19LwMLI http://ligman.me/1JHmqiB http://ligman.me/1JHmqiB
http://ligman.me/1KHqGNK http://ligman.me/1KHqGNK http://ligman.me/1M7Ycve
http://ligman.me/1M7Ycve http://ligman.me/1M7Ycve http://ligman.me/1LSOsIu
http://ligman.me/1LSOsIu http://ligman.me/1UrQDFx http://ligman.me/1UrQDFx
http://ligman.me/TUmyTW http://ligman.me/TUmyTW http://ligman.me/1NLviCk
http://ligman.me/1NLviCk http://ligman.me/17iaq4l http://ligman.me/17iaq4l
http://ligman.me/1bPRqqz http://ligman.me/17iah0Q http://ligman.me/1287Jt4
http://ligman.me/29djdrk http://ligman.me/29rLH2H http://ligman.me/29fNPxT
http://ligman.me/29djdrk http://ligman.me/29ddm60 http://ligman.me/29gsnHt
http://ligman.me/29nc2QS http://ligman.me/29fOrnd http://ligman.me/29dk0Ze
http://ligman.me/29aewh2 http://ligman.me/29fOrnd http://ligman.me/29gteI8
http://ligman.me/29gteI8 http://ligman.me/29ibJVq http://ligman.me/29fKMD6
http://ligman.me/29cthAz http://ligman.me/29cthAz http://ligman.me/29ohqUT
http://ligman.me/29crGiw http://ligman.me/29gsFOl http://ligman.me/29ncgrb
http://ligman.me/29fKWdi http://ligman.me/29gsFOl http://ligman.me/29djvi9
http://ligman.me/29FW41O http://ligman.me/29ddKBp http://ligman.me/29djvi9
http://ligman.me/29dkhf4 http://ligman.me/29dkhf4 http://ligman.me/29rML6Q
http://ligman.me/29hxyEN http://ligman.me/29fPweO http://ligman.me/29fPMug
http://ligman.me/29fPnIe http://ligman.me/29fPweO http://ligman.me/29hxogT
http://ligman.me/29hxogT http://ligman.me/29deg2k http://ligman.me/29pwCBO
http://ligman.me/29fPAec http://ligman.me/29fPAec http://ligman.me/29deQgz
http://ligman.me/29afaLE http://ligman.me/29pxarl http://ligman.me/29pxarl
http://ligman.me/29ne5V5 http://ligman.me/29csBjh http://ligman.me/29dkZZK
http://ligman.me/29dkZZK http://ligman.me/29ojrAy http://ligman.me/29jJLdx
http://ligman.me/29df2fw http://ligman.me/29df2fw http://ligman.me/29cu6sX
http://ligman.me/29dlaE5 http://ligman.me/29FZx0B http://ligman.me/29FZx0B
http://ligman.me/29idnpO http://ligman.me/29dlH9d http://ligman.me/29cs157
http://ligman.me/29cs157 http://ligman.me/29dkq1V http://ligman.me/29aelm3
http://ligman.me/29ycEnb http://ligman.me/29ycEnb http://ligman.me/29w5gr9
http://ligman.me/29w5gr9 http://ligman.me/29pfQ50 http://ligman.me/29pfQ50
http://ligman.me/1sgMWDe http://ligman.me/1sgMWDe http://ligman.me/1sgMWDe
http://ligman.me/29w68w8 http://ligman.me/29w68w8 http://ligman.me/29viFmi
http://ligman.me/29viFmi http://ligman.me/29zYWzl http://ligman.me/29zYWzl
http://ligman.me/29H2bo1 http://ligman.me/29H2bo1 http://ligman.me/29AT60J
http://ligman.me/29AT60J http://ligman.me/29xtlNa http://ligman.me/29xtlNa
http://ligman.me/29xtHDs http://ligman.me/29xtHDs http://ligman.me/29GpFsY
http://ligman.me/29GpFsY http://ligman.me/29ddd2b http://ligman.me/29fJpEc
http://ligman.me/29cs7Fg http://ligman.me/29ddd2b http://ligman.me/29fJA2g
http://ligman.me/29fJA2g http://ligman.me/29fJA2g http://ligman.me/29cqzzq
http://ligman.me/29ddbYb http://ligman.me/29ddbYb http://ligman.me/29rLlZV
http://ligman.me/29ialCf http://ligman.me/29pvj5I http://ligman.me/29pvj5I
http://ligman.me/29hwj8y http://ligman.me/29dj7zY http://ligman.me/1sl39Hs
http://ligman.me/1sl39Hs http://ligman.me/1anyEJj http://ligman.me/1anyEJj
http://ligman.me/17icbPc http://ligman.me/ZZezok http://ligman.me/12S035G
http://ligman.me/12S035G http://ligman.me/12RZWY1 http://ligman.me/13PlvVY
http://ligman.me/12FMEMP http://ligman.me/12FMEMP http://ligman.me/128a1ID
http://ligman.me/19LCgpM http://ligman.me/13Pn1XY http://ligman.me/13Pn1XY
http://ligman.me/13WChkr http://ligman.me/12FN71F http://ligman.me/ZZh7Ts
http://ligman.me/ZZh7Ts http://ligman.me/14HcD5O http://ligman.me/17UHSNJ
http://ligman.me/19LEPIz http://ligman.me/19LEPIz http://ligman.me/11VIxdB
http://ligman.me/12FObmf http://ligman.me/11HXnjD http://ligman.me/11HXnjD
http://ligman.me/14fCxLS http://ligman.me/16CkUI4 http://ligman.me/17VqB79
http://ligman.me/17VqB79 http://ligman.me/13XRfqr http://ligman.me/19fCnqV
http://ligman.me/11WvSqL http://ligman.me/11WvSqL http://ligman.me/1002Upx
http://ligman.me/14HMJ1Q http://ligman.me/10ttQ3n http://ligman.me/10ttQ3n
http://ligman.me/15fRBI9 http://ligman.me/19fDuqE http://ligman.me/11IMDlh
http://ligman.me/11IMDlh http://ligman.me/10tu5eD http://ligman.me/128NAD6
http://ligman.me/1bRgXzV http://ligman.me/1bRgXzV http://ligman.me/14givRo
http://ligman.me/18VNatF http://ligman.me/128Ogso http://ligman.me/128Ogso
http://ligman.me/13Qbl7k http://ligman.me/10tuWvL http://ligman.me/16dFAWc
http://ligman.me/16dFAWc http://ligman.me/11lkViX http://ligman.me/1bRhVMn
http://ligman.me/14HOjki http://ligman.me/14HOjki http://ligman.me/1bRiXbc
http://ligman.me/13QcfRi http://ligman.me/16E4lf4 http://ligman.me/16E4lf4
http://ligman.me/1005lbB http://ligman.me/14HOxb2 http://ligman.me/16E6G9L
http://ligman.me/16E6G9L http://ligman.me/14gkBRc http://ligman.me/1006fEV
http://ligman.me/16dHXZ9 http://ligman.me/16dHXZ9 http://ligman.me/13QdHDb
http://ligman.me/11IQ8bd http://ligman.me/17jgPfG http://ligman.me/17jgPfG
http://ligman.me/15fXTHQ http://ligman.me/11vx5Cy http://ligman.me/N1JiHO
http://ligman.me/OudHlO http://ligman.me/OudJdr http://ligman.me/N1I2o4
http://ligman.me/N1I2o4 http://ligman.me/Oue0NG http://ligman.me/Oue2oE
http://ligman.me/N1HQW0 http://ligman.me/N1HQW0 http://ligman.me/Ouebsh
http://ligman.me/OuecfK http://ligman.me/N1Ienp http://ligman.me/N1Ienp
http://ligman.me/OuelQu http://ligman.me/OueoMd http://ligman.me/N1J8A8
http://ligman.me/N1J8A8 http://ligman.me/OueFPb http://ligman.me/OueIKU
http://ligman.me/N1I935 http://ligman.me/N1I935 http://ligman.me/OueUd4
http://ligman.me/OueVxy http://ligman.me/N1HMW7 http://ligman.me/N1HMW7
http://ligman.me/Ouf6sO http://ligman.me/Ouf9og http://ligman.me/N1Jo27
http://ligman.me/N1Jo27 http://ligman.me/OufgQN http://ligman.me/OufkQs
http://ligman.me/N1HEpM http://ligman.me/N1HEpM http://ligman.me/Oufwzg
http://ligman.me/OufCXN http://ligman.me/N1JfvI http://ligman.me/N1JfvI
http://ligman.me/OufRSs http://ligman.me/OufVlq http://ligman.me/N1HX3Q
http://ligman.me/N1HX3Q http://ligman.me/Oug5Jl http://ligman.me/Oug74a
http://ligman.me/1H1Exty http://ligman.me/1H1Exty http://ligman.me/1S1f34H
http://ligman.me/1S1f34H http://ligman.me/1HGqihD http://ligman.me/1HGqihD
http://ligman.me/1G2ccS5 http://ligman.me/1G2ccS5 http://ligman.me/1ffeiJo
http://ligman.me/1ffeiJo http://ligman.me/1NKjUqp http://ligman.me/1NKjUqp
http://ligman.me/1KEShAt http://ligman.me/1KEShAt http://ligman.me/1S1i4C0
http://ligman.me/1S1i4C0 http://ligman.me/1ReR3Qq http://ligman.me/1ReR3Qq
http://ligman.me/1dGxnSW http://ligman.me/1dGxnSW http://ligman.me/1IZCarE
http://ligman.me/1IZCarE http://ligman.me/1H2Bq3J http://ligman.me/1H2Bq3J
http://ligman.me/1Rf7BaZ http://ligman.me/1Rf7BaZ http://ligman.me/1LRIveQ
http://ligman.me/1LRIveQ http://ligman.me/1dGxEW7 http://ligman.me/1dGxEW7
http://ligman.me/1omCrM6 http://ligman.me/1omCrM6 http://ligman.me/1j5aDhH
http://ligman.me/1j5aDhH http://ligman.me/1n3mkVY http://ligman.me/1n3mkVY
http://ligman.me/1n3mAUZ http://ligman.me/1n3mAUZ http://ligman.me/1vKOGot
http://ligman.me/1vKOGot http://ligman.me/1H7bxTv http://ligman.me/1H7bxTv
http://ligman.me/1G0DEjb http://ligman.me/1G0DEjb http://ligman.me/1qC1pu4
http://ligman.me/29pbiLY http://ligman.me/29pbiLY http://ligman.me/29dlTVV
http://ligman.me/29rOYz9 http://ligman.me/29ie2rq http://ligman.me/29dlTVV
http://ligman.me/29fQN5c http://ligman.me/29idEct http://ligman.me/29nevuE
http://ligman.me/29fQN5c http://ligman.me/29olaWI http://ligman.me/29olaWI
http://ligman.me/29pz4Im http://ligman.me/29fQRlQ http://ligman.me/29hzPzU
http://ligman.me/29rON6X http://ligman.me/29cumbH http://ligman.me/29hzPzU
http://ligman.me/29cv1JW http://ligman.me/29dmh6L http://ligman.me/29dfUkt
http://ligman.me/29cv1JW http://ligman.me/1giniO7 http://ligman.me/1giniO7
http://ligman.me/29H7K5O http://ligman.me/29H7K5O http://ligman.me/1qbEeVc
http://ligman.me/1qbEeVc http://ligman.me/1ewwcq6 http://ligman.me/1ewwcq6
http://ligman.me/1H1MFKr http://ligman.me/1H1MFKr http://ligman.me/1pxniH4
http://ligman.me/1pxniH4 http://ligman.me/1dG2ZZ9 http://ligman.me/1dG2ZZ9
http://ligman.me/29v9igV http://ligman.me/29hofVk http://ligman.me/29v9igV
http://ligman.me/29idcee http://ligman.me/29idcee http://ligman.me/29rOvx0
http://ligman.me/29ctkki http://ligman.me/29ctkki http://ligman.me/29okalt
http://ligman.me/29hofVk http://ligman.me/29i2CEe http://ligman.me/29FT71n

In part 1, I started building my advanced function for setting registry values on both the local machine, or a remote machine using the built-in .net methods.  Using the PSDrive called HKLM: or HKCU: both have significant limitations (like not being able to access a remote machine, and that registry values are considered ItemProperties of the registry key which is an Item.  There are other limitations also, but these are a substantial hassle for me, so I try to avoid using them).

The rest of the main body of the code looks like this:

Write-Debug -Message "Entering foreach (computer in computername) loop"
ForEach ($Computer in $ComputerName)
{
	Write-Debug -Message "ForEach ($Computer in ComputerName) pass"
	try
	{
		$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Hive, $Computer)
		Write-Debug -Message "Successfully able to open $hive on $computer"
	}
	catch
	{
		Write-Error -Message "Unable to open remote registry hive for $Computer. Please verify connectivity and permissions to retry"
		continue
	}
	Write-Debug -Message "Entering foreach loop to create full registry path if $force is enabled"
	foreach ($SubKey in $RegKeyPath.Split('\'))
	{
		$RegName = $Reg.Name
		Write-Debug -Message "SubKey is $subkey, and current registry path is $RegName"
		if ($Reg.GetSubKeyNames -contains $SubKey)
		{
			Write-Debug "\$Reg contains $SubKey, opening that key"
			$Reg = $Reg.OpenSubKey($SubKey)
		}
		else
		{
			Write-Debug -Message "\$Reg does not contain $subkey, checking for Force"
			if ($Force)
			{
				Write-Debug -Message "\$Force enabled, continuing"
				try
				{
				Write-Debug -Message "Trying to create SubKey ($SubKey)"
				$Reg = $Reg.CreateSubKey($SubKey)
				}
				catch
				{
					$Reg.Close()
					Write-Error -Message "Unable to create $subkey in $RegName, please check your permissions or the Remote Registry service may not be running" -ErrorAction 'Stop'
				}
			}
			else
			{
				$Reg.Close()
				Write-Error -Message "Force not specified, and subkey ($subkey) does not exist in $RegName" -ErrorAction 'Stop'
			}
		}
	}
	Write-Debug -Message "Registry either exists, or has been created. Proceeding with setting the value"
		try
		{
			$Reg.SetValue($ValueName, $ValueData, $ValueType)
			Write-Debug -Message "Succesfully set the value $ValueName on $Computer"
		}
		catch
		{
			Write-Debug -Message "Unable to set the value $ValueName in $RegName on $Computer; Please validate your access & permissions and try again"
		}
		Finally
		{
			$Reg.Close()
		}
	}
}

The primary block of code is the

ForEach ($Computer in $ComputerName) {
}

This is a critical component of the advanced function. Since we declared that the $ComputerName parameter could contain multiple parameters from the pipeline, this construction allows the script to automatically process each of the passed computer objects.

The next block is where we attempt to open the registry hive on the remote computer

Write-Debug -Message "ForEach ($Computer in ComputerName) pass"
try
{
	$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Hive, $Computer)
	Write-Debug -Message "Successfully able to open $hive on $computer"
}
catch
{
	Write-Error -Message "Unable to open remote registry hive for $Computer. Please verify connectivity and permissions to retry"
	continue
}

I use the Write-Debug lines to provide inline comments if the script is run using the -debug parameter (thanks to Don Jones). The other option is to use write-verbose which is another text “stream” that can be used to provide inline comments. Typically, if you had a comment for your code that you would use in another language (cmd, vbscript, etc.) then write-verbose is your alternative. Write-debug is useful for “soft” errors that don’t necessarily need to stop your script and won’t necessarily have a huge effect.

The other major construction in this piece of code is the Try/Catch loops. Many other languages have the same type of error handling, but in my background (cmd, vbscript, kix, winbatch, etc.) that construction didn’t necessarily exist. This allows us to try a piece of code to see if it works, and then if it does *not* work, then we expect and error and we catch it. In the catch block, we provide whatever code we think is necessary. In my catch block, I am first writing a non-terminating error with write-error. I also use a continue statement to “break” out of the foreach loop and move on to the next computer.

The next section starts the actual registry processing..

Write-Debug -Message "Entering foreach loop to create full registry path if $force is enabled"
	foreach ($SubKey in $RegKeyPath.Split('\'))
	{
		$RegName = $Reg.Name
		Write-Debug -Message "SubKey is $subkey, and current registry path is $RegName"

First, is our inline comment explaining what is happening, and then we start a foreach loop. In the loop we are getting each element of the path specified since Split breaks it along the backslash dividers. We set $RegName equal to the name of the registry item. This sets RegName to the actual string of the path, and of course the inline comment.

Next is the section to test the path. We use the registry object’s method for GetSubKeyNmes which gives you an array of all the subkey names of the provided object, and then we use the standard -contains function to see if the subkey name from the foreach loop. If it does contain it, then we change the $Reg object to be the actual subkey that we found. This is the meat of the recursive part; with each round of the loop, we look at the registry key and if it is there, we set it one layer deeper.

if ($Reg.GetSubKeyNames -contains $SubKey)
	{
		Write-Debug "\$Reg contains $SubKey, opening that key"
		$Reg = $Reg.OpenSubKey($SubKey)
	}
	else
	{

Now, if the $Reg.GetSubKeyNames does not contain the $Subkey, then we perform the next piece of code.

Write-Debug -Message "\$Reg does not contain $subkey, checking for Force"
			if ($Force)
			{
				Write-Debug -Message "\$Force enabled, continuing"
				try
				{
				Write-Debug -Message "Trying to create SubKey ($SubKey)"
				$Reg = $Reg.CreateSubKey($SubKey)
				}
				catch
				{
					$Reg.Close()
					Write-Error -Message "Unable to create $subkey in $RegName, please check your permissions or the Remote Registry service may not be running" -ErrorAction 'Stop'
				}

In this piece of code, we check to see if the $Force switch is present which tells us to go ahead and create the path “piece” because it is missing. So, if $Force is there, we try to create the piece and then if it fails, we catch the error. And of course, with an error we follow the proper programming practice and Close() the registry key, and then write out an error indicating that there was a problem. The text message includes the most likely causes of the issue.
This takes us to the next piece of code, which is the else section of the main “if” block.

else
			{
				$Reg.Close()
				Write-Error -Message "Force not specified, and subkey ($subkey) does not exist in $RegName" -ErrorAction 'Stop'
			}
		}
	}

By closing out the registry section, we wrap up the loop to finish our recursive piece. In pseudo code, we are doing this:


<pre>for each piece of the path
     Open the registry key
          Did it open?
               If yes, then set the key to this level
               If no, then are we set to force create it?
                    If yes, then create it and set the key to this level
                    If no, then write out and error and close the registry level
go to top of the loop

Then the last portion of the main body of code is this piece:

Write-Debug -Message "Registry either exists, or has been created. Proceeding with setting the value"
		try
		{
			$Reg.SetValue($ValueName, $ValueData, $ValueType)
			Write-Debug -Message "Succesfully set the value $ValueName on $Computer"
		}
		catch
		{
			Write-Debug -Message "Unable to set the value $ValueName in $RegName on $Computer; Please validate your access &amp; permissions and try again"
		}
		Finally
		{
			$Reg.Close()
		}
	}
}

We try to set the registry value that we specified at the beginning of the function. If we can set the value, we write the information that we can, if we can’t then we write out a debug message indicating this, and then the Finally block follows the proper programming practices and closes the registry key (we don’t want memory leaks). And because we are accepting multiple computer names, the entire script will be repeated for each of the computers provided. I hope everyone finds this useful. This is now one of my standard “toolkit” scripts.

And here is a link to a PDF containing the script (it has to be a PDF because WordPress doesn’t like plain text documents as far as I can tell).
set-drfremoteregistry.pdf

Till next time,

David

I know it has been several months since my last post, but I’ve been steadily increasing my powershell knowledge, and creating scripts that I can’t make public.

Recently, I had a need to make some registry changes by script. I found tons of examples on how to do this, however, none of them really seemed to address what happens if the registry key is not there.  Eventually, I did find one that did, however, the way it was written, it only tolerated 1 level missing. So, if you needed to create a registry path several levels deep that didn’t exist, it did not provide a way to do this.

This script is going to be a reusable tool that I will put in my toolkit, so the first few lines are my “typical” advanced function lines.

function Set-DRFRemoteRegistry
{
	[CmdletBinding()]
	param
	(
		[Parameter(Position = 0, Mandatory = $false, ValueFromPipeline=True, ValueFromPipelinebyName=$true)]
		[Alias("CN")]
		[string[]]$ComputerName = $Env:COMPUTERNAME,

		[Parameter(Position = 1, Mandatory = $true)]
		[ValidateSet("ClassesRoot", "CurrentConfig", "CurrentUser", "DynData", "LocalMachine", "PerformanceData", "Users")]
		[string]$Hive = "LocalMachine",

		[Parameter(Position = 2, Mandatory = $true)]
		[string]$RegKeyPath,

		[Parameter(Position = 3, Mandatory = $true)]
		[string]$ValueName,

		[Parameter(Position = 4, Mandatory = $true)]
		$ValueData,

		[Parameter(Position = 5, Mandatory = $true)]
		[ValidateSet("String", "ExpandString", "Binary", "DWord", "MultiString", "QWord", "Default")]
		[string]$ValueType,

		[Parameter(Position = 6, Mandatory = $false)]
		[Switch]$Force
	)

Most of this is pretty self explanatory.  I’m defining the function, using an approved verb set, and the noun starts with my initials to keep it unique – drf, and a descriptive name – remoteregistry.  And then of course the open brace and then the [CmdletBinding()].  CmdLetBinding tells powershell that the function should act like part of the shell, and to support some of the common parameters (Debug,ErrorAction,ErrorVariable,OutVariable,OutBuffer,Verbose,WarningAction,WarningVariable).  http://msdn.microsoft.com/en-us/library/dd901844(v=vs.85).aspx  CmdletBinding has some parameters you can put inside the parenthesis, but I am not covering those here.

The next part is to setup my parameters.  Since I’m creating registry values.. I need several pieces of information

  • What Computer(s)?
  • What Registry Hive?
  • What Registry Path?
  • What Registry Value name?
  • What’s the data in the Value?
  • What’s the data type for that Value?
  • Do I need to build the entire path? (the real focus of this article)

Inside the Param( ) set, each parameter consists of a [Parameter()] tag with options inside the ( ) marks, a variable name for the parameter.

And here is the list of parameters inside of the Param( ) block

[Parameter(Position = 0, Mandatory = $false, ValueFromPipeline=True, ValueFromPipelinebyName=$true)]
[Alias("CN")]
[string[]]$ComputerName = $Env:COMPUTERNAME,

The first parameter is the name of the computer to apply the registry value to.  The first option is the Position parameter.  This tells Powershell that if the function is run and the parameter names are not specfied, then the first position (0) is going to be the ComputerName.  It’s typically bad form to not specify the parameter names, but in a pinch, this lets you do that.  Without it, the function will ignore that parameter.

The second option is Mandatory.  This option is pretty self-explanatory.  If a Mandatory = $true is specified, then the function will not run if that parameter is provided – it will prompt the user to provide a value if it is not provided.

The next two options ValueFromPipeline and ValueFromPipelineByPropertyName. allow the function to accept input from the pipeline.  They are different, and the difference is subtle.  I’d love to say I understand what the real difference is, but I don’t.

On the next line, I’m setting a .Net PropertyType accelerator – [string].  When you use this, then the variable immediately following it is forced to be a string type.  But, notice the interior brackets – [string[]].  These brackets tell powershell that it should expect to have multiple values for that particular parameter.  It’s also called an accelerator because it allows direct access to any of the .net properties of a string.

Next, we have another bracketed option – [Alias(“CN”)].  This line tells powershell that you have defined an alias to use inline for your named parameter.  So, instead of using the full variable name ($ComputerName), you can simply use -CN.  You can have mutliple aliases separated by commas inside the parenthesis.

Then we have the variable name ($ComputerName in this case), followed by an equals sign and a value.  This assignment (=) tells it that if the parameter is not supplied by the user, then this should be assumed to be the default. So, we are telling it that the $ComputerName should default to the local computername (by environment variable) if one is not supplied.

The next parameter is structured very similarly:

[Parameter(Position = 1, Mandatory = $true)]
[ValidateSet("ClassesRoot", "CurrentConfig", "CurrentUser", "DynData", "LocalMachine", "PerformanceData", "Users")]
[string]$Hive = "LocalMachine",

It is very similar to the first parameter, except we gave it the next position, and we use [ValidateSet()].

  • [ValidateSet()] This option allows you to force the parameter to only accept a value from your comma separated list.  It also gives the ability for powershell to know what the acceptable values are, so that when you type in the command, and enter the parameter, you can hit Tab and have the value filled in.  Powershell editors also use this to provide a form of intellisense.

We’ve pre-supplied the list of values that can be accepted.  These values are the names of the registry hives that are used by several .Net functions.  Most of the value names are pretty straight forward, but just in case:

  • “ClassesRoot” – this represents HKEY_CLASSES_ROOT
  • “CurrentConfig” – this represents HKEY_CURRENT_CONFIG
  • “CurrentUser” – this represents HKEY_CURRENT_USER
  • “DynData” – this represents HKEY_DYNAMIC_DATA
  • “LocalMachine” – this represents HKEY_LOCAL_MACHINE
  • “PerformanceData – this represents HKEY_PERFORMANCE_DATA
  • “Users” – this represents HKEY_USERS

There are plenty of other validation sets and options that can be used, but they are not part of this article.

Next is the parameter for the Registry key path. Since this is separate from the Hive, it is spelled out in a normal path fashion, such as Software\Microsoft\Windows. If the path has spaces in it, you’ll need to use quotes around the path. Such as ‘Software\Microsoft\Windows NT\CurrentVersion’. (And since it is powershell, we use single quotes when possible, since it tells powershell that there are no replaceable variables in the string. If there were going to be, we would use double quotes instead.

[Parameter(Position = 2, Mandatory = $true)]
[string]$RegKeyPath,

Next we have parameters 3 & 4

[Parameter(Position = 3, Mandatory = $true)]
[string]$ValueName,

[Parameter(Position = 4, Mandatory = $true)]
$ValueData,

This covers the name of the registry value to create and the data for the value. However, notice that the $ValueData parameter does not have a type accelerator. This is due to the fact that we don’t know exactly what kind of data we’ll be using, such as binary data, dwords, etc.

There is also another useful parameter type – [Switch]. This parameter either takes effect, or doesn’t depending on whether it exists.  In this case, I am using it to indicate force.  For this script, if the switch is present it will mean we forcibly create the full registry key path if it does not exist.

This is a lot of explanation just for the first part of the script, but, Part 2 will have the working body of the script.

Until then..

David Figueroa

At this point, we’ve created the parameter block for the get-zdc function.  It’s set up to accept pipeline data, and it is a mandatory parameter.

The next step is to initialize a few variables that we will be using.  Most powershell variables don’t require initialization, but for my own purposes, I frequently find it easier to have it initialized first.

	$Namespace = "root\Citrix"
	$FarmCanBeReached = $false # This is used for Ping &amp; WMI tests
	$FarmIsAvailable = $false

As an option, all powershell functions have 3 major blocks that are processed

	BEGIN { #code }
	PROCESS { #code }
	END { #code }

The Begin block is processed once, the Process block is repeated with a loop, and the End block is processed once. These 3 lines could be placed inside the BEGIN block if desired. It’s not required per-se, for long scripts it is an option for organization.

The first line shows the namespace for WMI we’ll be using. Virtually all the scripts I’ve seen for ‘training’ purposes, or ‘how to’ scripts always use Root\CIMV2. But, as you can see, Citrix has it’s own provider. This is one of the reasons that WMI Explorer is so useful (see the first post). The 2nd and 3rd lines ore just establishing that we have not validated that we can reach a farm or not.

Next, we start a loop to process all of the objects being passed into the function. (This entire section of the loop could be embedded in the PROCESS section as mentioned above).

	foreach ($Computer in $ComputerName) {
	If ($FarmCanBeReached) {
		Continue
	} else {

The lines are pretty straight forward – We’re processing each individual computer from the ComputerName string that was passed to us, either by the pipeline, or by specifying the data on the command line. This could also be done using a ForEach-Object { loop. Using the foreach is actually slightly faster than ForEach-Object. The ForEach-Object loop uses the pipeline for each of the objects, and that pipelining adds some overhead. However, without a large number of objects (tens of thousands and above), you are likely to not notice a difference between the two, unless you are using measure-object.

So, for each of the objects in the computername list, we check to see if the $FarmCanBeReached variable is true. Since it is a boolean variable, and it starts out initialized as $false. If it is $true, then we use the Continue command. This command allows you to skip the remaining code in the loop, and move on to the next object. This is a Powershell V3 command. It can be done with V2, with a slightly different structure. (To use the alternate construction, you label your loop, and use a break statement. Your foreach loop becomes :LabelName foreach ($Computer in $ComputerName){ and our Continue becomes break LabelName. And obviously, our reason for this is if it is $true, we’ve established contact with the farm, and we don’t need to continue to try.

Next we do a ping check to validate that the computer is online. Obviously, you must either have the Windows firewall disabled, or allow ping access.

	$PingResult = Test-Connection -ComputerName $Computer -Count 1 -TimeToLive 5 -Quiet
	If ($PingResult) {

The Test-Connection command does a WMI ping. The interesting part is the -Quiet switch. This switch does not appear in the help for Test-Connection. The switch converts the output of the command to a boolean $true or $false depending on what happens. I’m storing it in a variable to provide an easier method of checking the data again later in the script.

We then start the If check to find out if the server can be pinged. If the ping succeeds, we start testing WMI. If pings are not allowed, the If ($PingResult) test could be removed. This next segment of code was designed to verify that WMI connectivity can be established. My coworker wrote the segment, and I don’t fully understand how it works, but it does. The gist of it is that it first attempts to bind to the specified WMI namespace (Recall that we set this to Root\Citrix) and tries to access the __Provider class, which is a required default for any WMI provider. It uses this to check for an Access Denied condition, and if that fails, it changes to Packet Privacy for the WMI calls, instead of plain text.

	try { Get-WmiObject -ComputerName $Computer -Namespace $Namespace -Class "__Provider" -ErrorAction Stop }
	catch [System.Management.ManagementException] { $Error[0].Exception.ErrorCode }
	catch { $Error[0] }
	)
	$Authentication = $(
		if ($AccessTestErrorCode -eq [System.Management.ManagementStatus]::AccessDenied) {
			[System.Management.AuthenticationLevel]::PacketPrivacy
		} else {
			[System.Management.AuthenticationLevel]::Default
		}
		)

Next we attempt to pull the Citrix_Product class from the provider. If it fails, it will not return anything, and if it does, we don’t care what it is, just that it exists.

$ProductWmi = Get-WmiObject -ComputerName $Computer -Authentication $Authentication -Namespace $Namespace -Class Citrix_Product -ErrorAction SilentlyContinue
	if ($ProductWMI -eq $null) {
		#WMI test failed
		$WMIResult = $false
	} else {
		$WMIResult = $true
	} #End of If ProductWMI is null test

You’ll notice, I tend to tag the end of my blocks of code by adding a comment (i.e. #End of If ProductWMI…. It makes it much easier to keep track of keeping all the blocks properly closed. (It is very difficult to track down a code block that is not properly closed (as in does not have the closing symbol such as a ) or }.

One possible optimization I have not tried to make yet with this script function, would be to embed the WMI code inside of the pingresult block. If you can’t ping the server, there is a good chance that you can’t connect to it by WMI and the WMI timeout is pretty long. Currently, that could be an issue where a machine could be pinged by not connected to by WMI.

But, we verify the combination of tests, and if they work, then we set the $FarmCanBeReached to $true. This lets the main loop skip over the other machines once we have succesfully connected.

If ($PingResult -and -$WMIResult) {
	$FarmCanBeReached = $true
	Continue
} #End of Ping &amp; WMI result tests

In the next block, once we have established the fact that we have a machine in the farm that we can connect to, we start enumerating the Zones through WMI. If you notice the Select-Object -first 1 line, we’re selecting the first zone that populates. Since we only want a ZDC for the farm, we really don’t care which zone appears first. We do this first because the Zone Data Collector is a property of a Zone. I put a check in there in case the WMI call to get the zones were to fail, but in theory, it should not happen, since Citrix zones *always* have a ZDC, even if there is only one server, and/or all of them are set to Not Preferred, etc.

if ($FarmCanBeReached) {
	$ZoneWmi = Get-WmiObject -ComputerName $ComputerName -Authentication $Authentication -Namespace $Namespace -Class Citrix_Zone -ErrorAction SilentlyContinue | Select-Object -First 1
	#Test the WMI
if ($ZoneWmi -eq $null) {
	Continue
}

This is one of the most important lines of code here. It actually gets the name of the Zone Data Collector from the Zone itself.

	$MyZDC = ([System.Management.ManagementObject]$ZoneWMI.DataCollector).ServerName
} else {
	$MyZDC = $null
} #End of If FarmIsAvailable block

The construction of the line is important. Using the normal order of operations, the items inside of the parenthesis are processed first. So, we declare the type [System.Management.ManagementObject]. Just like most things in Powershell, using the square brackets indicates that the item is a .Net item (property, method, etc.). Declaring this is not strictly necessary, but it definitely helps decipher what is supposed to be happening in the code. And we are pulling the property with the ZDC ($ZoneWMI.DataCollector). However, if you look at it in WMI Explorer, this is a full WMI Path that is returned by default. This can be parsed directly, but if the path ever changes, the script will break, and since it is all object based, we just pull it directly. We do that by adding the property .ServerName at the end of ($ZoneWMI.DataCollector). And we deliberately set $MyZDC to null if we don’t find anything (for legibility/comprehension), even though it should never be possible. (It is actually possible, if you don’t provide *any* servers that are in the farm, are not contactable by ping and WMI, or possibly even a corrupt WMI stack on all the provided machines.)

And finally, we just wrap up the function and return the results.

 return $MyZDC 

I’ve attached the full script here. The full script has comment based help, and also has a lot of write-verbose statements to show what it is doing as it processes. The script also includes a function I had found online to make it easier to identify what line a script is at during execution, especially when combined with the write-verbose. The short version of this is that the by default, the MyInvocation is going to pull information about the execution of the script itself. By burying it in a function, it pulls the information about the execution of the function.

# ==============================================================================================
# 
# Microsoft PowerShell Source File -- Created with SAPIEN Technologies PrimalScript 2012
# 
# NAME: Get-ZDC.ps1 
# 
# AUTHOR: David Figueroa
# DATE  : 6/11/2014
# 
# COMMENT: This script is used to get the Zone Data Collector when given a Citrix server name
# 			or multiple Citrix server names
# 
# ==============================================================================================

function Get-CurrentLineNumber
{
	$MyInvocation.ScriptLineNumber
}
New-Alias -Name __LINE__ -Value Get-CurrentLineNumber –Description 'Returns the current line number in a PowerShell script file.'

Function Get-ZDC {

<#
	.SYNOPSIS
	Takes a computer name and get the Citrix Zone data collector through WMI calls
	
	.DESCRIPTION
	The function accepts a series of servernames within a single farm.  (If the servers
	span multiple farms, then any farms after the first successful one are ignored).
	The script does a ping test against each of them, and a WMI test for Citrix.  
	If both tests succeed, then the function queries the zone and gets the ZDC as of that moment.
		
	.PARAMETER ComputerName
	This is the name of the computer(s) for the initial query.   Multiple computer names can be sent
	in order to try and guarantee that the ZDC is found. 
	
	.EXAMPLE
	PS C:\> Get-ZDC -ComputerName server01
	serverzdc01
	PS C:\>
	
	.EXAMPLE
	PS C:\>Get-ZDC -ComputerName server01,server02
	serverzdc01
	PS C:\>
#>

	
	[CmdletBinding()]
	
	Param(
		[Parameter(mandatory=$true, valuefrompipeline=$true)]
		[Alias("CN")]
		[string[]]$ComputerName
		)
		
		write-verbose "$(__LINE__)`tEntering PROCESS block of Get-ZDC"
		#Get the list of servers from the ComputeName list and
		#parse them looking for ones that can be pinged & reached with WMI
	
		$Namespace = "Root\Citrix"
		write-verbose "$(__LINE__)`t`$NameSpace is $NameSpace"
		
		$FarmCanBeReached = $false # This is used for Ping & WMI tests
		write-verbose "$(__LINE__)`t`$FarmCanBeReached is $FarmCanBeReached"
			
		write-verbose "$(__LINE__)`tEntering ForEach Computer block"
		foreach ($Computer in $ComputerName)
		{
		write-verbose "$(__LINE__)`t`t`$Computer is $computer"
			If ($FarmCanBeReached) {
			write-verbose "$(__LINE__)`t`t`$FarmCanBeReached is True, Skip to the next server"
			Continue
			}
			else
			{
			write-verbose "$(__LINE__)`t`t`$FarmCanBeReached is False"
			write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
			write-verbose "$(__LINE__)`t`t# Connectivity tests, ping & WMI "
			write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
			write-verbose "$(__LINE__)`t`tPinging $Computer"
			$PingResult = Test-Connection -ComputerName $Computer -Count 1 -TimeToLive 5 -Quiet
				write-verbose "$(__LINE__)`t`tPing result is $PingResult"
			If ($PingResult)
			{
				write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
				write-verbose "$(__LINE__)`t`t# WMI"
				write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
				write-verbose "$(__LINE__)`t`tBeginning WMI test"
				write-verbose "$(__LINE__)`t`tGet WMI Citrix_Product class from '$Namespace' namespace..."
				$AccessTestErrorCode = $(
			try { Get-WmiObject -ComputerName $Computer -Namespace $Namespace -Class "__Provider" -ErrorAction Stop }
				catch [System.Management.ManagementException] { $Error[0].Exception.ErrorCode }
				catch { $Error[0] }
			)
				write-verbose "$(__LINE__)`t`t`$AccessTestErrorCode = $AccessTestErrorCode"
				$Authentication = $(
				if ($AccessTestErrorCode -eq [System.Management.ManagementStatus]::AccessDenied)
				{
					[System.Management.AuthenticationLevel]::PacketPrivacy
				}
				else
				{
					[System.Management.AuthenticationLevel]::Default
				}
				)
			$ProductWmi = Get-WmiObject -ComputerName $Computer -Authentication $Authentication -Namespace $Namespace -Class Citrix_Product -ErrorAction SilentlyContinue
			if ($ProductWMI -eq $null)
				{
					#WMI test failed
					$WMIResult = $false
					write-verbose "$(__LINE__)`t`t`$WMIResult is failed, `$ProductWMI is null"
				}
				else
				{
					$WMIResult = $true
					write-verbose "$(__LINE__)`t`t`WMIResult is good.  `$Product is not null"
				} #End of If ProductWMI is null test
				
				write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++"
				write-verbose "$(__LINE__)`t`t# End of WMI test"
				write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++"
			} #end of $PingResult block
			
			#Now that connectivity tests are done..
			#Validate that they are both good to receive the name of the ZDC
			If ($PingResult -and -$WMIResult) {
					write-verbose "$(__LINE__)`tPing & WMI results are good, FarmCanBeReached set to true"
				$FarmCanBeReached = $true 
					write-verbose "$(__LINE__)`tFound a good machine, using Continue to skip to the next computer"
				Continue
			} #End of Ping & WMI result tests
				write-verbose "$(__LINE__)`tEnd of if FarmCanBeReached block"
		} #End of If FarmCanBeReached test

		write-verbose "$(__LINE__) End of ForEach computer block"
		} #End of ForEach $computer block

		#Test if the farm is available and proceed from there.
		if ($FarmCanBeReached) {
			write-verbose "$(__LINE__)`tGetting WMI for Zones collection"
				$ZoneWmi = Get-WmiObject -ComputerName $ComputerName -Authentication $Authentication -Namespace $Namespace -Class Citrix_Zone -ErrorAction SilentlyContinue | Select-Object -First 1
			
			#Test the WMI
			if ($ZoneWmi -eq $null) {
				Continue
			}
				$MyZDC = ([System.Management.ManagementObject]$ZoneWMI.DataCollector).ServerName
				write-verbose "$(__LINE__)`t`$MyZDC is $MyZDC"
		} else {
		
				#If we get this far, no successful ZDC was found, return $null
				write-verbose "$(__LINE__)`tCould not locate ZDC.  Please investigate and rerun.  A likely cause is no valid servers are in the config file"
				$MyZDC = $null 
		
		} #End of If FarmIsAvailable block
	
		Write-Verbose -Message "Returning `$MyZDC ($MyZDC)"
		return $MyZDC 

} #End of Get-ZDC function

I was one of countless Citrix consultants that has seen the Citrix WMI service, and kind of ignored it.  In one of my past jobs, I had found a specific use for the WMI service, but that was the only use I’d ever found for it.  (For the curious, when you use the Citrix Service Provider licensing, it requires the WMI service to be active on your servers.  It is used to to query the license for some critical information.  I had started to set up the CSP environment, and it was reporting the incorrect license type, and the IMA service would not start.  I had to start the WMI service, and then the license began to report correctly, and the IMA started).

But, I digress:-)  In the new job, I needed to work on some scripting for our various farms (we have a mix of 4.5, 6.0, & 6.5 farms).  I had done some previous documentation type things with MFCOM in 4.5 farms, but I’d never done anything for 6.0/6.5.  I knew there were powershell cmdlets to perform a number of functions, so, that was a possibility, but I really needed to interact with all of the farms.  A coworker has a very very extensive script used to document server inventories.  I took a peek out of it out of straight curiosity, and he was using WMI calls (as expected) for everything, but to my surprise, he was using WMI calls to get Citrix data out of all 3 farms, with the same lines of code.  That definitely got my attention.

First, my coworker recommended I get Microsoft’s WMI explorer.  http://www.microsoft.com/en-us/download/details.aspx?id=24045.  This is a fantastic tool, and easily lets you connect to your local WMI providers, or remote WMI providers, and fully browse everything about them.  I’ve seen other WMI browsers, and the Microsoft one was just easier to use.  Having used this to help build my scripts, I’d strongly recommend you download it.

In my particular case, I need to query the Zone Data Collector for some information.  Generally, most environments have a dedicated machine or two to be the ZDC’s (and we are no exception).  As I was considering the design for my script, it occurred to me – what if one of those two is not going to be the ZDC at the moment my script ran?  It would fail.  It isn’t a critical function per-se, but I liked to plan to make sure my scripts will always run correctly (if at all possible).  So, I needed a way to discover the current zdc at any given moment. In theory, I could have tried to do a qfarm commend and then parsed the text, but that’s not elegant, and to run it remotely, I have to tie in something like psexec.exe or run it on one of my Citrix servers. But, I didn’t want those restrictions, so I created this powershell function script) to handle that discovery process.

The first step of course is define the function:

function get-zdc {

Because we are defining a function, and we want Powershell to automatically generate help, and support some of the common parameters, we turn on cmdlet binding.  This enables the script to act like a built-in cmdlet.  To do that, we use [CmdletBinding()].  This also has it’s own options that were not using here.

Next, we’re going to define a parameter for the function.  We need to tell it what machine to connect to in order to find the ZDC.  Because I’m dealing with lists of servers, I wanted to make sure it can handle multiple input names.  I want the function to take my list of servers and just find it.

Param(
[Parameter(mandatory=$true, valuefrompipeline=$true)]
[Alias("CN")]
[string[]]$ComputerName
)

The Param ( statement starts a list of parameters, and you only have 1 parameter block for each function.  This is required when the function is written this way.  It is possible to do an alternate structure that doesn’t use a Param block structure, but to me, that is more difficult to read.

The second line starts the definition of *a* parameter.  We’re telling it that the parameter is mandatory, and the script will throw an error if it is not provided, then we are telling it to get the value from the pipeline.  Pretty straight forward stuff, if we pipe the data into the function, it will accept it and use it.  Notice that these options are comma delimited, and are contained inside of parenthesis.   And the entire block is contained inside of brackets [ ]. (Just like the Param ()block itself, it’s not truly *required*, but it makes it easier to read.

The 3rd line defines an alias for the parameter.  This will provide a shorter/easier name to use on the command line.  Aliases are not required, but they are a nice option.  And while I didn’t use it here, you can string multiple aliases together, or you can have multiple alias blocks.  To do multiple aliases within one block, you just provide them as a comma delimited list.  I.e. [Alias(“Alias1”, “Alias2”)] (And there is not any appreciable limit to the number of aliases you can have that I’m aware of).

Lastly, we are defining the type-cast of the data we will be inputting, along with the variable name. But,this line needs a little more explanation. As I mentioned, by using the brackets, we’re defining the type of data (a string), but the important part to this is that we’re using an extra set of brackets (shown in bold) to tell the system we will accept data from the pipeline. [string[]]. Then we are giving the input parameter a name ($ComputerName). Be careful in choosing your parameter names. Many functions have ‘standardized’ parameter names for a specific reason — by using these ‘standard’ names, you can pipe input in and out of the function and the following/preceding function will accept that data in those specific parameters. (That is an entire discussion in and of itself, and as always, there are alternate techniques that can be used with it or instead of it, but it is still a best practice.

As a side note you can also add a default to a parameter just by adding an equals sign and a value. If you do that, then your function will use the default value if one is not provided. This is a great idea for a mandatory parameter, if you have something that would be appropriate. As a quick example, if I need an integer number, and I’d like the default to be the answer to life, the universe, and everything, I would use [int]$Number=42.
Also note, that within a parameter block, you can use commas to separate multiple parameters.

Param (
[parameter(mandatory=$false,valuefrompipeline=$false)]
[Alias("P1")]
[Int]$Number=42,


[parameter(mandatory=$false,valuefrompipeline=$true)]
[Alias("P2")]
[string[]]$String='Some string'
)

Notice the comma in the example after the first parameter, and the fact that the second parameter does not have that comma. (Also, be aware, there are a lot of options that can be used on the parameter statement line, or you can skip them all depending on your needs.

Next time I’ll dig into the real meat of the function.

David Figueroa

I don’t have a lot of time for another post right now, but I found this one particularly interesting.

I was finding a lot of warning errors on my PVS servers.  There were numerous errors, but this was the most common.
The Citrix Broker Service failed to contact virtual machine ‘<machinename>’ (IP address ).
Check that the virtual machine can be contacted from the controller and that any firewall on the virtual machine allows connections from the controller. See Citrix Knowledge Base article CTX126992.
Error details:
Exception ‘Client is unable to finish the security negotiation within the configured timeout (00:00:05). The current negotiation leg is 1 (00:00:05). ‘ of type ‘System.TimeoutException’.

There were several articles on the subject, most of them talking about the VM’s registering, and deregistering, etc. but I didn’t have that problem. I ran down the articles, and none of the problems fit my situation.  Since I wasn’t really having a problem, I dropped the subject for a while.  I had some time recently to start looking back into the subject.

I had followed the best practices for the VM’s (or thought I had).  I ended up calling Citrix support, and went through everything.  We were going through all the same articles again, and not finding anything.  But, the Citrix tech noticed one thing.. I had the firewall turned off for the domain profile, but not for the private or public profiles.  We began discussing the subject, and why it was set that way.  Best practices say that the firewall be turned off for the PVS machines.  Since the machines already belonged to the domain, I turned off the domain profile by GPO.  (To me, this is a pretty typical security view – only enable/disable what you have to, and lock the rest).

It turns out that even though PVS is booting from the network, and that network connection is the very first thing established, as that streamed copy of Windows boots up, it does not recognize the network connection profile as the domain profile until well after the machine is up, and it has already tried to register.   Once it gets up far enough, the GPO takes effect, and the firewall comes down, the re-register works, and everything is good.   I turned around and adjusted the GPO to have the the firewall always off.  Success!  90%+ errors of the errors were gone.  I still get the occasional warning errors, but now they are unusual.

So, the next step will be to check the actual firewall configurations, and forcibly allow all the ports through the firewall in all profiles.

David F.

4.1 Wrinkle.. Quick update

Posted: March 21, 2014 in Citrix

Just to let everyone know — on my situation with the resolution & DPI scaling issue.  Citrix has escalated my case and they will be working with development to address it.

David F.

I haven’t had any time to work on this yet, but I still have the call open with Citrix.  However, my coworker who found the issue also found a way to get the full resolution he was looking for.  He went into the Display Settings and set the slider for “Change the size of all items” down to Smaller.  Obviously, this peaked my interest..  (And for reference, here is a link that discusses these settings http://www.eightforums.com/tutorials/28310-dpi-scaling-size-change-displays-windows-8-1-a.html.)  I immediately started thinking that it might be tinkering with the dots per inch (DPI) settings (http://en.wikipedia.org/wiki/Dpi).   As most long term Citrix admins will know, by default the ICA sessions are going to run at 96 DPI; this has been the standard display setting for years.  However, many of today’s high resolution monitors run at higher DPI settings to keep the fonts etc. from becoming microscopic.

So, on to some testing.  I booted up my Win 8.1 system I’m testing with and connected in.  As before, i got my 1536×864 resolution.  I jumped back to the local machine and checked that “Change the size…” setting, and it was set to the middle of the slider.  So, I logged out of my Citrix session, set the slider to the smallest setting, and logged back into my desktop.  Eureka.. full resolution. I sent an email to my engineer at Citrix with what I had found, and used some numbers that sounded good in my head.  (As it turns out, it was a fantastic guess.. the numbers were exactly correct).  What I found was this —

Normal View 96 DPI Scaled View 120 DPI Ratio (96/120)*100 = 80
1920 1536 80%
1080 864 80%

There’s the resolution I was getting.. So, I jumped in and went through the whole sequence again to test the next level. And came up with similar results.

Normal View 96 DPI Scaled View 144 DPI Ratio (96/144)*100 = 67
1920 1280 80%
1080 720 80%

It came out exactly as I had guessed it would based on 2 seconds of math.  I have since sent these results to Citrix, and it is completely reproducible.

So my working theory is that when the Receiver connects from a high DPI system, it cannot properly negotiate the resolution if the scaling is turned on.  I also believe that Windows 8.1 starts with the scaling enabled, whereas Windows 8.0 does not.  And when 8.0 is upgraded to 8.1, the DPI settings are left alone as a user preference.  It’s a good theory, and all the evidence bears it out.

David F.