Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1052: In a namespace with mapped IPM, the `info` command works again and the intro message displays the IPM version and where its mapped from
- #1102: %IPM.Storage.QualifiedModuleInfo:%New() will now copy over version properties when passed in a resolvedReference
- #1112: Packaging a module with a globals resource now respects SourcesRoot, placing the exported file at the correct path in the tarball
- #1057: Fix IPM not cleaning up after itself on self-uninstall

## [0.10.6] - 2026-02-24

Expand Down
2 changes: 1 addition & 1 deletion src/cls/IPM/CLI.cls
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,7 @@ ClassMethod FormatName(pName As %String) As %String

ClassMethod TerminalPromptColor() As %String
{
quit $case(##class(%IPM.Repo.UniversalSettings).GetValue("TerminalPrompt"),"green":$$$Green,"red":$$$Red,"magenta":$$$Magenta,"yellow":$$$Yellow,"blue":$$$Blue,"cyan":$$$Cyan,"none":$$$Default,:$$$Default)
quit $select(##class(%Dictionary.CompiledClass).%ExistsId("%IPM.Repo.UniversalSettings"):$case(##class(%IPM.Repo.UniversalSettings).GetValue("TerminalPrompt"),"green":$$$Green,"red":$$$Red,"magenta":$$$Magenta,"yellow":$$$Yellow,"blue":$$$Blue,"cyan":$$$Cyan,"none":$$$Default,:$$$Default),1:$$$Default)
}

}
61 changes: 39 additions & 22 deletions src/cls/IPM/General/HistoryTemp.cls
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,49 @@ ClassMethod Init(
quit log
}

/// Explicitly persist current state to the PersistedTwin
/// Can be called before classes are deleted during self-uninstall
Method PersistToTwin() As %Status
{
set sc = $$$OK
if $$$comClassDefined("%IPM.General.History") {
if (..PersistedTwin '= "") {
// Reload to get latest state
$$$ThrowOnError(..%Reload())

// copy all values to the Persisted Twin
set ..PersistedTwin.TimeStart = ..TimeStart
set ..PersistedTwin.TimeEnd = ..TimeEnd
set ..PersistedTwin.Action = ..Action
set ..PersistedTwin.Package = ..Package
set ..PersistedTwin.CommandString = ..CommandString
set ..PersistedTwin.UserName = ..UserName
set ..PersistedTwin.Version = ..Version
set ..PersistedTwin.SourceName = ..SourceName
set ..PersistedTwin.SourceMoniker = ..SourceMoniker
set ..PersistedTwin.SourceDetails = ..SourceDetails
set ..PersistedTwin.Success = ..Success
set ..PersistedTwin.Committed = ..Committed
set ..PersistedTwin.Phases = ..Phases
// Mark as finalized and record should be locked
set ..PersistedTwin.Finalized = 1
set sc = ..%Save()
}
}
quit sc
}

/// Persist this more permanently by saving to the PersistedTwin
Method %OnClose() As %Status [ Private, ServerOnly = 1 ]
{
if ..PersistedTwin '= "" {
// copy all values to the Persisted Twin
set ..PersistedTwin.TimeStart = ..TimeStart
set ..PersistedTwin.TimeEnd = ..TimeEnd
set ..PersistedTwin.Action = ..Action
set ..PersistedTwin.Package = ..Package
set ..PersistedTwin.CommandString = ..CommandString
set ..PersistedTwin.UserName = ..UserName
set ..PersistedTwin.Version = ..Version
set ..PersistedTwin.SourceName = ..SourceName
set ..PersistedTwin.SourceMoniker = ..SourceMoniker
set ..PersistedTwin.SourceDetails = ..SourceDetails
set ..PersistedTwin.Success = ..Success
set ..PersistedTwin.Committed = ..Committed
set ..PersistedTwin.Phases = ..Phases
// Mark as finalized and record should be locked
set ..PersistedTwin.Finalized = 1
}
// Ensure the persisted twin is saved
set sc = ..%Save()
set sc = $$$OK
if $$$comClassDefined("%IPM.General.History") {
// Persist to twin if not already done
set sc = ..PersistToTwin()

// Delete this temp entry as it's no longer needed
set sc = $$$ADDSC(sc, ..%DeleteId(..%Id()))
// Delete this temp entry as it's no longer needed
set sc = $$$ADDSC(sc, ..%DeleteId(..%Id()))
}
quit sc
}

Expand Down
12 changes: 8 additions & 4 deletions src/cls/IPM/Lifecycle/Base.cls
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,15 @@ Method CheckBeforeClean(
set tVerbose = $get(pParams("Verbose"))
set tLevel = $get(pParams("Clean","Level"),0)
set tRecurse = $get(pParams("Clean","Recurse"),1)
set tForce = $get(pParams("Clean","Force"),0)

if (..Module.GlobalScope && '$get(pParams("Clean","GlobalScope"))) {
do ..Log("Clean SKIPPED - module has global scope.")
set pSkip = 1
quit
}

if '$get(pParams("Clean","Force")) {
if 'tForce {
// Check to see if anything depends on this module and return an error status if it does.
set tSC = ##class(%IPM.Utils.Module).GetDependentsList(.tList,,..Module.Name)
if $$$ISERR(tSC) {
Expand Down Expand Up @@ -487,9 +488,12 @@ Method %Clean(ByRef pParams) As %Status
}

if (tLevel > 0) && '$get(pParams("Clean","Nested"),0) {
set tSC = ##class(%IPM.StudioDocument.Module).Delete(..Module.Name_".ZPM")
if $$$ISERR(tSC) {
quit
// Check if StudioDocument.Module class still exists before trying to delete
if $$$comClassDefined("%IPM.StudioDocument.Module") {
set tSC = ##class(%IPM.StudioDocument.Module).Delete(..Module.Name_".ZPM")
if $$$ISERR(tSC) {
quit
}
}
}
} catch e {
Expand Down
63 changes: 63 additions & 0 deletions src/cls/IPM/Lifecycle/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,69 @@ Method %Clean(ByRef pParams) As %Status
if $$$ISERR(tSC) {
quit
}

// Special handling for IPM self-uninstall: clean up operational globals
if (..Module.Name = $$$IPMModuleName) {
set verbose = $get(pParams("Verbose"))

// Check if user wants full cleanup (delete ALL module records and operational globals)
if $get(pParams("Clean","FullCleanup"), 0) {
write:verbose !,"Performing cleanup of IPM globals..."

kill ^IPM.Storage.ModuleD // Delete ALL module records (including orphans)
kill ^IPM.Storage.ModuleI
kill ^IPM.Storage.InvokeReferenceC
kill ^IPM.Storage.ResourceReferenceC
kill ^IPM.Storage.ResourceReferenceI
kill ^IPM.Storage.SystemRequirementsD
kill ^IPM.Repo.DefinitionD
kill ^IPM.Repo.DefinitionI
kill ^IPM.Repo.Filesystem.CacheD
kill ^IPM.General.SettingsD
kill ^IPM.UpdateStep.AnyMemberD
kill ^IPM.UpdateStep.PrimaryOnlyD
kill ^IPM.StudioDoc.ModuleStreamD
kill ^IPM.StudioDoc.ModuleStreamI
kill ^IPM.StudioDoc.ModuleStreamS
kill ^IPM.StudioDoc.LocalMsgStreamD
kill ^IRIS.Temp.HistoryTempD
kill ^IRIS.Temp.IPM.LoadedResourceD

// Note: Preserve ^IPM.General.HistoryD for audit trail
// Note: Preserve ^IPM.settings in %SYS for user preferences

write:verbose !,"Cleanup complete."
} else {
// Check if modules were orphaned
// IPM is always at index 1, so start checking at index 2
set orphanedModules = ""
set index = 1
for {
set index = $order(^IPM.Storage.ModuleD(index))
quit:index=""
set orphanedModules = orphanedModules _ $listbuild(index)
}

if $listlength(orphanedModules) > 0 {
// Modules were orphaned - warn user but still clean up
write !,"Warning: ",$listlength(orphanedModules)," module(s) orphaned:"

// List orphaned modules with versions
set ptr = 0
while $listnext(orphanedModules, ptr, index) {
try {
set modData = ^IPM.Storage.ModuleD(index)
set modName = $listget(modData, 2)
set version = $listget(modData, 4)
write !," - ",modName," (",version,")"
} catch {
write !," - (unknown module at index ",index,")"
}
}
write !,"Reinstalling IPM will automatically re-adopt these modules."
}
}
}
} catch e {
set tSC = e.AsStatus()
}
Expand Down
54 changes: 48 additions & 6 deletions src/cls/IPM/Main.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,13 @@ ClassMethod ShellInternal(
}
do ..DisplayError(pException.AsStatus())
}

// Check if IPM has uninstalled itself - exit gracefully if so
if $get(^||%IPM.SelfUninstallFlag, 0) {
write !,!,$$$FormattedLine($$$Yellow, "IPM has been uninstalled from this namespace. Exiting shell..."),!
return
}

set tCommand = ""
if tOneCommand {
quit
Expand Down Expand Up @@ -2551,6 +2558,40 @@ ClassMethod Uninstall(ByRef pCommandInfo) [ Internal ]
}
if (tModuleName = $$$IPMModuleName) {
$$$ThrowOnError(..CheckModuleNamespace())

// Check if other modules are installed
set hasModules = 0
set rs = ##class(%SQL.Statement).%ExecDirect(,
"SELECT Name, VersionString FROM %IPM_Storage.ModuleItem WHERE Name != ?",
$$$IPMModuleName)
if rs.%Next() {
set hasModules = 1

// Show list of installed modules
write !,"The following modules are currently installed:"
write !," - ",rs.Name," (",rs.VersionString,")"
while rs.%Next() {
write !," - ",rs.Name," (",rs.VersionString,")"
}
write !

if 'tForce {
// Dialog: Ask about full cleanup (downgrade scenario)
write !,"Do you want to fully remove all modules and IPM metadata (except history log)?"
write !,"This is required for downgrading IPM. [y/N]: "
read fullCleanupResponse
set tParams("Clean","FullCleanup") = $case($zconvert(fullCleanupResponse,"L"),"y":1,"yes":1,:0)
}
}

if $get(tParams("Clean","FullCleanup")) {
// User chose full cleanup - uninstall all modules
write !,"Performing full cleanup: uninstalling all modules..."
$$$ThrowOnError(##class(%IPM.Utils.Module).UninstallAll(tForce,.tParams))
}

// Set flag to indicate self-uninstall - will be checked by Interactive() loop
set ^||%IPM.SelfUninstallFlag = 1
}
set tRecurse = $$$HasModifier(pCommandInfo,"recurse") // Recursively uninstall unneeded dependencies
$$$ThrowOnError(##class(%IPM.Storage.Module).Uninstall(tModuleName,tForce,tRecurse,.tParams))
Expand Down Expand Up @@ -3249,28 +3290,29 @@ ZPM(pArgs...)
// TODO: Needs to support enabling IPM from here, also need to decide what level of customization to provide
set quitOnError = $get(pArgs(2))
set haltOnComplete = $get(pArgs(3))
write !, "IPM is not enabled in this namespace."
set currentNs = $namespace
set rs = ##class(%SQL.Statement).%ExecDirect(, "SELECT Nsp FROM %SYS.Namespace_List()")
if rs.%SQLCODE '= 0 {
write !, "Error getting namespace name. SQLCODE =", rs.%SQLCODE
write !, "Error getting namespace list. SQLCODE =", rs.%SQLCODE
write !, "IPM is not enabled in namespace ", currentNs, "."
} else {
new $namespace
set found = 0
while rs.%Next() {
set $namespace = $zstrip(rs.%Get("Nsp"), "<>WC")
// Some I4H containers come with %IPM.Main but the "version" command doesn't work ?!
if $system.CLS.IsMthd("%IPM.Main", "Shell") && ($namespace '= "HSLIB") && ($namespace '= "HSSYS") {
write !, "Change namespace to one of the following to run the ""zpm"" command"
write !, "IPM is not enabled in namespace ", currentNs, ". Switch to one of the following namespaces to use IPM:"
do ##class(%IPM.Main).Shell("version")
write !, "If you want to map IPM globally, switch to one of the namespaces above and run: zpm ""enable -map -globally""."
write !, "If you want to reset repository and map IPM globally along with repository settings, switch to one of the namespaces above and run: zpm ""enable -community""."
write !, "To map IPM globally, switch to one of the namespaces above and run: zpm ""enable -map -globally""."
write !, "To reset repository and map IPM globally with repository settings, run: zpm ""enable -community""."
set found = 1
quit
}
}
// Shouldn't happen since %ZLANGC00.mac and %ZLANGF00.mac are present
if 'found {
write !, "No namespace found with IPM enabled."
write !, "IPM is not installed. No namespace found with IPM enabled."
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/cls/IPM/ResourceProcessor/PythonWheel.cls
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ Method OnPhase(
ByRef pParams,
Output pResourceHandled As %Boolean = 0) As %Status
{
if (pPhase = "Clean") {
// Mark as handled to prevent fallback to generic cleanup
// which tries to delete wheels as Studio documents
// TODO: actually cleanup the python wheel. This is non-trivial because we don't want to delete the wheel if it's shared with other modules
set pResourceHandled = 1
quit $$$OK
}

if (pPhase '= "Initialize") {
set pResourceHandled = 0
quit $$$OK
Expand Down
49 changes: 42 additions & 7 deletions src/cls/IPM/Storage/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -624,19 +624,47 @@ ClassMethod ExecutePhases(
// Lifecycle before / (phase) / after
new $$$DeployedProjectInstalled
$$$ThrowOnError(tLifecycle.OnBeforePhase(tOnePhase,.pParams))

// Self-uninstall: Finalize history before Clean phase deletes IPM's own classes
set isSelfUninstall = (tOnePhase = "Clean") && (tModule.Name = $$$IPMModuleName)
if isSelfUninstall && $isobject(pLog) {
// Log the Clean phase completion and finalize now
do pLog.FinalizePhase(tOnePhase, $$$OK)
set tSC = pLog.Finalize($$$OK, 0)
$$$ThrowOnError(tSC)
// Explicitly persist to twin while History classes still exist
$$$ThrowOnError(pLog.PersistToTwin())
// Delete the temp record immediately to prevent %OnClose() from trying to reload it later
$$$ThrowOnError(pLog.%DeleteId(pLog.%Id()))
// Clear the log object reference so it's not used again
set pLog = ""
}

$$$ThrowOnError($method(tLifecycle,"%"_tOnePhase,.pParams))

// Self-uninstall: After Clean deletes IPM's own classes, skip all post-phase callbacks
if isSelfUninstall {
// Print success message before skipping
write !,"["_$namespace_"|"_tModule.DisplayName_"]",$char(9),tOnePhase," SUCCESS"
if tTiming {
write " ("_($zhorolog-tStart)_" s)"
}
// Skip all post-phase processing - classes have been deleted
continue
}

$$$ThrowOnError(tLifecycle.OnAfterPhase(tOnePhase,.pParams))
}

#; Call Invoke Methods After Phase
set tKey = ""
for {
set tInvoke = tModule.Invokes.GetNext(.tKey)
if (tKey = "") || '$isobject(tInvoke) {
quit // '$IsObject can happen reasonably after namespace changes
}
set tSC = tInvoke.OnAfterPhase(tOnePhase,.pParams)
quit:$$$ISERR(tSC)
set tInvoke = tModule.Invokes.GetNext(.tKey)
if (tKey = "") || '$isobject(tInvoke) {
quit // '$IsObject can happen reasonably after namespace changes
}
set tSC = tInvoke.OnAfterPhase(tOnePhase,.pParams)
quit:$$$ISERR(tSC)
}
quit:$$$ISERR(tSC)

Expand Down Expand Up @@ -717,7 +745,14 @@ ClassMethod Uninstall(
set tSC = e.AsStatus()
}
set devMode = $get(pParams("DeveloperMode", 0))
set tSC = $$$ADDSC(tSC, log.Finalize(tSC, devMode))
// Only finalize if log wasn't already finalized and deleted (e.g., during self-uninstall)
if $isobject(log) {
try {
set tSC = $$$ADDSC(tSC, log.Finalize(tSC, devMode))
} catch {
// Skip if log was already deleted during self-uninstall
}
}
quit tSC
}

Expand Down
3 changes: 2 additions & 1 deletion src/inc/IPM/Formatting.inc
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
ROUTINE %IPM.Formatting [Type=INC]
#; Contains utility macros for formatting lines printed to Terminal

#define ColorScheme $Case(##class(%IPM.Repo.UniversalSettings).GetValue("ColorScheme"),"none":0,:1)
#define ColorScheme $Select(##class(%Dictionary.CompiledClass).%ExistsId("%IPM.Repo.UniversalSettings"):##class(%IPM.Repo.UniversalSettings).GetValue("ColorScheme")'="none",1:0)

/// Returns a line with given formatting, clearing the formatting at the end of the line
#define FormattedLine(%formatCode, %line) $Select($$$ColorScheme:$$$ControlSequence(%formatCode)_%line_$$$ControlSequence($$$ResetAll),1:%line)
#define pad(%width, %text) $Justify("", %width - $Length(%text))
Expand Down
Loading