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
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âŚ
- Assuming that the file required is stored as
d:/foo.rb
with this simple content :
class Foo
end
- 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
- 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'}
=> []
- 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>
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?
Hi Thomas,
Some cases where the load command canât do the job like :
- 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 theload
command canât ensure that all dependencies will be reloaded properly (require
will be skipped). - When using
require_relative
(cause noload_relative
exists but can eventually be coded), same effects than the previous point. -
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. - 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.
- 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
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.