Voilà, j'ai tenté d'adapter votre classe common avec du codemode = objectgenerator:

Include Ensemble

Class XXXX.Outils.BS.AuditCommons
{

/// Class of the audit to use for this flow. If the flow use/// iterative process, for instance a loop to inserted multiple/// lines in a table, you can use its insertdetails() method to/// log an error that occured in the loop without poluting/// the journal.Property auditClass As%String(MAXLEN = 500) [ InitialExpression = "XXXX.REPORTING.Tables.Commons.Main" ];/// Name of the flow that will be inserted in the audit.Property flowName As%String(MAXLEN = 500);/// Source of the flow that will be inserted in the audit.Property flowSource As%String(MAXLEN = 500);/// Target of the flow that will be inserted in the audit.Property flowTarget As%String(MAXLEN = 500);/// If true, the message will be sent synchronously.Property asynchronous As%Boolean;/// Audit instance of the current session /// XXXX.REPORTING.Tables.Commons.Main;Property audit As XXXX.REPORTING.Tables.Commons.Main;Property errMsg As%String(MAXLEN = "");Parameter auditLogError = 1;Parameter SETTINGS = "auditClass:flowParameters,flowName:flowParameters,flowSource:flowParameters,flowTarget:flowParameters,asynchronous:flowParameters";
Method SendRequestSync(pTargetDispatchName As%String, pRequest As%Library.Persistent, ByRef pResponse As%Library.Persistent, pTimeout As%Numeric = -1, pDescription As%String = "") As%Status [ CodeMode = objectgenerator, ForceGenerate ]
{
	; Le code ne peut être généré que dans les sous classesIf%class.Name = "XXXX.Outils.BS.AuditCommons"Quit$$$OKDo%code.WriteLine($Char(9) _ "if ..asynchronous quit ..SendRequestAsync(pTargetDispatchName, pRequest, pDescription)")
	Do%code.WriteLine($Char(9) _ "set pResponse = """"")
	Do%code.WriteLine($Char(9) _ "set sc = ..initAudit(pRequest) quit:'sc sc")
	Do%code.WriteLine($Char(9) _ "set req = ..setReq(pRequest, .sc) quit:'sc sc")
	Do%code.WriteLine($Char(9) _ "quit ##super(pTargetDispatchName, req, .pResponse, .pTimeout, pDescription)")
	
	Quit$$$OK
}

Method SendRequestAsync(pTargetDispatchName As%String, pRequest As%Library.Persistent, pDescription As%String = "") As%Status [ CodeMode = objectgenerator, ForceGenerate ]
{
	; Le code ne peut être généré que dans les sous classesIf%class.Name = "XXXX.Outils.BS.AuditCommons"Quit$$$OKDo%code.WriteLine($Char(9) _ "if '..asynchronous quit ..SendRequestSync(pTargetDispatchName, pRequest, .pResponse, .pTimout, pDescription)")
	Do%code.WriteLine($Char(9) _ "set sc = ..initAudit(pRequest) quit:'sc sc")
	Do%code.WriteLine($Char(9) _ "set req = ..setReq(pRequest, .sc) quit:'sc sc")
	Do%code.WriteLine($Char(9) _ "quit ##super(pTargetDispatchName, req, pDescription)")
}

/// Initialize an audit for the current session. You can/// use the audit instance using the ..audit property.
Method initAudit(req As Ens.Request = "") As%Status
{
    do..generateSession()
 
    set auditData = {
        "session": (..getSessionId())
        ,"flowName": (..flowName)
        ,"flowSource": (..flowSource)
        ,"flowTarget": (..flowTarget)
        ,"filename": (##class(XXXX.OUTILS.File.FileInfos).getFilename(req))
    }

    set..audit = $CLASSMETHOD(..auditClass, "initialize", auditData, .sc)

    if 'sc return..error("<Initialize audit>",, "initAudit", sc).AsStatus()

    if..errMsg '= ""do..audit.setError(..errMsg, ..#auditLogError)

    return1
}

Method setReq(pRequest As%Library.Persistent, Output gsc As%String = 1) As Ens.Request
{
    return pRequest
}

Method generateSession() [ CodeMode = objectgenerator, ForceGenerate ]
{
	; Le code ne peut être généré que dans les sous classesIf%class.Name = "XXXX.Outils.BS.AuditCommons"Quit$$$OKDo%code.WriteLine($Char(9)_"Set (..%SessionId,$$$JobSessionId) = ##class(Ens.MessageHeader).%New().MessageId()")
}

Method getSessionId() As%String [ CodeMode = objectgenerator, ForceGenerate ]
{
	; Le code ne peut être généré que dans les sous classesIf%class.Name = "XXXX.Outils.BS.AuditCommons"Quit$$$OKDo%code.WriteLine($Char(9) _ "Quit ..%SessionId")
}

ClassMethod OnGetConnections(pArray As%String, item As Ens.Config.Item) As%Status
{
    do##class(XXXX.OUTILS.Admin).linkBsToBp(.pArray, .item)
}

/// Method used as an alias to shorten the code.ClassMethod error(name As%String, code As%String = "", location As%String = "", data As%String = "") As%Exception.General
{
    return##class(%Exception.General).%New(name, code, location, data)
}

}

ensuite, pour la classe "XXXX.Outils.BS.ComplexMap.FTP", modifiez juste l'ordre de l'héritage : 

Class XXXX.Outils.BS.ComplexMap.FTP Extends (XXXX.Outils.BS.AuditCommons, EnsLib.RecordMap.Service.ComplexBatchFTPService)
{

}

Après compilation si vous regardez le code de la routine générée pour la classe ComplexMap.FTP (ctrl+maj+v) vous pourrez remarquer que le code des méthodes [ codemode = objectgenerator, ForceGenerate] a bien été inclus:

zSendRequestAsync(pTargetDispatchName,pRequest,pDescription="") public {
	if '..asynchronousquit..SendRequestSync(pTargetDispatchName, pRequest, .pResponse, .pTimout, pDescription)
	set sc = ..initAudit(pRequest) quit:'sc sc
	set req = ..setReq(pRequest, .sc) quit:'sc sc
	quit##class(XXXX.Outils.BS.AuditCommons)$this.SendRequestAsync(pTargetDispatchName, req, pDescription)
}
zSendRequestSync(pTargetDispatchName,pRequest,pResponse,pTimeout=-1,pDescription="") public {
	if..asynchronousquit..SendRequestAsync(pTargetDispatchName, pRequest, pDescription)
	set pResponse = ""set sc = ..initAudit(pRequest) quit:'sc sc
	set req = ..setReq(pRequest, .sc) quit:'sc sc
	quit##class(XXXX.Outils.BS.AuditCommons)$this.SendRequestSync(pTargetDispatchName, req, .pResponse, .pTimeout, pDescription)
}
zgenerateSession() public {
	Set (..%SessionId,%Ensemble("SessionId")) = ##class(Ens.MessageHeader).%New().MessageId()
}
zgetSessionId() public {
	Quit ..%SessionId }

Logiquement comme la classe common n'hérite plus de Ens.BusinessService, le fait qu'elle soit la classe la plus à gauche dans l'ordre d'héritage ne devrait plus avoir d'influence négative.

Bonjour @Moussa SAMB ,

Habituellement lorsque je dois faire ce type d'opération j'utilise directement un logiciel de compression comme 7zip (ou autres peu importe).

Avec la commande $ZF(-100,...) il est possible d'éxécuter d'autres programmes en ligne de commande.  

Essayez d'abord de faire la décompression en ligne de commande dans une invite de commande msdos ou dans un shell (en fonction de votre OS).  Lorsque ça fonctionne, tentez d'adapter cela en ObjectScript avec le $ZF -100.

Lorenzo.

Bonsoir @Jules Pontois 

Oui dans le cas ou une classe hérite de "XXXX.Outils.BS.ComplexMap.FTP" effectivement les méthodes sont générées dans toutes les classes et les appels ##super ne vous arrange pas.

En fait, si on retire le keyword "ForceGenerate" des méthodes "objectgenerator", les méthodes ne seront générées que dans la sous classe direct.  J'ai utilisé ce keyword pour être sûr que ce soit au moins généré dans sa première sous-classe, mais après avoir fait un test sur ma machine, ce n'est visiblement pas nécessaire.  Vous pouvez donc retirer ce keyword de la classe common, cela devrait régler le problème.

Toutefois si dans l'une de vos sous classes vous faites un override de SendRequestSync\SendRequestAsync en se terminant par ##super, la méthode exécutée par le ##super sera celle injectée par la sous classe direct de "AuditCommons".  Ce n'est pas vraiment un problème, car nous pouvons contrôler cela via un "Parameter" ou encore plus simplement avec un Cast, exemple:

Class XXXX.Outils.BS.ComplexMap.Sub Extends XXXX.Outils.BS.ComplexMap.FTP
{

Method SendRequestSync(pTargetDispatchName As%String, pRequest As%Library.Persistent, ByRef pResponse As%Library.Persistent, pTimeout As%Numeric = -1, pDescription As%String = "") As%Status
{
    ; ...; lignes de code spécifiques à cette classe; bla bla; ...; maintenant on délègue à EnsLib.RecordMap.Service.ComplexBatchFTPService sans passer; par la méthode générée dans XXXX.Outils.BS.ComplexMap.FTPQuit##class(EnsLib.RecordMap.Service.ComplexBatchFTPService)##this.SendRequestSync(pTargetDispatchName, pRequest, .pResponse, pTimeout, pDescription)
}

}

Bonjour @Jules Pontois ,

Oui, tout à fait, c'est pas évident à voir.

A ma connaissance, on ne sait pas forcer l'affichage de ses méthodes malheureusement.

Sous VSCode, il faut faire clique droit -> View Other ou shift+ctrl+v (dans le panel avec le de code de la classe) et ça ne fonctionne que si le flag "k" a été utilisé pour compiler:

    k - Keep source.  When this flag is set, source code of
        generated routines will be kept.

donc oui, il est parfois nécessaire de refaire un $SYSTEM.OBJ.Compile(<class>, "brck") sinon nous n'y avons pas accès.
Si quelqu'un à une astuce pour afficher les méthodes générées qu'il n'hésite pas a partager, ça m'interesse aussi 😉

Salut @Julia Pertin ,

Pour créer un un %DynamicObject tu peux utiliser les syntaxes suivantes : 

set obj = {}
; ouSet obj = ##class(%DynamicObject).%New()

ZEN étant déprécié, j'imagine que la classe %ZEN.proxyObject l'est aussi (je déconseil son utilisation dans un nouveau développement ;-) ).  

Il y a eu des changements dans les dernières versions d'IRIS avec la gestion des streams dans les %DynamicObject, j'ai tenté d'adapter votre code en le simplifiant un peu, mais je n'ai pas vraiment pu le tester:

Set obj = {}
    Set update = $CLASSMETHOD(classname,"%OpenId",id)
    Setproperty = $Property(update, propertyName)
    If$Isobject(property), property.%IsA("%Stream.GlobalBinary") {
        Set pnewContent = ##class(%Stream.GlobalCharacter).%New()
        Doproperty.Rewind()
        While 'property.AtEnd {
            Do pnewContent.Write($zcvt(property.ReadLine(),"O", "UTF8"))
        }
        Do obj.%Set(sqlFieldName, pnewContent, "stream>base64")
    }

La méthode %Set avec comme 3ème argument "stream>64" de set le stream dans le %DynamicObject en le convertissant directement en base64.

L'inverse est aussi possible avec la methode %Get (ex : obj.%Get(sqlFieldName,"stream<base64") ).
J'espère que ça aidera.

Bonjour @Jean-Charles.Cano ,

Dans le cadre de script CI\CD il m'est déjà arrivé d'avoir besoin de cela.

Dans mon cas, c'était un système fermé donc j'ai pu simplement dans le service "%Service_Console" coché uniquement "Non authentifié".

Une fois que ce service est configuré avec "No authentifié", la commande 

irissession <instance_name>

ouvrira une session avec l'utilisateur "UnknownUser".  Il faut donc que cet utilisateur soit activé, vous pouvez aussi éventuellement le paramétré pour qu'il se positionne automatiquement sur un namespace.  Pour configurer l'utilisateur depuis le portail d'administration allez dans "Administration système -> Sécurité -> Utilisateurs".

Dans le cas ou cette solution ne serait pas envisageable pour des raisons de sécurités (ce qui serait très compréhensible), je peux essayer de creuser un peu plus la question.  Sur un serveur Linux, on peut aisément mettre le login\password dans le buffer pour que le système effectue la lecture, mais sur Windows, je pense que c'est plus compliqué.

Bonjour @Cyril Grosjean ,

Les droits peuvent être accordés depuis le portail admin "Administration système -> Sécurité -> Utilisateurs".  Toutefois vous devriez pouvoir augmenter les droits par programmation avec la variable spécial $Roles 

par exemple:

Set$Roles = "%ALL"

Donne tous les droits.

Re @Cyril Grosjean , @Jean-Charles.Cano 

J'ai une autre approche à vous proposer, mais ça nécessite que le login\password soit dans un fichier.   (Laissez "Mot de passe" coché dans le service "%Service.console" cette fois-ci).

Il est nécessaire que ce fichier soit encodé avec LF donc Unix Style et pas CR LF comme c'est le cas par défaut sous windows.  Si vous utilisez Notepad++ suffit de cliquer sur "Edit -> EOL Conversion -> Unix (LF)".

Ex:

_system
SYS
Write"Hello !"halt

La première et la deuxième ligne doivent être respectivement le login et password.

vous pouvez ensuite faire : 

irissession instance_name < input.txt

J'insiste sur le fait que le fichier input.txt doit être encodé avec le caractère de fin de ligne LF et non CR LF sinon ça ne fonctionne pas.

Est-ce que cette nouvelle approche peut vous aider ?

Entre-temps, j'ai trouvé une meilleure solution, activez l'authentification OS level : 

ensuite dans le "%Service.Console":

Lorsque je fais cela sur mon installation irissession instance_name ne nécessite plus de mot de passe et la session est démarrée avec le compte windows actuellement connecté.  Cela implique toutefois logiquement de créer un utilisateur IRIS avec le même nom que le compte de connection Windows utilisé.  Dans mon cas, ce compte avait déjà été créé.

en faisant:

Write$Username

Vous pourrez vérifier quel est le compte qui a ouvert la session.
Si cette solution peut fonctionner dans votre environnement, c'est probablement la meilleure et plus sécurisée.

Super!

Finalement laquelle des solutions à fonctionner ?  Celle avec "Allow O/S authentication" et "%Service.Console" avec "Systeme d'exploitation" + "Cache des droits Kerberos" ?

Merci.

Le résultat est vraiment impressionnant.  Il y a certains termes techniques que nous ne sommes pas habitués à traduire, mais ça reste une super performance !

Je n'ai pas le lien dans la documentation, mais saviez-vous que : 

USER>!ping www.google.com
 
 
Pinging www.google.com [142.251.36.4] with 32 bytes of data:
Reply from 142.251.36.4: bytes=32 time=12ms TTL=110
Reply from 142.251.36.4: bytes=32 time=11ms TTL=110
Reply from 142.251.36.4: bytes=32 time=11ms TTL=110
Reply from 142.251.36.4: bytes=32 time=12ms TTL=110
 
Ping statistics for142.251.36.4:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 11ms, Maximum = 12ms, Average = 11ms

ou encore : 

USER>!echo "hello world""hello world"
USER>

Bonjour @Cyril Grosjean ,

Je ne sais pas si ça peut vous aider mais il existe la possibilité d'enregistrer les credentials

Il a très longtemps, j'ai déjà utilisé cela pour stocker login/password pour des requêtes http basic auth.

Je dois avouer que je ne sais plus exactement comment ça fonctionne, mais c'est peut être une piste.

Le point de menu est disponible dans "Interoperability -> Configurer -> Droits"

Bonjour @Pierre LaFay

Oui, il est possible de faire du mapping, mais il faut bien distinguer deux choses:

  1. Package mapping Celui-ci permet de rendre accessible des package d'un namespace à un autre.   Dans le cas d'une classe %Persistent, il faut bien comprendre que seul le code sera mappé. En ce qui concerne les données enregistrée cela nous amène au 2ème point.  
  2. Global Mapping Il permet de rentre accessible les données de vos globals à votre namespace en spécifiant la base de données source.

Ces configurations peuvent s'effectuer par programmation ou plus simplement via le portail d'administration au niveau de la configuration du namespace (voici le lien de la doc officiel si cela peut vous être utile).

Dans le cas qui vous occupe peut être qu'il serait plus simple d'effectuer la requête SQL directement dans "RemoveIrisTestUsers" étant donné que vous faites déjà le changement de namespace.

Toutefois, pour un package de classe utilitaire comme "Bna.Utils" peut être que vous pourriez le mapper sur le namespace %ALL.

Le namespace %ALL, est spécial, les packages (ou les globals et routines) qui y sont mappées sont automatiquement accessible pour tous les autres namespace.  Donc le code : 

Set sc = ##class(Bna.Utils.Sql).SelectFirstColsInArray(query, .userIds)

Pourra être exécuter même si vous venez de faire un Set $Namespace = "%SYS".

Si besoin, je peux vous fournir une réponse plus complète avec des captures d'écran concernant la configuration avec %ALL.

Lorenzo.

Merci @Pierre LaFay .

Content que tu as pu trouver une solution :)

Le mapping même avec le namespace %ALL permet de sélectionner exactement ce que tu souhaites mettre à disposition des autres namespaces (donc pas forcément tout ce qui est dans une DB, une sélection peut être effectuée).  Je souligne ce point juste au cas ou cela te serait utile à l'avenir.

Lorenzo.

Salut @Cyril Grosjean 
 

Ce n'est pas mon domaine d'expertise, mais je suppose que le processus utilise le framework d'interoperabilité en mode synchrone et qu'une transaction (TSTART) est encore ouverte avant l'appel au business service.

Lorsque cette situation se produit, le système effectue automatiquement un TCOMMIT.

Tu peux tenter vérifier cela en plaçant une ligne de debug $TLEVEL (cette variable spécial donne le niveau de transaction courant ou 0 si pas de transaction).

Lorenzo.

Bonjour @Pierre LaFay ,

Effectivement comme l'a mentionné @Sylvain Guilbaud IAM t'offrira pas mal de fonctionnalité.

Toutefois si tu veux juste forcer un "Hang 1" lors du login, tu peux t'en sortir une sous classe de %CSP.SessionEvents, ex:

Class dc.pierre.RestEvents Extends%CSP.SessionEvents
{

ClassMethod OnStartRequest() As%Status
{
	#dim%requestAs%CSP.RequestSet^dc.pierre("OnStartRequest", "LastRequest") = $ZDT($Horolog, 3, 1) _ " " _ %request.URLSet loginURL = "/login"If$Extract(%request.URL, * - $Length(loginURL) + 1, *) = loginURL {	; Vérifie si l'url se termine par /loginHang1
	}
	
	Quit$$$OK
}

}

Il faut alors configurer l'application Web pour utiliser cette classe d'évènements.

Cela peut se paramétrer via le portail admin gestion de la sécurité -> Application Web, ex : 

Lorenzo.

Bonjour @Pierre LaFay ,

Avec plaisir!

Pour le OnLogin, je n'ai pas essayé, mais pour le OnEndRequest j'ai tenté le coup et ce fut un échec :D

Malgré que le OnEndRequest soit bien exécuté, j'ai l'impression qu'il est "asynchrone" ou plutôt qu'il est exécuté après que la réponse soit envoyée au client.  Donc même si on fait Hang dans le OnEndRequest, c'est déjà trop tard malheureusement.

N'hésite pas à partager tes découvertes :)

Merci pour la traduction de cet intéressant article.
J'en profite pour partager ici mon extrait de code que j'utilise souvent comme base pour le SQL Dynamic:

Set sc = $$$OKSet sql = ""; put your sql querySet args($Increment(args)) = "";Set args($Increment(args)) = "";Set args($Increment(args)) = ""Set tStatement = ##class(%SQL.Statement).%New()
    ; Set tStatement.%SelectMode = 2    #Dim tRes As%SQL.StatementResult = ##class(%SQL.Statement).%ExecDirect(.tStatement, sql, args...)
    If tRes.%SQLCODE < 0Return$$$ERROR($$$SQLError, tRes.%SQLCODE, tRes.%Message)

    While tRes.%Next(.sc) {
        Quit:$$$ISERR(sc)

        /// do something
    }

    If$$$ISERR(sc) Return sc