diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 097ce86d..d118a661 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -31,7 +31,7 @@ jobs: do # Skip irishealth-community due to bad interaction with ZPM document type # Also skip 2023.2 because the license has expired - if [ "$n" = "irishealth-community" -a "$tag" = "2023.3" -o "$tag" = "2023.2" ]; + if [ "$tag" = "2023.3" -o "$tag" = "2023.2" ]; then continue fi diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f74a6646..f8df9fdb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -171,7 +171,7 @@ jobs: run: | curl http://localhost:52773/registry/packages/-/all | jq curl http://localhost:52773/registry/packages/zpm/ | jq - ASSET_NAME='zpm-0.7.2.xml' + ASSET_NAME='zpm-0.7.3.xml' ASSET_URL=`wget --header "Authorization: token ${GITHUB_TOKEN}" -qO- https://api.github.com/repos/intersystems/ipm/releases | jq -r ".[].assets[] | select(.name == \"${ASSET_NAME}\") | .browser_download_url"` wget $ASSET_URL -O /tmp/zpm.xml CONTAINER=$(docker run --network zpm --rm -d ${{ steps.image.outputs.name }} ${{ steps.image.outputs.flags }}) diff --git a/CHANGELOG.md b/CHANGELOG.md index e49430a9..9f1cccc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #474 Added compatibility to load ".tar.gz" archives in addition to ".tgz" - #469 Added ability to include an `IPMVersion` in `SystemRequirement` of module.xml - #530 Added a `CustomPhase` attribute to `` that doesn't require a corresponding %method in lifecycle class. +- #578 Added functionality to record and display zpm history of install, uninstall, and load - #582 Added functionality to optionally see time of last update and server version of each package - #609 Added support for `-export-deps` when running the "Package" phase of lifecycle diff --git a/preload/cls/IPM/Installer.cls b/preload/cls/IPM/Installer.cls index c8aa2751..e585e79b 100644 --- a/preload/cls/IPM/Installer.cls +++ b/preload/cls/IPM/Installer.cls @@ -102,6 +102,7 @@ ClassMethod ZPMInit(pRegistry As %String = "", pAnalyticsTrackingID As %String = $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetAnalyticsAvailable(1, 0)) $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetAnalyticsTrackingId(pAnalyticsTrackingID, 0)) $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("ColorScheme","", 0)) + $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("DefaultLogEntryLimit",20, 0)) Quit $$$OK } diff --git a/src/cls/IPM/DataType/RepoMoniker.cls b/src/cls/IPM/DataType/RepoMoniker.cls new file mode 100644 index 00000000..47e4e4b7 --- /dev/null +++ b/src/cls/IPM/DataType/RepoMoniker.cls @@ -0,0 +1,6 @@ +Class %IPM.DataType.RepoMoniker Extends %IPM.DataType.RegExString [ ClassType = datatype ] +{ + +Parameter MAXLEN = 100; + +} diff --git a/src/cls/IPM/DataType/RepoName.cls b/src/cls/IPM/DataType/RepoName.cls new file mode 100644 index 00000000..169f569a --- /dev/null +++ b/src/cls/IPM/DataType/RepoName.cls @@ -0,0 +1,6 @@ +Class %IPM.DataType.RepoName Extends %IPM.DataType.RegExString [ ClassType = datatype ] +{ + +Parameter MAXLEN = 100; + +} diff --git a/src/cls/IPM/General/History.cls b/src/cls/IPM/General/History.cls new file mode 100644 index 00000000..50351a1a --- /dev/null +++ b/src/cls/IPM/General/History.cls @@ -0,0 +1,524 @@ +Include (%IPM.Common, %IPM.Formatting) + +/// Persistent class for history of all installations, uninstalls, and loads in the system. This class is shared among namespaces that share %IPM mappings. +Class %IPM.General.History Extends %Persistent +{ + +/// Action of this history record. Can be load, install, or uninstall +Property Action As %String(VALUELIST = ",load,install,uninstall") [ Required ]; + +/// Name of the package being loaded/installed/uninstall. This is not necessarily requried. E.g., when loading a nonexistent directory. +Property Package As %IPM.DataType.ModuleName; + +/// Version of the package being loaded/installed/uninstall. This is not necessarily requried. E.g., when loading a nonexistent directory. +Property Version As %IPM.General.SemanticVersion [ Required ]; + +/// Name of the repository where the package is being installed. For load and uninstall, this will be empty. +Property SourceName As %IPM.DataType.RepoName(MAXLEN = 100); + +/// Moniker of the repository where the package is being installed. For load and uninstall, this will be empty. +Property SourceMoniker As %IPM.DataType.RepoMoniker(MAXLEN = 100); + +/// Details of the repository where the package is being installed. For load and uninstall, this will be empty. +Property SourceDetails As %String(MAXLEN = ""); + +/// Time when the action started +Property TimeStart As %TimeStamp [ Required ]; + +/// Time when the action ended. This will be empty if the action is still in progress or ended abnormally. +Property TimeEnd As %TimeStamp; + +/// Namespace where the action is being performed +Property NameSpace As %String [ InitialExpression = {$NAMESPACE}, Required ]; + +/// User who initiated the action +Property UserName As %String [ InitialExpression = {$USERNAME}, Required ]; + +/// Status of the action. If the action is still in progress, this will be 0. Otherwise, it will be the status code of the action. +Property Success As %Status [ InitialExpression = 0, Required ]; + +/// Whether the action is committed. In non developer mode, this indicates whether the action was successful. In developer mode, this is always true. +Property Committed As %Boolean [ InitialExpression = 0, Required ]; + +/// The command string that triggered the action +Property CommandString As %String(MAXLEN = 8192) [ Required ]; + +ClassMethod Init(Action As %String, Package As %IPM.DataType.ModuleName) As %IPM.General.History [ Private ] +{ + Set log = ..%New() + Set log.TimeStart = $ZDateTime($Now(), 3) + Set log.Action = Action + Set log.Package = Package + Set log.CommandString = $Get($$$ZPMCommandToLog, "") + Set log.NameSpace = $Namespace + Set log.UserName = $Username + $$$ThrowOnError(log.SetVersion()) // Set the version to placeholder 0.0.0-0, Will be updated before finalizing + $$$ThrowOnError(log.%Save()) // Save it now in case Finalize isn't called + Quit log +} + +ClassMethod InstallInit(Package As %IPM.DataType.ModuleName) As %IPM.General.History +{ + Quit ..Init("install", Package) +} + +ClassMethod LoadInit() As %IPM.General.History +{ + // Package name is not known at this point, so use a placeholder + Quit ..Init("load", "") +} + +ClassMethod UninstallInit(Package As %IPM.DataType.ModuleName) As %IPM.General.History +{ + Quit ..Init("uninstall", Package) +} + +Method SetSource(ServerName As %String) As %Status +{ + $$$ThrowOnError(..%Reload()) + Set server = ##class(%IPM.Repo.Definition).ServerDefinitionKeyOpen(ServerName) + If '$IsObject(server) { + Quit $$$ERROR($$$GeneralError,$$$FormatText("Repository '%1' is not defined.", ServerName)) + } + Set ..SourceName = ServerName + Set ..SourceMoniker = server.#MONIKER + Set ..SourceDetails = server.Details + Quit ..%Save() +} + +Method SetVersion(pVersion As %IPM.General.SemanticVersion) As %Status +{ + If ..%Id() '= "" { + $$$ThrowOnError(..%Reload()) + } + If $Get(pVersion) = "" { + Set pVersion = "0.0.0-0" + } + Set ..Version = $Select($IsObject(pVersion): pVersion, 1: ##class(%IPM.General.SemanticVersion).FromString(pVersion)) + Quit ..%Save() +} + +Method SetName(pName As %String) As %Status +{ + $$$ThrowOnError(..%Reload()) + Set ..Package = pName + Quit ..%Save() +} + +Method Finalize(status As %Status, dev As %Boolean = 0) As %Status +{ + $$$ThrowOnError(..%Reload()) + Set ..Success = status + Set ..Committed = (dev = 1) || $$$ISOK(status) + Set ..TimeEnd = $ZDateTime($Now(), 3) + Return ..%Save() +} + +/// @API.Query +/// Get load/install/uninstall records in the specified namespace +Query Records(ascend As %Boolean, limit As %Integer, namespace As %String) As %Query(CONTAINID = 1, ROWSPEC = "Action:%String,Package:%String,Version_Major:%Integer,Version_Minor:%Integer,Version_Patch:%Integer,Version_Prerelease:%String,Version_Build:%String,SourceName:%String,SourceMoniker:%String,SourceDetails:%String,TimeStart:%String,TimeEnd:%String,NameSpace:%String,UserName:%String,Success:%String,Committed:%String,CommandString:%String") [ SqlProc ] +{ +} + +ClassMethod RecordsExecute(ByRef qHandle As %Binary, ascend As %Boolean, limit As %Integer, namespace As %String) As %Status [ Internal ] +{ + Try { + Kill filter + Set qHandle = ..GetHistory(.filter, .ascend, .limit, .namespace) + } Catch e { + Return e.AsStatus() + } + Return $$$OK +} + +ClassMethod RecordsFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ Internal, PlaceAfter = RecordsExecute ] +{ + Try { + If qHandle.%Next() { + Set Row = $ListBuild( + qHandle.%Get("Action"), + qHandle.%Get("Package"), + qHandle.%Get("Version_Major"), + qHandle.%Get("Version_Minor"), + qHandle.%Get("Version_Patch"), + qHandle.%Get("Version_Prerelease"), + qHandle.%Get("Version_Build"), + qHandle.%Get("SourceName"), + qHandle.%Get("SourceMoniker"), + qHandle.%Get("SourceDetails"), + qHandle.%Get("TimeStart"), + qHandle.%Get("TimeEnd"), + qHandle.%Get("NameSpace"), + qHandle.%Get("UserName"), + qHandle.%Get("Success"), + qHandle.%Get("Committed"), + qHandle.%Get("CommandString") + ) + Set AtEnd = 0 + } Else { + Set Row = "" + Set AtEnd = 1 + } + } Catch ex { + Return ex.AsStatus() + } + Return $$$OK +} + +ClassMethod RecordsClose(ByRef qHandle As %Binary) As %Status [ Internal, PlaceAfter = RecordsFetch ] +{ + Set qHandle = "" + Quit $$$OK +} + +/// Get an SQL "where" clause based on the filter argument +/// See GetHistory() for the `filter` argument +/// Output "clause" is the SQL WHERE clause as a string, container zero or more ? placeholders +/// The "varargs" is an array of values to be substituted in the ? placeholders. +/// The "varargs" may already be populated with values, and new values will be appended to it. +ClassMethod ConstructSQLWhere(filter As %String, namespace As %String, Output clause As %String, ByRef varargs As %String) As %Status [ Internal ] +{ + Try { + Set clause = " WHERE 1 = 1 " + Set namespace = $ZStrip($Get(namespace, $Namespace), "<>WC") + If (namespace '= "") { + Set clause = clause _ " AND NameSpace = ?" + Set varargs($Increment(varargs)) = namespace + } + Set col = "" + For { + Set col = $Order(filter(col), 1, value) + If (col = "") || ($Data(value) # 2 = 0) || ('$Match(col, "^[a-zA-Z][a-zA-Z0-9]*$")) { + Quit + } + // col is safe from SQL injection because we ensured it's alphanumeric above + If $ListFind($ListBuild(">=", "<=", "<>"), $Extract(value, 1, 2)) { + Set clause = clause _ " AND " _ col _ $Extract(value, 1, 2) _ "?" + Set varargs($Increment(varargs)) = $Extract(value, 3, *) + } ElseIf $ListFind($ListBuild(">", "<", "="), $Extract(value, 1, 1)) { + Set clause = clause _ " AND " _ col _ $Extract(value, 1) _ "?" + Set varargs($Increment(varargs)) = $Extract(value, 2, *) + } ElseIf value [ "*" { + Set clause = clause _ " AND " _ col _ " LIKE ?" + Set varargs($Increment(varargs)) = $Replace(value, "*", "%") + } Else { + Set clause = clause _ " AND " _ col _ "=?" + Set varargs($Increment(varargs)) = value + } + } + Set clause = clause _ " " // Ensure there's a space at the end + } catch ex { + Return ex.AsStatus() + } + Return $$$OK +} + +/// Get the history of all installations, uninstalls, and loads in given namespace +/// The filter argument is a multidimensional array with structure +/// filter(columnName) = value +/// Where value can optionally start with >, >=, <, <=, =, <> or contain * to indicate a wildcard +ClassMethod GetHistory(ByRef filter, ascend As %Boolean = 0, limit As %Integer = 0, namespace As %String) As %SQL.StatementResult +{ + Set limit = +limit // Coerce to number to prevent SQL injection + Set query = "SELECT " _ $SELECT(limit>0: "TOP " _ limit, 1: "") _ " * FROM %IPM_General.History " + $$$ThrowOnError(..ConstructSQLWhere(.filter, .namespace, .where, .varargs)) + Set query = query _ where + Set query = query _ " ORDER BY ID " _ $Select(ascend: "ASC", 1: "DESC") + Quit ##class(%SQL.Statement).%ExecDirect(, query, varargs...) +} + +/// Delete the history of installations, uninstalls, and loads in +ClassMethod DeleteHistory(ByRef filter, namespace As %String, allowDeleteAll As %String) As %Integer +{ + Set query = "DELETE FROM %IPM_General.History " + $$$ThrowOnError(..ConstructSQLWhere(.filter, namespace, .where, .varargs)) + If ($Data(varargs) = 0) && ('allowDeleteAll) { + $$$ThrowOnError("Cannot delete all history records by default. If you want to delete everything, use `history delete -g`.") + } + Set query = query _ where + Set rs = ##class(%SQL.Statement).%ExecDirect(, query, varargs...) + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + Quit rs.%ROWCOUNT +} + +/// @API.Query +Query GlobalRecords(ascend As %Boolean, limit As %Integer) As %Query(ROWSPEC = "Action:%String,Package:%String,Version_Major:%Integer,Version_Minor:%Integer,Version_Patch:%Integer,Version_Prerelease:%String,Version_Build:%String,SourceName:%String,SourceMoniker:%String,SourceDetails:%String,TimeStart:%String,TimeEnd:%String,NameSpace:%String,UserName:%String,Success:%String,Committed:%String,CommandString:%String") [ SqlProc ] +{ +} + +ClassMethod GlobalRecordsExecute(ByRef qHandle As %Binary, ascend As %Boolean, limit As %Integer) As %Status [ Internal ] +{ + Try { + Kill filter + Set qHandle = ..GetHistoryGlobally(.filter, .ascend, .limit) + } Catch e { + Return e.AsStatus() + } + Return $$$OK +} + +ClassMethod GlobalRecordsFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ Internal, PlaceAfter = GlobalRecordsExecute ] +{ + Try { + While qHandle.Count() > 0 { + // Always get the first namespace. Remove it from list once corresponding result set is exhausted. + Set ns = "" + Set rs = qHandle.GetNext(.ns) + $$$ThrowOnError(..RecordsFetch(rs, .Row, .AtEnd)) + If AtEnd { + // If rs is empty, there could still be more namespaces to process + Do qHandle.RemoveAt(ns, .success) + If 'success { + $$$ThrowStatus($$$ERROR($$$GeneralError, "Failed to remove namespace "_ns_" from list")) + } + } Else { + Return $$$OK + } + } + // Only when the list is empty should we return AtEnd = 1 and Row = "" + Set Row = "", AtEnd = 1 + Return $$$OK + } Catch ex { + Return ex.AsStatus() + } + Return $$$OK +} + +ClassMethod GlobalRecordsClose(ByRef qHandle As %Binary) As %Status [ Internal, PlaceAfter = GlobalRecordsFetch ] +{ + // Should not happen, but just in case + If qHandle.Count() '= 0 { + $$$ThrowStatus($$$ERROR($$$GeneralError, "Namespace list not empty in GlobalRecordsFetch()!")) + } + Set qHandle = "" + Quit $$$OK +} + +/// Get the history of all installations, uninstalls, and loads in all namespaces +/// See GetHistory() for the `filter` argument +ClassMethod GetHistoryGlobally(ByRef filter, ascend As %Boolean = 0, limit As %Integer = 0) As %Library.ArrayOfObjects +{ + Set originalNS = $Namespace + New $Namespace + Set $Namespace = "%SYS" + Set rs = ##class(%SQL.Statement).%ExecDirect(, "SELECT DISTINCT Nsp FROM %SYS.Namespace_List()") + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + Set $Namespace = originalNS + Set array = ##class(%Library.ArrayOfObjects).%New() + While rs.%Next() { + Set ns = rs.%Get("Nsp") + Set historyRS = ..GetHistory(.filter, ascend, limit, ns) + If historyRS = $$$NULLOREF { + Continue + } + Do array.SetAt(historyRS, ns) + } + Return array +} + +ClassMethod DisplayOneRecord(id As %Integer) +{ + Set rs = ##class(%SQL.Statement).%ExecDirect(, "SELECT TOP 1 * FROM %IPM_General.History WHERE ID = ?", id) + If rs.%Next() { + Do ..ShowColumns(rs, 1) + } Else { + $$$ThrowStatus($$$ERROR($$$GeneralError, "No record found with ID "_id_". Use `history find` command to show all entries. ")) + } +} + +/// If detail is 1, will display full details each property on a separate line +/// If detail is "header", will display the column names using the same style as the data +/// Otherwise, will display on a single line, with some details omitted +ClassMethod ShowColumns(rs As %SQL.StatementResult, detail As %Boolean = 0) As %Status [ Internal, Private ] +{ + Set columnStyles = $ListBuild( + $ListBuild("ID", $$$Bright), + $ListBuild("Action", $$$Red), + $ListBuild("Package", $$$Green), + $ListBuild("Version", $$$Green), + $ListBuild("UserName", $$$Blue), + $ListBuild("Namespace", $$$Magenta), + $ListBuild("Time", $$$Yellow), + $ListBuild("Success", $$$Default), + $ListBuild("Committed", $$$Cyan), + $ListBuild("CommandString", $$$Red), + $ListBuild("Source", $$$Default) + ) + + Set ptr = 0 + Write ! + While $ListNext(columnStyles, ptr, pair) { + Set $ListBuild(column, style) = pair + If detail = "header" { + Write $$$FormattedLine(style, column), " " + Continue + } + Set cls = $classname() + Set mthd = column_"ToString" + If $System.CLS.IsMthd(cls, mthd) { + Set value = $classmethod(cls, mthd, rs, detail) + } Else { + Set value = rs.%Get(column) + } + Set value = $Replace(value, $Char(13), $$$FormattedLine($$$Dim, "")) + Set value = $Replace(value, $Char(10), $$$FormattedLine($$$Dim, "")) + Set value = $Replace(value, $Char(9), $$$FormattedLine($$$Dim, "")) + Set value = $$$FormattedLine(style, value) + If detail { + Write $$$FormattedLine($$$Dim, column), ": ", value, ! + } Else { + Write value, " " + } + } + Return $$$OK +} + +ClassMethod VersionToString(rs As %SQL.StatementResult, detail As %Boolean = 0) As %String [ Internal, Private ] +{ + Set obj = ##class(%IPM.General.History).%OpenId(rs.%Get("ID")) + If $IsObject(obj) { + Set str = obj.Version.ToString() + If str '= "0.0.0-0" { + Quit "v"_str + } + } + Quit "" +} + +ClassMethod TimeToString(rs As %SQL.StatementResult, detail As %Boolean = 0) As %String [ Internal, Private ] +{ + If detail { + Quit "Started At " _ rs.%Get("TimeStart") _ "; Ended At " _ rs.%Get("TimeEnd") + } + + Set start = rs.%Get("TimeStart") + Set end = rs.%Get("TimeEnd") + If end = "" { + Quit rs.%Get("TimeStart") + } + Set diff = $System.SQL.DATEDIFF("ss", start, end) + + Quit $$$FormatText("%1 (%2 sec)", start, diff) +} + +ClassMethod SourceToString(rs As %SQL.StatementResult, detail As %Boolean = 0) As %String [ Internal, Private ] +{ + If rs.%Get("Action") '= "install" { + Quit "n/a" + } + If rs.%Get("SourceName") = "" { + Quit "" + } + If detail { + Quit rs.%Get("SourceName") _ " (" _ rs.%Get("SourceMoniker") _ "): " _ rs.%Get("SourceDetails") + } Else { + Quit rs.%Get("SourceName") + } +} + +ClassMethod SuccessToString(rs As %SQL.StatementResult, detail As %Boolean = 0) As %String [ Internal, Private ] +{ + Set success = rs.%Get("Success") + If detail { + Quit success + } + Quit $Select($$$ISOK(success): "Success", 1:$System.Status.GetErrorText(success)) +} + +ClassMethod CommittedToString(rs As %SQL.StatementResult, detail As %Boolean = 0) As %String [ Internal, Private ] +{ + Quit $Select(rs.%Get("Committed"): "Committed", 1: "Uncommitted") +} + +ClassMethod DisplayQueryResult(rs As %SQL.StatementResult, namespace As %String) +{ + // TODO improve the display output + If (rs = $$$NULLOREF) { + Write !, "IPM is not enabled for this namespace" + Quit + } + + Set found = 0 + While rs.%Next() { + If 'found { + Set found = 1 + If $Data(namespace) { + Write !, "Namespace: ", namespace + } + Do ..ShowColumns(rs, "header") + Write ! + } + Do ..ShowColumns(rs, 0) + } + If found { + Write ! + } +} + +ClassMethod DisplayArray(array As %Library.ArrayOfObjects) +{ + Set ns = "" + For { + Set rs = array.GetNext(.ns) + If rs = "" { + Quit + } + Do ..DisplayQueryResult(rs, ns) + } +} + +Storage Default +{ + + +%%CLASSNAME + + +Action + + +Package + + +Version + + +SourceName + + +SourceMoniker + + +SourceDetails + + +TimeStart + + +TimeEnd + + +NameSpace + + +UserName + + +Success + + +Committed + + +CommandString + + +^%IPM.General.HistoryD +HistoryDefaultData +^%IPM.General.HistoryD +^%IPM.General.HistoryI +^%IPM.General.HistoryS +%Storage.Persistent +} + +} diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index f2ef2389..3afae305 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -649,6 +649,26 @@ generate /my/path -export 00000,PacketName2,IgnorePacket2^00000,PacketName3,Igno + + Manage history of package installation/uninstallation. + Manage history of package installation/uninstallation. Commands logged are install, load, and uninstall. By default, all entries will be affected. To specify filters, use the -Dcol=val syntax. See examples for more. + + + + + + + history find + history find -DCommandString="load*" + history find -DTimeStart=">2000-01-01 00:00:00" + history find -sort asc -limit 5 + history schema + history find -Daction=install -Dpackage=zpip + history find -globally -Daction=install -Dpackage=zpip + history delete + history details 3 + + } @@ -764,6 +784,10 @@ ClassMethod ShellInternal(pCommand As %String, Output pException As %Exception.A Do ..GetNamespaceDefaultModifiers(.tDefaultArray) Merge tCommandInfo("data") = tDefaultArray Merge tCommandInfo = tParsedCommandInfo + + // Stashed for use in %IPM.General.History:Init + New $$$ZPMCommandToLog + Set $$$ZPMCommandToLog = tCommand If (tCommandInfo = "quit") { Return @@ -818,6 +842,8 @@ ClassMethod ShellInternal(pCommand As %String, Output pException As %Exception.A Do ..Namespace(.tCommandInfo) } ElseIf (tCommandInfo = "enable") { Do ..EnableIPM(.tCommandInfo) + } ElseIf (tCommandInfo = "history") { + Do ..History(.tCommandInfo) } } Catch pException { If (pException.Code = $$$ERCTRLC) { @@ -1889,6 +1915,19 @@ ClassMethod LoadFromRepo(tDirectoryName, ByRef tParams) [ Internal ] } ClassMethod Load(ByRef pCommandInfo) [ Internal ] +{ + Set devMode = $Get(pCommandInfo("data", "DeveloperMode"), 1) + Set log = ##class(%IPM.General.History).LoadInit() + Try { + Do ..LoadInternal(.pCommandInfo, log) + } Catch Ex { + $$$ThrowOnError(log.Finalize(Ex.AsStatus(), devMode)) + Throw Ex + } + $$$ThrowOnError(log.Finalize($$$OK, devMode)) +} + +ClassMethod LoadInternal(ByRef pCommandInfo, pLog As %IPM.General.History) [ Internal ] { Set tDirectoryName = $Get(pCommandInfo("parameters","path")) Merge tParams = pCommandInfo("data") @@ -1899,7 +1938,7 @@ ClassMethod Load(ByRef pCommandInfo) [ Internal ] } If ##class(%File).DirectoryExists(tDirectoryName) { Set tParams("DeveloperMode") = $Get(tParams("DeveloperMode"), 1) - $$$ThrowOnError(##class(%IPM.Utils.Module).LoadNewModule(tDirectoryName,.tParams)) + $$$ThrowOnError(##class(%IPM.Utils.Module).LoadNewModule(tDirectoryName,.tParams, , , pLog)) } ElseIf ##class(%File).Exists(tDirectoryName) && (($$$lcase($Piece(tDirectoryName,".", *))="tgz") || ($$$lcase($Piece(tDirectoryName,".", *-1, *))="tar.gz")) { Set tTargetDirectory = $$$FileTempDirSys If $Get(pCommandInfo("data", "Verbose")) { @@ -1928,7 +1967,7 @@ ClassMethod Load(ByRef pCommandInfo) [ Internal ] Write !, "Using module.xml file at ", tModuleFilePath } } - $$$ThrowOnError(##class(%IPM.Utils.Module).LoadNewModule(tTargetDirectory, .tParams)) + $$$ThrowOnError(##class(%IPM.Utils.Module).LoadNewModule(tTargetDirectory, .tParams, , , pLog)) } } @@ -1939,73 +1978,84 @@ ClassMethod Install(ByRef pCommandInfo) [ Internal ] If (tModuleName["/") { Set $ListBuild(tRegistry, tModuleName) = $ListFromString(tModuleName, "/") } - If (tModuleName = "") { - Quit $$$OK - } + If (tModuleName = "") { + Quit $$$OK + } Set tVersion = $Get(pCommandInfo("parameters","version")) Set tKeywords = $$$GetModifier(pCommandInfo,"keywords") + + #dim log As %IPM.General.History + Set log = ##class(%IPM.General.History).InstallInit(tModuleName) + Set devMode = $Get(tParams("DeveloperMode"), 0) - Set tSearchCriteria = ##class(%IPM.Repo.SearchCriteria).%New() - Set tSearchCriteria.Registry = tRegistry - Set tSearchCriteria.Name = $$$lcase(tModuleName) - Set tSearchCriteria.VersionExpression = tVersion - Set tSearchCriteria.Keywords = tKeywords - $$$ThrowOnError(##class(%IPM.Repo.Utils).SearchRepositoriesForModule(tSearchCriteria,.tResults)) - - If (tResults.Count() > 0) { - Set tResult = "" - #dim tResult As %IPM.Storage.QualifiedModuleInfo - // Results are ordered by semantic version, descending. (So the "latest" version will always be first.) - If ('$$$HasModifier(pCommandInfo,"prompt") || (tResults.Count() = 1)) && (tKeywords = "") { - Set tResult = tResults.GetAt(1) - } ElseIf (tResults.Count() > 0) { - For i=1:1:tResults.Count() { - Set tResultInfo = tResults.GetAt(i) - Set tOptArray(i) = tResultInfo.DisplayName_" "_tResultInfo.VersionString_" @ "_tResultInfo.ServerName + Try { + Set tSearchCriteria = ##class(%IPM.Repo.SearchCriteria).%New() + Set tSearchCriteria.Registry = tRegistry + Set tSearchCriteria.Name = $$$lcase(tModuleName) + Set tSearchCriteria.VersionExpression = tVersion + Set tSearchCriteria.Keywords = tKeywords + $$$ThrowOnError(##class(%IPM.Repo.Utils).SearchRepositoriesForModule(tSearchCriteria,.tResults)) + + If (tResults.Count() > 0) { + Set tResult = "" + #dim tResult As %IPM.Storage.QualifiedModuleInfo + // Results are ordered by semantic version, descending. (So the "latest" version will always be first.) + If ('$$$HasModifier(pCommandInfo,"prompt") || (tResults.Count() = 1)) && (tKeywords = "") { + Set tResult = tResults.GetAt(1) + } ElseIf (tResults.Count() > 0) { + For i=1:1:tResults.Count() { + Set tResultInfo = tResults.GetAt(i) + Set tOptArray(i) = tResultInfo.DisplayName_" "_tResultInfo.VersionString_" @ "_tResultInfo.ServerName + } + + Set tValue = "" + Set tResponse = ##class(%Library.Prompt).GetMenu("Which version?",.tValue,.tOptArray,,$$$InitialDisplayMask+$$$EnableQuitCharMask) + If (tResponse '= $$$SuccessResponse) { + $$$ThrowStatus($$$ERROR($$$GeneralError,"Operation cancelled.")) + } + + If (tValue '= "") { + Set tResult = tResults.GetAt(tValue) + } } - Set tValue = "" - Set tResponse = ##class(%Library.Prompt).GetMenu("Which version?",.tValue,.tOptArray,,$$$InitialDisplayMask+$$$EnableQuitCharMask) - If (tResponse '= $$$SuccessResponse) { - $$$ThrowStatus($$$ERROR($$$GeneralError,"Operation cancelled.")) - } - - If (tValue '= "") { - Set tResult = tResults.GetAt(tValue) + If (tResult '= "") { + Do ##class(%IPM.Lifecycle.Base).GetDefaultParameters(.tParams) + Merge tParams = pCommandInfo("data") + Set tParams("DeveloperMode") = devMode + Set tParams("cmd") = "install" + Set tParams("Install") = 1 + If tResult.Deployed { + Set platformVersion = $System.Version.GetMajor() _ "." _$System.Version.GetMinor() + Set tResult.PlatformVersion = platformVersion + If ('tResult.PlatformVersions.Find(platformVersion)) { + $$$ThrowStatus($$$ERROR($$$GeneralError, "Deployed package '" _ tModuleName _ "' " _ tResult.VersionString _ " not supported on this platform " _ platformVersion _ ".")) + } + } + $$$ThrowOnError(log.SetSource(tResult.ServerName)) + $$$ThrowOnError(log.SetVersion(tResult.Version)) + $$$ThrowOnError(##class(%IPM.Utils.Module).LoadQualifiedReference(tResult, .tParams)) } - } - - If (tResult '= "") { - Do ##class(%IPM.Lifecycle.Base).GetDefaultParameters(.tParams) - Merge tParams = pCommandInfo("data") - Set tParams("DeveloperMode") = $Get(tParams("DeveloperMode"), 0) - Set tParams("cmd") = "install" - Set tParams("Install") = 1 - If tResult.Deployed { - Set platformVersion = $System.Version.GetMajor() _ "." _$System.Version.GetMinor() - Set tResult.PlatformVersion = platformVersion - If ('tResult.PlatformVersions.Find(platformVersion)) { - $$$ThrowStatus($$$ERROR($$$GeneralError, "Deployed package '" _ tModuleName _ "' " _ tResult.VersionString _ " not supported on this platform " _ platformVersion _ ".")) - } - } - $$$ThrowOnError(##class(%IPM.Utils.Module).LoadQualifiedReference(tResult, .tParams)) - - } - } Else { - Set tPrefix = "" - If (tModuleName '= "") { - If (tVersion '= "") { - $$$ThrowStatus($$$ERROR($$$GeneralError, tModuleName_" "_tVersion_" not found in any repository.")) + } Else { + Set tPrefix = "" + If (tModuleName '= "") { + If (tVersion '= "") { + $$$ThrowStatus($$$ERROR($$$GeneralError, tModuleName_" "_tVersion_" not found in any repository.")) + } Else { + $$$ThrowStatus($$$ERROR($$$GeneralError, "'"_tModuleName_"' not found in any repository.")) + } + } ElseIf (tKeywords '= "") { + $$$ThrowStatus($$$ERROR($$$GeneralError,"No modules found matching keywords: '"_tKeywords_"'")) } Else { - $$$ThrowStatus($$$ERROR($$$GeneralError, "'"_tModuleName_"' not found in any repository.")) + Write !,"No modules found. Are there any repositories configured in the current namespace?" } - } ElseIf (tKeywords '= "") { - $$$ThrowStatus($$$ERROR($$$GeneralError,"No modules found matching keywords: '"_tKeywords_"'")) - } Else { - Write !,"No modules found. Are there any repositories configured in the current namespace?" } + } Catch ex { + $$$ThrowOnError(log.Finalize(ex.AsStatus(), devMode)) + Throw ex } + $$$ThrowOnError(log.Finalize($$$OK, devMode)) } ClassMethod Reinstall(ByRef pCommandInfo) [ Internal ] @@ -2036,9 +2086,9 @@ ClassMethod Uninstall(ByRef pCommandInfo) [ Internal ] { Merge tParams = pCommandInfo("data") Set tForce = $$$HasModifier(pCommandInfo,"force") // Force uninstallation even if things depend on this module + If $$$HasModifier(pCommandInfo,"all") { $$$ThrowOnError(##class(%IPM.Utils.Module).UninstallAll(tForce,.tParams)) - Return } Else { Set tModuleName = pCommandInfo("parameters","module") Set tRecurse = $$$HasModifier(pCommandInfo,"recurse") // Recursively uninstall unneeded dependencies @@ -2894,6 +2944,50 @@ ClassMethod EnableIPM(ByRef pCommandInfo) } } +ClassMethod History(ByRef pCommandInfo) +{ + Set action = $Get(pCommandInfo("parameters", "action")) + + Set globally = $$$HasModifier(pCommandInfo, "globally") + Set ascend = ($$$GetModifier(pCommandInfo, "sort") = "asc") + Set confirm = $$$HasModifier(pCommandInfo, "confirm") + Set limit = $$$GetModifier(pCommandInfo, "limit") + Set limit = $Select(limit="": ##class(%IPM.Repo.UniversalSettings).GetValue("DefaultLogEntryLimit"), limit=0: "", 1: +limit) + Merge filter = pCommandInfo("data", "zpm") + + If (action = "find") || (action = "") { + Set mthd = $Select(globally: "GetHistoryGlobally", 1: "GetHistory") + Set rs = $Classmethod("%IPM.General.History", mthd, .filter, ascend, limit) + Set mthd = $Select(globally: "DisplayArray", 1: "DisplayQueryResult") + Do $Classmethod("%IPM.General.History", mthd, rs) + } ElseIf action = "details" { + Set id = $Get(pCommandInfo("parameters", "argument")) + Do ##class(%IPM.General.History).DisplayOneRecord(id) + } ElseIf action = "schema" { + $$$ThrowOnError($System.SQL.Schema.GetAllColumns("%IPM_General.History", , .columns)) + Write "Columns available for filtering are: " + Set idx = "" + For { + Set idx = $Order(columns(idx), 1, col) + If idx = "" { + Quit + } + If $Extract(col, 1, 3) = "x__" { + Continue + } + Write !, "- ", col + } + } ElseIf action = "delete" { + If ($Data(filter) \ 2 = 0) && ('globally) && ('confirm) { + $$$ThrowStatus($$$ERROR($$$GeneralError,"No filter specified. Should specify at least one column to filter on or pass -confirm to delete all records in the current namespace.")) + } + Set count = ##class(%IPM.General.History).DeleteHistory(.filter, $Select(globally: "", 1: $Namespace), globally) + Write !, "Deleted ", count, " record(s)." + } Else { + $$$ThrowStatus($$$ERROR($$$GeneralError,"Invalid action specified. Use `help history` to see available actions.")) + } +} + /// Runs package manager commands in a way that is friendly to the OS-level shell. /// Creates pOutputLogFile if it does not exist. /// If it does, and pAppendToLog is true, appends to it; otherwise, deletes the file before outputting to it. diff --git a/src/cls/IPM/Repo/Definition.cls b/src/cls/IPM/Repo/Definition.cls index 8ed2d48f..ffb21df8 100644 --- a/src/cls/IPM/Repo/Definition.cls +++ b/src/cls/IPM/Repo/Definition.cls @@ -21,7 +21,7 @@ Parameter MaxDisplayTabCount As INTEGER = 3; Index ServerDefinitionKey On Name [ Unique ]; -Property Name As %String(MAXLEN = 100) [ Required ]; +Property Name As %IPM.DataType.RepoName(MAXLEN = 100) [ Required ]; Property Enabled As %Boolean [ InitialExpression = 1 ]; diff --git a/src/cls/IPM/Repo/UniversalSettings.cls b/src/cls/IPM/Repo/UniversalSettings.cls index f231b98e..33827bcd 100644 --- a/src/cls/IPM/Repo/UniversalSettings.cls +++ b/src/cls/IPM/Repo/UniversalSettings.cls @@ -22,7 +22,9 @@ Parameter TerminalPrompt = "TerminalPrompt"; Parameter PublishTimeout = "publish_timeout"; -Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout"; +Parameter DefaultLogEntryLimit = "DefaultLogEntryLimit"; + +Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,DefaultLogEntryLimit"; /// Returns configArray, that includes all configurable settings ClassMethod GetAll(Output configArray) As %Status diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index e74677c1..56e25fec 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -419,6 +419,12 @@ ClassMethod ExecutePhases(pModuleName As %String, pPhases As %List, pIsComplete ClassMethod Uninstall(pModuleName As %String, pForce As %Boolean = 0, pRecurse As %Boolean = 0, ByRef pParams) As %Status { Set tSC = $$$OK + Set log = ##class(%IPM.General.History).UninstallInit(pModuleName) + Set module = ..NameOpen(pModuleName,,.tSC) + If $IsObject(module) { + $$$ThrowOnError(log.SetVersion(module.Version)) + } + Try { Merge tParams = pParams Set tParams("Clean","Level") = 1 // Simulate clean of module as dependency. @@ -428,6 +434,8 @@ ClassMethod Uninstall(pModuleName As %String, pForce As %Boolean = 0, pRecurse A } Catch e { Set tSC = e.AsStatus() } + Set devMode = $Get(pParams("DeveloperMode", 0)) + Set tSC = $$$ADDSC(tSC, log.Finalize(tSC, devMode)) Quit tSC } diff --git a/src/cls/IPM/Storage/QualifiedModuleInfo.cls b/src/cls/IPM/Storage/QualifiedModuleInfo.cls index ffceb05c..fcb74e8f 100644 --- a/src/cls/IPM/Storage/QualifiedModuleInfo.cls +++ b/src/cls/IPM/Storage/QualifiedModuleInfo.cls @@ -6,7 +6,7 @@ Class %IPM.Storage.QualifiedModuleInfo Extends %IPM.Storage.ModuleInfo Parameter DEFAULTGLOBAL = "^IPM.Storage.QualifyModInfo"; /// The name of the repository the module is in (The Name property in %IPM.Repo.Definition.
-Property ServerName As %String; +Property ServerName As %IPM.DataType.RepoName; Method %OnNew(pServerName As %String = "", pResolvedReference As %IPM.Storage.ModuleInfo = "") As %Status [ Private, ServerOnly = 1 ] { @@ -66,4 +66,3 @@ Storage Default } } - diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index 2c3815af..d0e75456 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -120,10 +120,12 @@ ClassMethod LoadDependencies(ByRef pDependencyGraph, ByRef pParams, pWorkQueue A If ($TLevel = 0) && ($Get(pParams("Multicompile","Verbose"),$Get(pParams("Verbose"),0))) { Write !,"Queuing module load: ",tModuleName," ",tVersion," @ ",tServerName } + Merge tParams = pParams + Set tParams("CommandToLog") = $Get($$$ZPMCommandToLog, "") Set tSC = pWorkQueue.QueueCallback( "##class("_$ClassName()_").LoadModuleReference", "##class("_$ClassName()_").LoadCompleted", - tServerName, tModuleName, tVersion, $Get(tDeployed), $Get(tPlatformVersion), .pParams, .pDependencyGraph) + tServerName, tModuleName, tVersion, $Get(tDeployed), $Get(tPlatformVersion), .tParams, .pDependencyGraph) $$$ThrowOnError(tSC) } Else { // Remove this module from dependencies. @@ -229,7 +231,14 @@ ClassMethod LoadModuleReference(pServerName As %String, pModuleName As %String, If $Data(pParams) = 1 { Set qstruct = pParams } - + // If spawned from a work queue, $$$ZPMCommandToLog will be undefined in the spawned process. + If $Data(pParams("CommandToLog"), command) # 2 { + Set $$$ZPMCommandToLog = command + } + Set log = ##class(%IPM.General.History).InstallInit(pModuleName) + $$$ThrowOnError(log.SetSource(pServerName)) + $$$ThrowOnError(log.SetVersion(pVersion)) + Set tVerbose = $Get(pParams("Verbose")) If '$Data(pParams("qstruct")) { @@ -262,7 +271,7 @@ ClassMethod LoadModuleReference(pServerName As %String, pModuleName As %String, Set tModRef = ##class(%IPM.Storage.ModuleInfo).%New() Set tModRef.Name = pModuleName Set tModRef.VersionString = pVersion - Set tModRef.Deployed = pDeployed + Set tModRef.Deployed = pDeployed // Make sure we're not downgrading. If '$Get(pParams("PermitDowngrade")) { @@ -271,17 +280,17 @@ ClassMethod LoadModuleReference(pServerName As %String, pModuleName As %String, If $$$ISERR(tSC) { Quit } - If $Get(pParams("Install")),pModuleName'=$$$IPMModuleName,tInstModule.DeveloperMode,'$Get(pParams("DeveloperMode"),0) { + If $Get(pParams("Install")),pModuleName'=$$$IPMModuleName,tInstModule.DeveloperMode,'$Get(pParams("DeveloperMode"),0) { Set tSC = $$$ERROR($$$GeneralError, $$$FormatText("Cannot install '%1' over previously installed in developer mode", tInstModule.Name)) Quit - } + } If tInstModule.Version.Follows(tModRef.Version) { Set tSC = $$$ERROR($$$GeneralError,$$$FormatText("Cannot downgrade %1 from version %2 to %3",tInstModule.Name,tInstModule.VersionString,pVersion)) Quit } } } - + // Ensure requested versions match those required by other modules in the namespace, excluding versions currently being installed // (the requirements of such modules are already known to be satisfied) Set tSC = ..GetRequiredVersionExpression(pModuleName,%installcontext.GetPendingModuleList(),.tExpression,.tSourceList) @@ -370,10 +379,19 @@ ClassMethod LoadModuleReference(pServerName As %String, pModuleName As %String, Set tSC = e.AsStatus() } } + + If $IsObject($Get(log)) { + If $Data(tDeveloperMode) # 2 { + Set devMode = $Get(tDeveloperMode, 0) + } Else { + Set devMode = $Get(pParams("DeveloperMode"), 0) + } + Set tSC = $$$ADDSC(tSC, log.Finalize(tSC, devMode)) + } Quit tSC } -ClassMethod LoadModuleFromArchive(pModuleName As %String, pModuleVersion As %String, pArchiveStream As %Stream.Object, ByRef pParams, pRepository As %String = "") As %Status +ClassMethod LoadModuleFromArchive(pModuleName As %String, pModuleVersion As %String, pArchiveStream As %Stream.Object, ByRef pParams, pRepository As %String = "", pLog As %IPM.General.History) As %Status { Set tSC = $$$OK Try { @@ -402,7 +420,7 @@ ClassMethod LoadModuleFromArchive(pModuleName As %String, pModuleVersion As %Str Write:tVerbose !,tOutput(i) } - Set tSC = ..LoadModuleFromDirectory(tTargetDirectory, .pParams, , pRepository) + Set tSC = ..LoadModuleFromDirectory(tTargetDirectory, .pParams, , pRepository, .pLog) If $$$ISERR(tSC) { Quit } @@ -412,7 +430,7 @@ ClassMethod LoadModuleFromArchive(pModuleName As %String, pModuleVersion As %Str Quit tSC } -ClassMethod LoadModuleFromDirectory(pDirectory As %String, ByRef pParams, pOverrideDeveloperMode As %Boolean = 0, pRepository As %String = "") As %Status +ClassMethod LoadModuleFromDirectory(pDirectory As %String, ByRef pParams, pOverrideDeveloperMode As %Boolean = 0, pRepository As %String = "", pLog As %IPM.General.History) As %Status { Set tSC = $$$OK Try { @@ -439,7 +457,7 @@ ClassMethod LoadModuleFromDirectory(pDirectory As %String, ByRef pParams, pOverr Merge tParams("Artifactory") = pParams("Artifactory") Merge tParams("AngularArtifact") = pParams("AngularArtifact") } - Set tSC = ..LoadNewModule(pDirectory,.tParams,pRepository) + Set tSC = ..LoadNewModule(pDirectory,.tParams,pRepository, , .pLog) If $$$ISERR(tSC) { Quit } @@ -1097,8 +1115,11 @@ ClassMethod ExportDocumentForObject(pSourceModule As %IPM.Storage.Module, Output Quit tSC } -ClassMethod LoadNewModule(pDirectory As %String, ByRef pParams, pRepository As %String = "", pSynchronous As %Boolean = 0) As %Status +ClassMethod LoadNewModule(pDirectory As %String, ByRef pParams, pRepository As %String = "", pSynchronous As %Boolean = 0, pLog As %IPM.General.History) As %Status { + If $Data(pLog) # 2 = 0 { + Set pLog = "" + } Set tSC = $$$OK Set tInitTLevel = $TLevel Try { @@ -1168,11 +1189,15 @@ ClassMethod LoadNewModule(pDirectory As %String, ByRef pParams, pRepository As % Set tModule = ##class(%IPM.Storage.Module).NameOpen(tModuleName,,.tSC) $$$ThrowOnError(tSC) + If $IsObject(pLog) { + $$$ThrowOnError(pLog.SetName(tModuleName)) + $$$ThrowOnError(pLog.SetVersion(tModule.VersionString)) + } Set tModule.Repository = pRepository If $Data(pParams("DeveloperMode"),tDeveloperMode) { - // Mark module as not deployed if installing with dev mode - Set tModule.Deployed = 0 + // Mark module as not deployed if installing with dev mode + Set tModule.Deployed = 0 Set tModule.DeveloperMode = tDeveloperMode Set tModule.Root = pDirectory Set tSC = tModule.%Save() @@ -1180,7 +1205,7 @@ ClassMethod LoadNewModule(pDirectory As %String, ByRef pParams, pRepository As % } Else { Set tDeveloperMode = +tModule.DeveloperMode } - + If (tDeveloperMode && tUseTransactions) { // In developer mode, save the module manifest so we can fix it even if errors occur afterward. TCOMMIT @@ -1202,7 +1227,7 @@ ClassMethod LoadNewModule(pDirectory As %String, ByRef pParams, pRepository As % } Merge $$$ZPMHandledModules($Namespace) = pParams("Multicompile","ModuleContext") - $$$ThrowOnError(##class(%IPM.Storage.Module).CheckSystemRequirements(tModuleName)) + $$$ThrowOnError(##class(%IPM.Storage.Module).CheckSystemRequirements(tModuleName)) #dim tInstallContext As %IPM.General.InstallContext Set tInstallContext = ##class(%IPM.General.InstallContext).%Get(.tSC) @@ -1216,7 +1241,7 @@ ClassMethod LoadNewModule(pDirectory As %String, ByRef pParams, pRepository As % // Temporary custom loading of dependencies in a synchronous // manner to prevent lock timeouts until lifecycle rework // is complete (HSIEO-4450) - Do ..SyncLoadDependencies(tModule, .pParams) + Do ..SyncLoadDependencies(tModule, .pParams) } } diff --git a/src/inc/IPM/Common.inc b/src/inc/IPM/Common.inc index 0d0b318d..5eba8e24 100644 --- a/src/inc/IPM/Common.inc +++ b/src/inc/IPM/Common.inc @@ -11,6 +11,11 @@ ROUTINE %IPM.Common [Type=INC] #; Local % Variable to indicate that ^Sources update trigger should not be run for modifications to individual resources #define ZPMDeferModifyResources %IPMDeferModifyResources +#; Local % Variable to memorize the command string +#; Useful because each install/load/uninstall command can potentially affect multiple packages +#; The log entry for each package should include the same command +#define ZPMCommandToLog %IPMCommandToLog + #; Default packages for package manager, module lifecycle classes and resource processors #define ZPMRootPackage "%IPM"