How can I unload/reload a script ? and if its actually possible

How can I unload/reload a Ruby script ? and if its actually possible, without restarting SU every time.
Thanks

Use the load method to load a script again. There is no way to unload what has already been loaded on a file basis. However Ruby is extremely dynamic so maybe you can delete methods and classes, I don’t know.

Yes, I am using the load method, but of course that there are things already created. I would like a clean slate :)… I dont want to clean things by myself, seems a bit stupid.
Some unload method whould have been great… oh well, c’est la vie.
I presume the scripts are “merged” in a global Ruby context and that’s why unloading is not possible.

They are.

If you run code that defines new stuff by the same name the old is simple overwritten by it. It’s only when you remove or rename things in your code you need a clean state.

By the way, this is fundamental to Ruby, not anything peculiar to the SketchUp Ruby.

Yes, I’m trying out now with extensions, but I cannot dynamically unload it either. Not even from UI.

This makes development really tedious.
A solution would be to place all my code into a single class and at reload to destroy the old instance and create a new one.

It wouldn’t remove menu items created by the plugin though.

Yeah, the “external” created UI must be manually deleted (if possible), because for example I cannot delete a toolbar button :slight_smile:

Context menus can be set to show or not by setting it to check an instance variable. I also do the same thing in methods to create an ‘off’ setting in my Face Cutter extension. Unfortunately there is no way to remove toolbars or menu items, as far as I know.

For classes and modules you can simply do:

CLASS_NAME = Class.new
MODULE_NAME = Module.new

This does result in a warning, but effectively clears entire modules and classes.

I just have a restart button, that safely closes SU, and watches for the pid to end in system…

the that happens SU is relaunched with the same skp file I was testing with…

mine is for mac and has to deal with ‘multi documents’, but I was told it easy on windows…

##########################################################################
# JcB.mac_Restart (C) 2014 john[at]drivenupthewall[dot]co[dot]uk
#
# Permission to use, copy, modify, and distribute this software for
# any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies.
#
# THIS SOFTWARE IS PROVIDED 'AS IS'
# WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, WITHOUT LIMITATION,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#
# Description: This plugin aids quick restarting of the SU on a mac.
# It is useful for development.
#
# CREDITS: Special thanks to 'SCF' member- slbaumgartner for coding tutorage,
# examples and un-named others who did any testing...
##########################################################################
# my namespace module comment
module JcB
  # First we get the path to the current parent version of SketchUp,
  # this is the easiest way I've found to insure the right version restarts
  # when more than one is open.
  ig_root = ENV['IG_ROOT'].split('/Contents')[0].freeze.inspect
  CUR_APP_PATH = ig_root unless defined? CUR_APP_PATH

  # Next we get the process identity [pid] of the actual running version...
  CUR_APP_PID = Process.pid.freeze unless defined? CUR_APP_PID

  # my namespace sub-module comment
  module Restart
    # Firstly, we create an instance var for the current model 'Title'
    mod_path = Sketchup.active_model.path
    if mod_path.length > 2
      @status_quo = "open -a #{CUR_APP_PATH} #{mod_path} "
    else @status_quo = "open #{CUR_APP_PATH}"
    end
    ##########################################################################

    def self.mac_quit
      ver = Sketchup.version.to_i < 14
      ver ? Sketchup.quit : Sketchup.send_action('terminate:')
    end
    ##########################################################################

    def self.wait_for_su
      # inside the timer block, to 'wait' for the file to be generated...
      waiting = (UI.start_timer(0.1, true) do
        unless Sketchup.active_model
          UI.stop_timer waiting
          mac_quit
        end
      end)
    end
    ##########################################################################

    def self.mac_restart
      # We use a Ruby thread to spawn a non-blocking System thread
      # that sleeps while this CUR_APP_PID is running,
      # then, opens the exact same version found at '@status_quo'
      # after it has been closed via 'mac_quit'...
      Thread.new do
        `while ps -p #{CUR_APP_PID} > /dev/null; do
      sleep 2;
      done
      #{@status_quo}`
      end

      # an edge case when using with "plugin's tool" active would crash SU...
      Sketchup.active_model.select_tool(nil)

      # an edge case when using with "deeply nested component" would crash SU...
      Sketchup.active_model.close_active

      # an edge case when using with "Version Warning" active would crash SU...
      Sketchup.send_action('closeDocument:')

      wait_for_su
    end # def self.mac_restart
    ##########################################################################
  end # Restart
end # module JcB

john

True, but they can be set as MF_GREYED or MF_DISABLED and so unclickble.

“Yes and kind of”,… it is a bit more complicated.

“Scripts”, aka Ruby code files, are evaluated in the top level context, which is referenced by the global constant TOPLEVEL_BINDING, and also is colloquially called “the particular instance of Object known as ‘main’.” (The ‘main’ coming the fact that the Ruby interpreter is written in C, and the execute function is named main() like most C programs.) The formal name of this in Ruby is the ObjectSpace, (this name is also used for a utility module ObjectSpace.)

So, … In Ruby everything is a descendant of Object (and the Kernel module which is mixed into Object.)

Since your script / code is evaluated within Object, everything you create has the potential to become global, and be inherited by everything. Ie, create a method at the toplevel, and everyone’s else’s modules and classes will inherit it. (And they will not like you for it.) This is called “pollution.”

Local variables that pollute are hard to cleanup and can cause issues. Because of this, the load and require methods do some special cleanup that removes instance variables after a script runs. (Sort of like creating a temporary anonymous class instance for the script to evaluate inside.)

But other objects like instance variables, class variables, constants, classes and modules, will be persistent, when the script is done evaluating.

So,… all extension writers, because the SketchUp Ruby ObjectSpace is shared, must wrap all their extensions within an author (or company) toplevel namespace module. (You can create any number of nested sub-modules later for any number of extensions and common use library modules.)

Not true.

If you wrap your code in a container, you can undefine a constant, which is used to reference modules and classes. However you must do a bit of cleanup. All instances of your classes should be dereferenced (set to point at nil.) Garbage Collection should then be run: GC.start
Personally I’d also clear all netsed constants, class and module variables as well, in my code. Any observer instances should be detached from model objects (so you had better have kept track of what your code attached to what.)

Ruby expects classes at the top level to be Core classes, and does not like to undefine them. But your extension classes and modules should all be 2nd level (at least) nested within your namespace module. So not a problem. It is also why the method I speak of, is defined within class Module.

Now before doing any of this any menu items and toolbar buttons need to be disabled.
You do not need to keep references to them (as SketchUp does so on the C++ side of things.) Just make sure they cannot be clicked because the command method they call will be gone when you undefine your modules.

Anyway, standard Ruby has a bunch of cleanup methods in Object, Kernel and Module for removing different kinds of objects.

Regarding that messagebox that extensions cannot be dynamically unloaded. It is for end-user consumption, and it is not totally true. It is just that in order for it to work, the SketchUp Extensibility Team would have to force a particular format of coding extensions, and they are unwilling to do this.

So you can trigger a cleanup unload by a call to an AppObserver#onUnloadExtension() callback method.
Obviously your cleanup observer class needs to be outside any sub-modules that will get undefined.

Despite of what is usually said, it’s possible to unrequire/unload ruby files using this process. This prevent the side effects that eneroth talked about when using the load command…

  1. Assuming that the file required is stored as d:/foo.rb with this simple content :
class Foo

end
  1. Any Class, Module or Method is defined as a constant in Ruby so you can first unlink it :
irb(main):001:0> require 'd:/foo.rb'
=> true
irb(main):002:0> defined? Foo
=> "constant"
irb(main):003:0> Object.send(:remove_const, :Foo)
=> Foo
irb(main):004:0> defined? Foo
=> nil
  1. The files already required/loaded are recorded in the global var $" , you then need to purge what you’ve already required from it :
irb(main):005:0> $".select{|r| r.include? 'foo.rb'}
=> ["d:/foo.rb"]
irb(main):006:0> $".delete('d:/foo.rb')
=> "d:/foo.rb"
irb(main):007:0> $".select{|r| r.include? 'foo.rb'}
=> []
  1. Now you can require your file again and everything will be refreshed and available.
irb(main):008:0> require 'd:/foo.rb'
=> true
irb(main):009:0> $".select{|r| r.include? 'foo.rb'}
=> ["d:/foo.rb"]
irb(main):010:0> defined? Foo
=> "constant"
irb(main):011:0> Foo.new
=> #<Foo:0x000000033ff8d8>
2 Likes

I find that using load works in 95% of the cases when I develop. Only time I need to reboot is if I’m modifying menu or toolbar items. Or I’m changing the inheritance of a class.

What issues are you running into when you want to “unload” code?

2 Likes

Hi Thomas,

Some cases where the load command can’t do the job like :

  1. When having dependencies in your class files only sourced by using the require command to avoid multiple load for each dependency when sourcing your code. Using the load command can’t ensure that all dependencies will be reloaded properly (require will be skipped).
  2. When using require_relative (cause no load_relative exists but can eventually be coded), same effects than the previous point.
  3. load command does not purge deleted content (just added and modified). So that can lead to your code working cause still pointing to deprecated code that isn’t anymore in your source code reloaded. You’ll only see it when reloading from scratch.
  4. When having to switch between code versions with some methods that are overritten or overloded between the two versions. Can lead to sides effects and time used to track why it doesn’t work as expected.
  5. Maybe some more…

I agree that it’s not really usefull for production purpose, mainly usefull for developping purpose but I had some cases where it save my life :slight_smile:

I use a debug toolbar to reload all the files I’m currently working on.

    def self.debugtoolbar
      toolbar = UI::Toolbar.new "Debug"
      cmd = UI::Command.new("Debug") {  BC.load_file        }
      cmd.tooltip = "Debug"
      cmd.status_bar_text = "Load a file to debug"
      cmd.menu_text = "Debug"
      toolbar = toolbar.add_item cmd
      toolbar.show
    end
  module BC
    def self.load_file
      load 'C:/Users/Neil Burkholder/Source/repos/Building Creator/RBScripts/Building_Creator/test/load debug file.rb'
    end
  end
  debugtoolbar

This way I can easily change which files are reloaded when the button is pressed.

1 Like