Du multithread avec un script VBS ?!

VBS MultithreadComment j’en suis arrivé à me poser cette question ? J’ai commencé avec un script vbs, j’aurai pu le faire en powershell, mais je devais être capable de tout lancer avec des droits user pour reproduire les droits d’un utilisateur standard. J’ai codé pas mal d’année en VB, je me sentais plus à faire ce script en VBS.

Je devais trouver des fichiers sur un réseau d’ordinateur complet. Mon premier script prenait trop de temps car je scannais les ordinateurs un à un et parfois le nombre de fichier était très important ; je me suis donc posé la question comment faire du multithread. L’idée était d’avoir un script qui contenait une boucle avec les ordinateurs à scanner et celui-ci lancerait en parallèle un certain nombre de scan.

Je ne vous cache rien, j’ai pris comme exemple le script présenté dans ce site (https://gallery.technet.microsoft.com/scriptcenter/32e0146a-83fc-4ee1-af7b-52a57d57466c). Le concept est vraiment intéressant par rapport à d’autre script que j’ai pu trouver, car il ne se contente pas de lancer un certain nombre de thread en parallèle, il attends que l’un d’entre eux se termine pour en relancer un nouveau. D’accord rien de nouveau par rapport à ce l’on peut faire par ailleurs, mais pour du scripting c’est vraiment pas mal !

Passons maintenant à la bête !

Le script principal, dans mon utilisation, liste tous les objets de type « Computer » de l’Active Directory qui est en cours d’utilisation par l’utilisateur lanceur du script. Puis pour chaque objet il lancera le script qui ira chercher le fichier souhaité, ce script est appelé avec le nom de l’ordinateur en paramètre.

Le nombre de thread est réglable dans les constantes du script.

'* -----------------------------------------------------------------------------
'* Define Constants
'*
Const ForReading                = 1
Const ForAppending                = 8
Const adVarChar                    = 200
Const adInteger                    = 3
Const adFilterNone                = ""
Const MaxCharacters                = 255
'* change MaxThreads to the number of cscripts you want to be running simultaneously
'* NOTE: there can actually be a MaxThreads + 2 script engines running
Const ADS_SCOPE_SUBTREE = 2
Const MaxThreads                = 12

'* -----------------------------------------------------------------------------
'* Declare variables
'*
dim g_ador
dim g_fso
dim g_ts
dim infile
dim oWshShell
dim uargs
dim sTarget
dim cThread
dim aExec()
dim logFile
dim i

'* the output
logFile = "C:\temp\myLog.log"

set g_fso = createobject("scripting.filesystemobject")

Set g_ts = g_fso.OpenTextFile(logFile, ForAppending, true)

'*********************** AD Query
Set Root    = GetObject("LDAP://RootDSE")
DomainPath    = Root.Get("DefaultNamingContext")
Set objConnection = CreateObject("ADODB.Connection")
Set objCommand =   CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Set objCommand.ActiveConnection = objConnection

objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE

objCommand.CommandText = "<LDAP://" & DomainPath & ">;(&(objectCategory=computer)(sAMAccountName=*)); sAMAccountName ;subtree"

Set objRecordSet = objCommand.Execute

'*********************** End AD Query

'* create Disconnected RS
Set g_ador = CreateObject("ADOR.Recordset")
g_ador.Fields.Append "ComputerName", adVarChar, MaxCharacters
g_ador.Fields.Append "ExecStatus", adInteger, 5
g_ador.Fields.Append "hExec", adInteger, 5
g_ador.Open

Set oWshShell = WScript.CreateObject("WScript.Shell")

'* count of script engines executing
cThread = 0
i = 0

objRecordSet.MoveFirst

Do Until objRecordSet.EOF
redim preserve aExec(i)

wscript.echo ComputerName
ComputerName = objRecordSet.Fields("sAMAccountName")
ComputerName = LEFT(ComputerName,LEN(ComputerName)-1)

'* script.vbs is a script that returns and exit code using wscript.quit(1) or wscript.quit(0)
'* the value of the exit code will be used as the results for logging
'* the call to the script will be script.vbs arg
'* this script could do anything such as return a ping result or check a machine for a certian
'* software.  I suppose you could even build a sort of enum and depending on your exit codes
'* return certian strings from the main script in a select case statment
set aExec(i) = oWshShell.Exec("cscript.exe /nologo "".\My_Script.vbs"" " & ComputerName)

g_ador.addnew
g_ador("ComputerName") = sTarget
g_ador("ExecStatus") = 0
g_ador("hExec") = i
g_ador.Update
cThread = cThread + 1
if cThread > MaxThreads then
WaitForThreads(MaxThreads)
g_ador.MoveLast
end if
i = i + 1

objRecordSet.MoveNext
Loop

'* wait for the remaining threads to quit
WaitForThreads(0)

g_ts.close
g_ador.Close

'* -----------------------------------------------------------------------------
'* Sub: WaitForThreads
'*
'* Purpose:  Wait for number of executions to drop under MAX before
'*             executing another thread (script engine)
'*
'* Input:    [in] Max  the number of threads running
'*
'* Output:   none
'*
'* -----------------------------------------------------------------------------
Public Sub WaitForThreads(byVal Max)

dim n          '* the handle number of the still running oWshShell.Exec

'* once the cThread drops below max the main loop will continue and start
'* executing more script engines
do while cThread > Max

'* give scripts a moment to return results
wscript.sleep 200
'* filter ador for records that we do not have results for yet
'* when a result is recieved the ExecStatus is changed to 1
g_ador.Filter = "ExecStatus = 0"
g_ador.MoveFirst
while not g_ador.EOF
n = g_ador("hExec")
if aExec(n).Status <> 0 then
'* update status to complete
g_ador("ExecStatus") =  aExec(n).Status
g_ts.writeline Now & " | " & g_ador("ComputerName") & " | Return: " &  aExec(n).ExitCode
end if
g_ador.MoveNext
wend
cThread = g_ador.RecordCount
loop

'* remove filter
g_ador.Filter = adFilterNone

end sub

Et voici le deuxième script qui parcours les partages disponibles en écriture à la recherche du fichier ! J’ai dû utiliser un simple Net View pour lister les partages car je n’avais pas les droits nécessaire pour utiliser le CIMV2.

Il y a certainement mieux à faire, mais sur le moment c’est la méthode que j’ai trouvé le plus rapidement !

on error resume next

'* use argument as the file path for input 
set uargs = wscript.arguments.unnamed 
 
if uargs.count = 0 then  
    wscript.quit(0)  
end if 

ComputerName = uargs(0)

Set fs = CreateObject("Scripting.FileSystemObject")
Set fich = fs.OpenTextFile("C:\TEMP\log_" & ComputerName & ".txt", 8, true)

  fich.writeline ComputerName & " - " & now
  'wscript.echo ComputerName
  
  Set shell=Wscript.CreateObject("Wscript.Shell")
  Set oExec = shell.Exec ("%comspec% /c net view \\" & ComputerName)
  i=1
  Do while not oExec.StdOut.AtEndOfStream
    result = oExec.StdOut.ReadLine
    if i >= 8 then
      share = Split(result, " ", -1, 1)
      if Ubound(share) > 0 then
        'wscript.echo (share(0)) &  " - " & i
        If WriteAccess("\\" & ComputerName & "\" & share(0)) then
          Wscript.echo "Droit écriture : \\" & ComputerName & "\" & share(0)
          fich.writeline "Droit écriture : \\" & ComputerName & "\" & share(0)
          Set fso = CreateObject("Scripting.FileSystemObject")
          result_fl = Find_File(fso.GetFolder("\\" & ComputerName & "\" & share(0)))
          if result_fl then
            Wscript.echo "******* Fichier détecté : " & "\\" & ComputerName & "\" & share(0) & " *****"
            fich.writeline "**************************************************************************"
            fich.writeline "**************************************************************************"
            fich.writeline "******* Fichier détecté : " & "\\" & ComputerName & "\" & share(0) & " *****"
            fich.writeline "**************************************************************************"
            fich.writeline "**************************************************************************"
            Set fich_rg = fs.OpenTextFile("C:\TEMP\Scan_report.txt", 8, true)
            fich_rg.writeline "******* Fichier détecté : " & "\\" & ComputerName & "\" & share(0) & " *****"
            fich_rg.close
          end if
        End If

      end if
    end if
    'dump = dump & vbcrlf & oExec.StdOut.ReadLine
    i=i+1
  Loop

fich.writeline "Fin - " & now
fich.close
wscript.quit

function Find_File(fldr)
    fich.Write "."
  For Each f In fldr.Files
    If LCase(f.Name) = "_File_instructions.txt" Then
      Find_Locky = True
    End If
  Next

  For Each sf In fldr.SubFolders
    Find_File sf
  Next
End function

Function WriteAccess(strFolderPath)
    Set objFso = CreateObject("Scripting.FileSystemObject")
    On Error Resume Next
    strTmpFile = objFso.GetTempName
    Set objTmpFile = objFso.CreateTextFile(strFolderPath & "\" & strTmpFile)
    If Err.Number <> 0 Then
        WriteAccess = False
        Err.Clear
    Else
        WriteAccess = True
        objTmpFile.Close
        objFso.DeleteFile strFolderPath & "\" & strTmpFile
    End If
End Function

 

Je partage le code car j’ai été confronté à une problématique que je ne dois pas être seul à rencontrer, j’ai passé un certain temps à chercher la solution et le script pourrait servir à différente personne (à quand un label « utilité publique » pour les scripts !).

 

Amusez vous bien ! 😀