Alzi la mano chi non ha mai incontrato quell’essere mitologico, antico, probabilmente ansiato, nemico giurato della digitalizzazione, inconsapevole amante dei giardini di plastica, che infesta tutti gli uffici: lo stampatore seriale!

Quello che stampa (quasi) tutte le mail per archiviarle nei classificatori ingialliti, quello che scarica un documento in PDF da 150 pagine per consultarne mezzo capitolo ma sul monitor “fa fatica a leggere” e quindi meglio stamparlo (tutto) per leggerlo su carta in pose che l’ortopedico ringrazia, quello che “ma non si sa mai…” insomma ci siamo capiti!
Arriva quindi, si spera, il momento dove il Manager con coscienza green o più probabilmente chi vede o paga i costi di gestione chiede finalmente uno strumento per la reportistica e il monitoraggio di queste povere, sfruttatissime stampanti (l’Amazzonia ringrazia).

Se stai leggendo questo articolo probabilmente hai già trovato dei software più o meno adatti allo scopo, c’è solo un problema, sono tanto belli e ben integrati che hanno una licenza commerciale da comprare (giustamente).
Ecco allora una soluzione, in ambito Windows, che possiamo usare come base di partenza e arricchire poi in base alle necessità, utilizzeremo:

  • uno script PowerShell per l’estrazione dei dati lato client
  • un DB SQL per l’archiviazione, in questo esempio un’istanza MSSQL
  • un Web Service che riceverà i dati dai client

Lo script

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
<#
.SYNOPSIS
    Enable operational print log and push updates on print activity
.DESCRIPTION
    Enable operational print log and push updates on print activity
    Exit 0 and log with ID 0 if successfull, 1 if something wrong
.NOTES
    Author:  Lestoilfante
    Email:   [email protected]
    Date:    18NOV2021
    PSVer:   5.0
#>	

# Definiamo URL del WebService che riceverà i dati
$serverUrl = "https://mioserver.contoso.local/api/PrintLog"
# Definiamo un nome da usare come Event Source in EventLog di Windows
$eventName = "Contoso_PrintReport"

# Creiamo, se non esistente, Event Source nel log Applicazione
If ([System.Diagnostics.EventLog]::SourceExists($eventName) -eq $False) {
    New-EventLog -LogName Application -Source $eventName
}

# Qui un passaggio chiave, abilitiamo il log delle stampe di Windows, di default non è attivo
$printLog = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration "Microsoft-Windows-PrintService/Operational" 
if ($printLog.IsEnabled -eq $False) {
    $printLog.IsEnabled = $True
    $printLog.MaximumSizeInBytes = 5242880
    $printLog.LogMode = Circular
    $printLog.SaveChanges()
}

# Recuperiamo il log scritto dalla precedente esecuzione dello script,
# se non troviamo nulla cercheremo le stampe avvenute negli ultimi 30 giorni.
# Notare la ricerca solo per Event ID 0, che per noi rappresenta un precedente report inviato con successo
$filterEvtScript = @{
    Logname='Application'
    ProviderName=$eventName
    ID=0
}
$evt = Get-WinEvent -filterHashTable $filterEvtScript -MaxEvents 1 -ErrorAction SilentlyContinue
if ($evt -eq $null){
    $evtStartTime = [datetime]::Today.AddDays(-30)
}
else {
    $evtStartTime = $evt.TimeCreated
}

# Cerchiamo gli eventi di stampa avvenuti successivamente ad $evtStartTime e li convertiamo in un array di oggetti Xml 
$filterEvtPrint = @{
    LogName = 'Microsoft-Windows-PrintService/Operational'
    ID=307
    StartTime = $evtStartTime
}
[xml[]]$xml = Get-WinEvent -filterHashTable $filterEvtPrint -ErrorAction SilentlyContinue | ForEach-Object{ $_.ToXml() }

# Se non abbiamo nuovi eventi di stampa terminiamo lo script ma prima scriviamo un log che sarà l'$evtStartTime alla prossima esecuzione
if ($xml.Count -eq 0) {
    Write-EventLog -LogName "Application" -Source $eventName -EventID 0 -EntryType Information -Message "0 events found" -Category 1
    exit 0
}

# Avendo trovate delle nuove stampe creiamo una object collection con i campi che ci servono
# In questo caso prenderemo l'username dell'utente che ha lanciato la stampa, la quantità di pagine inviate,
# il nome della stampante ed il timestamp della stampa
$collectionWithItems = New-Object System.Collections.ArrayList
Foreach ($event in $xml.Event)
{
    if ($event.Userdata.DocumentPrinted -ne $null){
        $temp = "" | select "UserName", "PageQty", "PrinterName", "Date"
        $temp.UserName = $event.Userdata.DocumentPrinted.Param3
        $temp.PageQty = $event.Userdata.DocumentPrinted.Param8
        $temp.PrinterName = $event.Userdata.DocumentPrinted.Param5
        $temp.Date = $event.System.TimeCreated.SystemTime
        $collectionWithItems.Add($temp) | Out-Null
    }
}

# Convertiamo tutto in formato JSON
try {
    $json = ConvertTo-Json $collectionWithItems
}
catch {
    Write-EventLog -LogName "Application" -Source $eventName -EventID 1 -EntryType Information -Message "Error in json conversion" -Category 1
    exit 1
}

# Inviamo il JSON tramite metodo POST al nostro WebService
# In caso di errore è importante loggare un EventId "1" in modo da reinviare i dati
try
{
    $Response = Invoke-WebRequest -Method 'Post' -Uri $serverUrl -Body $json -ContentType "application/json" -TimeoutSec 15 -UseBasicParsing
    $StatusCode = $Response.StatusCode
    $Message = [String]::new($Response.Content)
    Write-EventLog -LogName "Application" -Source $eventName -EventID 0 -EntryType Information -Message "Report sent, response: $Message" -Category 1
    exit 0
}
catch
{
    $StatusCode = $_.Exception.Response.StatusCode.value__
    Write-EventLog -LogName "Application" -Source $eventName -EventID 1 -EntryType Information -Message "Http error $StatusCode" -Category 1
    exit 1
}

exit 0

Il DB

Vogliamo avere un record riassuntivo giornaliero per Utente e Stampante in modo da sapere Chi/Dove/Quando/Quanto.

Creiamo quindi la nostra tabella PrintLog

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
USE [ContosoDB]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[PrintLog](
	[Username] [nvarchar](10) NOT NULL,
	[Date] [date] NOT NULL,
	[Printer] [nvarchar](30) NOT NULL,
	[Pages] [int] NOT NULL,
	CONSTRAINT [PK_PrintLog] UNIQUE NONCLUSTERED 
	(
		[Username] ASC,
		[Date] ASC,
		[Printer] ASC
	)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

E creiamo una Stored Procedure che gestirà l’inserimento dei dati

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
USE [ContosoDB]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[printLog_update](
	@_Username AS nvarchar(10),
	@_Date AS date,
	@_Printer AS nvarchar(30),
	@_Pages AS int
)
AS
BEGIN
	DECLARE @_varPages AS int;
	SET @_varPages = (SELECT Pages FROM PrintLog WHERE Username = @_Username and Date = @_Date and Printer = @_Printer);
	IF (@_varPages IS NULL)
	BEGIN
		INSERT INTO PrintLog (Username,Date,Printer,Pages) VALUES (@_Username,@_Date,@_Printer,@_Pages)
	END
	ELSE
	BEGIN
		UPDATE
			PrintLog
		SET
			Pages = @_Pages + @_varPages
		WHERE
			Username = @_Username AND Date=@_Date AND Printer=@_Printer
	END
END;
GO

Il Web Service

Vale la regola dell’usare quello che meglio conoscete o che è meglio supportato nella vostra infrastruttura, in questo caso un esempio in .NET.

Per prima cosa definiamo un modello per i dati che riceveremo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class PrintLogData
{
	public class LogCollection
    {
    	public List<LogEntry> Entry { get; set; }
	}
	public class LogEntry
	{
    	public string UserName { get; set; }
    	public int PageQty { get; set; }
        public string PrinterName { get; set; }
        public DateTime Date { get; set; }
	}
}

Quindi definiamo il nostro Web Service minimale che chiamerà la stored procedure definita sul DB

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class PrintLogController : ApiController
{
    private readonly string sqlServer = "";
    private readonly string sqlDB = "ContosoDB";
    private readonly string sqlUser = "";
    private readonly string sqlPwd = "";

    // POST: api/PrintLog
    public string Post([FromBody] ICollection<PrintLogData.LogEntry> json)
    {
        // Raggruppiamo i dati ricevuti per user/data/stampante sommandone le pagine
        var newList = json.GroupBy(x => new { x.UserName, x.Date.Date, x.PrinterName })
            .Select(y => new PrintLogData.LogEntry()
            {
                UserName = y.Key.UserName,
                Date = y.Key.Date.Date,
                PrinterName = y.Key.PrinterName,
                PageQty = y.Sum(c => c.PageQty)
            });
        int count = newList.Count();
        int entriesProcessed = 0;

        // Salviamo i dati sul DB e ritorniamo al web client l'HttpStatusCode opportuno in base all'esito 
        try
        {
            using (var sqlCon = new SqlConnection("Data Source=" + sqlServer + ";Initial Catalog=" + sqlDB + "; User ID = " + sqlUser + "; Password = " + sqlPwd))
            {
                sqlCon.Open();
                foreach (var item in newList)
                {
                    SqlCommand sql_cmnd = new SqlCommand("printLog_update", sqlCon);
                    sql_cmnd.CommandType = CommandType.StoredProcedure;
                    sql_cmnd.Parameters.AddWithValue("@_Username", SqlDbType.NVarChar).Value = item.UserName;
                    sql_cmnd.Parameters.AddWithValue("@_Date", SqlDbType.Date).Value = item.Date;
                    sql_cmnd.Parameters.AddWithValue("@_Printer", SqlDbType.NVarChar).Value = item.PrinterName;
                    sql_cmnd.Parameters.AddWithValue("@_Pages", SqlDbType.Int).Value = item.PageQty;
                    entriesProcessed += sql_cmnd.ExecuteNonQuery();
                }
                sqlCon.Close();
            }
        }
        catch (Exception e)
        {
            return SendStructResponse(e.Message, HttpStatusCode.BadRequest);
        }

        return SendStructResponse(entriesProcessed.ToString(), HttpStatusCode.OK);
    }

    internal T SendStructResponse<T>(T response, HttpStatusCode statusCode = HttpStatusCode.OK)
    {
        if (statusCode != HttpStatusCode.OK)
        {
            var r =
                new HttpResponseMessage(statusCode)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "text/plain")
                };
            throw new HttpResponseException(r);
        }
        return response;
    }
}

Mettiamo insieme i pezzi

Bene ora che abbiamo tutte le componenti come gestiamo il deploy sui client? Ovviamente dipende da voi e dagli strumenti che avete a disposizione, tenete solo presente che lo script deve girare almeno la prima volta con privilegi elevati per attivare correttamente il logging, io una soluzione completamente automatica in reti di Dominio Windows l’avrei… vediamo se arrivate alla stessa soluzione anche voi!