Perform operations in a plugin when its is being uninstalled

Hello!

I wonder if it is possible to perform operations in a plugin when its is being uninstalled? In my case, during the installation, the plugin creates some components in the Components\ folder of Sketchup

C:\Users\MyUserName\AppData\Roaming\SketchUp\SketchUp 2024\SketchUp\Components

and I would like to delete them when the plugin is uninstalled, so the sketchUp folder is left as it was before the installation.

If it is possible to write some code within the plugin to be executed during the uninstallation, it would be great to have an example.

kind regards, Carlos

I’m pretty sure the API doesn’t provide any way to do this. So far as I know there is no way to be notified when an extension is uninstalled. It might be a useful feature request though.

I agree with Steve.
__
( The “closest thing” you can monitor is when the user is disabling the extension in Extension Manager, but unfortunately this doesn’t fire if you uninstall any extension so - still far away from your asked for…

The #onUnloadExtension method is called when the user turns off a Ruby extension… )

Note that custom components should always be saved or copied into a subfolder of the Components folder as the subfolder name will be used as the collection name in the interface.

1 Like

The doc entry for that method doesn’t make clear what “when a user turns off an extension” means. I suspect it is when the extension is disabled via the extension manager, as otherwise it would be redundant with other methods. It’s also not the same as uninstalling.

Oops, this was meant as a reply to @DanRathbun not @dezmo !

Reading the definition file for the SketchupExtension class, we see that when a user unchecks an extension in the Extension Manager, OR some code calls #uncheck upon the extension object, SketchUp calls Sketchup::register_extension again with a false 2nd argument, which will call an internal #unload callback in the SketchupExtension object which sets the @load_on_start variable (which is eventually saved into the JSON preferences file for this extension.)

At some point after the “unchecking” the AppObserver#onUnloadExtension callback will get called in ALL app observers with this extension as the argument. When in the “unchecking” process it is called is not published. Ie, do the AppObservers get called before or after the extension object “housekeeping” ?

But it was actually @dezmo that mentioned the AppObserver#onUnloadExtension callback.

Yes, actually Ruby itself provides a way. It is called subclassing, where you use a modified “child” class of the SketchupExtension class.

I have just tested it and it does work. However, in the realm of best practices, it might be considered fragile, because the API SketchupExtension class’ behavior could change in the future and you would then need to issue an update to work around these possible changes. (To be fair however, the class has not changed at all in the last 11 years, since 2013.)


In the following example, I have a test extension with the DC Training components copied into the extension’s “Components” subfolder. When the extension is registered with SketchUp’s ExtensionsManager the subclass will copy the components files over into the user’s %AppData% Components folder within a destination collection folder.
When the extension is uninstalled this %AppData% Components collection subfolder is removed.

AcmeInc_RoadrunnerCatcher_1.0.2.rbz (173.0 KB) - UPDATED to v1.0.2

TL;DR - For those wanting a quick look, here is the example registrar file that also defines the subclass:

# encoding: UTF-8
#
# Example Extension Registrar File
#
# This example shows subclassing the SketchupExtension class to provide
# automated extension specific resource installation and removal.
#
# This example coding pattern is valid for SketchUp 2017 & higher that saves
# custom user resources in the user's %AppData% or "Library/Application Support"
# paths.
# Also does not work on old versions that did not have the Ruby Standard
# Library "fileutils.rb" available.
#
# v 1.0.1 - Moved the `super` calls to the end of the callback overrides hoping
#           to suppress the error message output to the console:
#           "Extension returned wrong type for method mark_as_uninstalled"
#
# v 1.0.2 - Return `nil` from the `SketchupExtension` class callback overrides
#           in order to suppress the core's complaint to STDOUT that it did
#           not receive the expected (NULL) return type.

require 'fileutils' unless defined?(FileUtils)

module AcmeInc
  module RoadrunnerCatcher

    class Extension < ::SketchupExtension

      COMPONENTS_PATH ||= Sketchup.find_support_file('Components')
      COLLECTION_PATH ||= File.join(COMPONENTS_PATH, 'Roadrunner Weapons')

      # Method to copy component files specific to this extension.
      def copy_components
        # Create the directories if they do not yet exist:
        unless File.directory?(COLLECTION_PATH)
          FileUtils.mkdir_p(COLLECTION_PATH)
        end
        # Copy the extension components from the extension distribution source path
        # to the components collection path:
        source = File.join(__dir__, 'AcmeInc_RoadrunnerCatcher', 'Components')
        Dir.glob('*.{skp,SKP}', base: source) do |source_filename|
          # This will add or overwrite destination files so that components can
          # be added, fixed or updated when the extension is updated:
          dest_path = File.join(COLLECTION_PATH, source_filename)
          source_path = File.join(source, source_filename)
          # Copy if not yet copied, Overwrite if source modification time is newer:
          if !File.exist?(dest_path) || File.mtime(source_path) > File.mtime(dest_path)
            FileUtils.install(source_path, COLLECTION_PATH)
          end
        end
      end

      # Method to delete component files specific to this extension.
      def delete_components
        # If the collection path does not exist, then there is nothing to delete:
        return unless File.directory?(COLLECTION_PATH)
        FileUtils.remove_dir(COLLECTION_PATH)
      end

      #{# NOTE: The following callbacks should never be called directly. They
      #         are called by the SketchUp engine at the appropriate times.

      # Callback called by SketchUp when the extension is uninstalled via the UI.
      def mark_as_uninstalled
        # Call a method to delete extension specific component files:
        delete_components()
        # Call the superclass' method which does some housekeeping:
        super
        return nil # The EM core wants a NULL result
      end

      # Callback called by SketchUp when the extension is registered via the
      # Sketchup.register_extension method.
      def register_from_sketchup()
        # Call a method to copy extension specific component files:
        copy_components()
        # Call the superclass' method which does some housekeeping:
        super
        return nil # The EM core wants a NULL result
      end
    #
    #}#

    end # subclass

    # Create the Extension informational instance object:
    EXTENSION = Extension.new("Roadrunner Catcher", "AcmeInc_RoadrunnerCatcher/main")

    # Set the extension detail properties:
    EXTENSION.creator     = "Wile E. Coyote"
    EXTENSION.description = "Catches those pesky roadrunner birds."
    EXTENSION.version     = "1.0.2"
    EXTENSION.copyright   = "©2024, Crazy Clown Utilites"

    # Register this extension with SketchUp's ExtensionsManager collection:
    Sketchup.register_extension(EXTENSION, true)

  end
end
6 Likes

@slbaumgartner (or anyone else on a Mac) I only tested on Win 11.
If anyone has the time, please also test on Mac platform.

The SKP files in the extension’s “Components” subfolder should be copied (I think) over to:
~/Library/Application Support/SketchUp 2024/SketchUp/Components/Roadrunner Weapons

Upon uninstall this user resource subfolder and its contents should be deleted.

On Windows in SU2024 the Components panel is refreshed automatically, adding the extension specific components collection to the menu when it is registered, and removed from the collections menu when the extension is uninstalled. (I seem to remember in past versions that we had to restart SketchUp to get user resource collections to be properly enumerated. Apparently this has been fixed.)

1 Like

I’ll take a look when I get a chance…tonight and tomorrow are heavily booked.

2 Likes

Wow, thank you very much for your detailed response DanRathbun!

First of all, yes I am placing my components in a Subfolder of the Components\ folder. I will try to use the code in your example to delete my folder instead of the Roadrunner Weapons, shouldn’t be very difficult. I wonder if this resource will work for all SU versions after SU8, for that one I believe fileutils is not available.

Kind regards, Carlos

NO! SU8 used Ruby 1.8.x and I did not write it for that old Ruby version. (I do not code for dead versions of Ruby. Supported versions of SketchUp are at Ruby 2.7 and higher. The latest version is at Ruby 3.2.)

Yes, SketchUp did not come with the Standard Library until SketchUp 2014 (which used Ruby 2.0.0,) so the FileUtils library module will not be available unless the user installs the Standard Library. Therefore no guarantees.
I wrapped up the Ruby 1.8.6 Standard Library into a SketchUp RBZ file for Windows only for SU8 and SU2013:

Also, there were some changes made to the SketchupExtension class with the 2013 release. So the mark_as_uninstalled() callback was not available in SU8.

There was also a major change with the 2017 release where the user custom resources were saved into the user’s %AppData% path. Prior to this on Windows the resources were in the %ProgramFiles% path which caused permissions issues.

Therefore, this solution would most likely only work (as is) for SketchUp 2017 and higher.

FYI: It is meant that you should rename the top-level namespace module to your namespace, and rename the extension submodule to your submodule. Of course the extension registrar file and extension subfolder must also be renamed to match according to Extension Requirements.
Of course, the extension properties must also be changed to match the extension under developement.

Ie, this code is not a library for use as is. It is an example extension registrar file (with a dummy "main.rb" and "Components" subfolder with some of the DC teaching components.)

ok, then I will make sure the procedure on uninstall is only triggered for SU17 and later. Thanks again!

1 Like

I finally got a chance to try this today. It appeared to run correctly, however on uninistall the following appeared in the Ruby Console:

Extension returned wrong type for method mark_as_uninstalled
1 Like

Yes, I got this also when testing and moved the super call to the end of the callback override to hopefully have the correct type returned to SketchUp’s Extension Manager “engine” (at some point in the future.)

However, I believe this is a bug in the calling C core code as it happens even if there is no subclass in use. Meaning that the callback simply makes a Boolean assignment to the @has_been_uninstalled instance variable, but the core expects some other type as the return type. (I suspect integer as in C there really is no Boolean type, it uses 0 and 1.)

There is no way to trap this exception when it is the user’s manual uninstalling an extension, because it is being raised in the C++ EM core that internally calls the mark_as_uninstalled() callback.
Likewise the API does not expose an uninstall method for either the SketchupExtension or Sketchup::ExtensionsManager classes.

I believe that I had reported this to @ChrisFullmer during the last release cycle.


ADD: Also, this originally came up in the public forum here:

1 Like

After testing a few return values, I have determined that the Extension Manager core wants a NULL return when calling the callback. So, I’ve updated the example to do this in the internal calback overrides in the subclass. viz:

      # Callback called by SketchUp when the extension is uninstalled via the UI.
      def mark_as_uninstalled
        # Call a method to delete extension specific component files:
        delete_components()
        # Call the superclass' method which does some housekeeping:
        super
        return nil # The EM core wants a NULL result
      end

UPDATED the example RBZ above to v 1.0.2

1 Like

mirror:

Hi again,

for some reason the mark_as_uninstalled function is not being triggered when the plugin is uninstalled using Windows_manager/manage/Uninstall in SU24. However, I can see the warning message “Extension returned wrong type for method mark_as_uninstalled” popping up in the ruby API when the plugin is uninstalled.

regards, Carlos

Carlos, this does not make any sense. You would not see the message unless the callback was being called and does not return nil.

  • Make sure that in your code you have spelled the name of the method override correctly.

I also tested the example in SU 2024.

Without more information as to what your code does and does not do, I will refrain from making any guesses.

1 Like

Hello Dan,

Here is a simplified code of my main.rb with the essential code regarding this SketchupExtension class

require 'fileutils' unless defined?(FileUtils)

module MyMain
  module MyPlugin
  
    class Extension < ::SketchupExtension

	  def delete_components
   	    components_path ||= Sketchup.find_support_file('Components')
	    collection_path ||= File.join(components_path, 'MyComponents')
	    return unless File.directory?(collection_path)
	    FileUtils.remove_dir(collection_path)
	  end  
		  
	  def mark_as_uninstalled
	    UI.messagebox("First Line of mark_as_uninstalled")
	    delete_components()
	    super
	    return nil
	  end
    end        # end class Extension 

    # moving of MyComponents to be a subfolder of the Components folder operation 
    # has been part of the body of my main.rb for some time and it seems to work fine, 
    # I will consider to put that inside the copy_components function.

    # get versionNo an integer with the SU version obtained from Sketchup.version

    if versionNo > 16 		  
	EXTENSION = Extension.new("Delete MyComponents during uninstall", "MyPlugin/main")
        EXTENSION.define_singleton_method(:mark_as_uninstalled) { super; nil }  
	EXTENSION.creator     = "Carlos"
	EXTENSION.description = "Delete MyComponents subfolder from the Components folder"
	EXTENSION.version     = "1.0.1"
	EXTENSION.copyright   = "©2025, Acme"
        Sketchup.register_extension(EXTENSION, true)
    end	    

  end
end

The plugin installs with no error messages, at the moment I uninstall using Extensions_manager/manage/Uninstall in SU24 I can see the he warning message “Extension returned wrong type for method mark_as_uninstalled” popping up in the ruby console. I suspected my mark_as_uninstalled function was not triggered because my UI.messagebox is not executed neither I can see the MyComponents subfolder of the components folder being deleted after the uninstall is completed.

regards, Carlos