Ever since I found the ruby scripting language been using it to extend existing functionality in ICM. However one thing which has struck me time and time again is the, almost, half-baked method of executing ruby scripts.
There are 4 options to execute a ruby scripts:
- Execute them from a ruby addon.
- Execute them from the user custom action menu.
- Execute them from the shared custom action menu.
- Execute them through the "run ruby script" menu item.
Number 1 is great but is limited to only 10 scripts at a time. The addons menu is also hidden away in a menu, completely out of site from the user, restricting how useful it can be. Numbers 2 and 3 are really easy to use however they require the user to set them up manually... You used to be able to dynamically setup these buttons for users, while using stand alone databases, however it seems Innovyze is phasing these out. Number 4 is decent, but once again it is hidden in a menu restricting its usefulness.
What we ideally need is a way to make custom GUIs outside of ICM, and then be able to interact with ICM directly. Custom GUIs can be set up to give users the ability to setup job specific tools full of their own personal ruby scripts. But in order to create these GUIs we need to be able to talk to ICM. We need ruby script injection.
Imagine the following ruby script:
require 'win32ole'
class Sandbox
def Sandbox.new()
return binding
end
end
invoker = WIN32OLE.connect("{5c172d3c-c8bf-47b0-80a4-a455420a6911}")
code = invoker.scripts[$$]
mode = invoker.modes[$$]
invoker.rbActive($$)
case mode
when 0
Sandbox.new.eval(code,__FILE__,__LINE__)
when 1
eval(code,binding,__FILE__,__LINE__)
else
box = "box" + mode.to_s
$boxes ||= {}
$boxes[box] ||= Sandbox.new
$boxes[box].eval(code,__FILE__,__LINE__)
end
invoker.rbClosing($$)
When this ruby script is executed, what does it do? Let's take a look, step by step
- We connect to a custom object by it's GUID.
- We grab some data from an array of scripts from the invoker. Note: We use the procID to identify that instance of ICM.
- We grab a mode id from an array of modes from the invoker.
- We execute a function provided by
invoker
,rbActive()
, to tell the invoker that the ruby script has been received and is being processed. - We evaluate the code in a box.
- We execute a function provided by
invoker
,rbClosing()
, to tell the invoker that the ruby script has finished executing and ICM will soon be ready to take new instructions.
With these steps we can easily see how we could potentially setup a means of communication between ICM and an invoker
program. The question is, What would the invoker look like?
To experiment I've created an AHK class to perform the job of the invoker.
class Injector {
modes := {}
scripts := {}
addonAddrs := {}
addonPaths := {}
callbacks := {}
activePIDs := {}
disabledPIDs := {}
;If install is default then overwrite file, else assume custom. Do not overwrite file.
;If not installed correctly, install and ask for restart
__makeInjector(path=0){
if path = 0
path = %A_Appdata%\Innovyze\WorkgroupClient\scripts\injector.rb
rb =
(
require 'win32ole'
class Sandbox
def Sandbox.new()
return binding
end
end
invoker = WIN32OLE.connect("{5c172d3c-c8bf-47b0-80a4-a455420a6911}")
code = invoker.scripts[$$]
mode = invoker.modes[$$]
invoker.rbActive($$)
case mode
when 0
Sandbox.new.eval(code,__FILE__,__LINE__)
when 1
eval(code,binding,__FILE__,__LINE__)
else
box = "box" + mode.to_s
$boxes ||= {}
$boxes[box] ||= Sandbox.new
$boxes[box].eval(code,__FILE__,__LINE__)
end
invoker.rbClosing($$)
)
FileDelete, %path%
FileAppend, %rb%, %path%
}
__requestRestart(PID){
if winexist("ahk_exe InnovyzeWC.exe") {
Msgbox, ICM Requires a restart. Do you want to restart ICM now?
if ErrorLevel {
Msgbox, ICM will restart shortly
WinClose, ahk_exe InnovyzeWC.exe
RunWait, InnovyzeWC.exe /ICM
} else {
this.disabledPIDs[PID]:=True
}
}
}
;3 Possible cases:
; Case 1. User does not have a scripts.csv
; -> Install scripts.csv -> Ask for ICM to restart -> return 0
; Case 2. User has a scripts.csv, however scripts.csv does not contain Inject RubyScript key.
; -> Append line to scripts.csv -> Ask for ICM to restart -> return 0
; Case 3. User has a scripts.csv AND scripts.csv contains Inject RubyScript key.
; -> return 1 (can execute)
__checkInstall(PID){
;Define Ruby Path
rbPath = %A_Appdata%\Innovyze\WorkgroupClient\scripts\injector.rb
scriptsPath = %A_Appdata%\Innovyze\WorkgroupClient\scripts\scripts.csv
rbContent := "Inject RubyScript, injector.rb`n"
;Check if already disabled
if this.disabledPIDs[PID] {
this.__requestRestart()
return 0
}
;If addon has already been evaluated jump straight to execution
;Note each instance of ICM may have it's own address for ICMInject.rb
;for this reason we store that data in a dictionary object 'addonAddrs'.
if (!this.addonAddrs[PID]){
;Does scripts.csv exist?
if fileexist(scriptsPath){ ;if scripts.csv exists
;Case 2 & 3
Loop, read, %scriptsPath%
{
index = A_Index + 1
If RegexMatch(A_LoopReadLine,"i)Inject RubyScript\s*,\s*(.+)",m){
;Case 3
;Index has been found
this.addonAddrs[PID] := index
this.addonPaths[PID] := m1
if this.addonPaths[PID] = "injector.rb"
this.__makeInjector()
return 1
} else {
RegexMatch(A_LoopReadLine,"i)Inject RubyScript\s*,\s*(.+)",m)
}
}
;Case 2
;If we get here then scripts.csv must not contain injector.rb
;So let's add it, ask for a restart and return 0
FileAppend, %rbContent%, %scriptsPath%
;Create injector script
this.__makeInjector()
this.__requestRestart(PID)
return 0
} else {
;Case 1
FileCreateDir, %scriptsPath%
content := "Inject RubyScript, injector.rb`n"
FileAppend, %rbContent%, %rbPath%
this.__makeInjector()
this.__requestRestart(PID)
return 0
}
} else {
if this.addonPaths[PID] = "injector.rb"
this.__makeInjector()
return 1
}
}
__callAddon(id){
;ID = 1: 35080
;ID = 2: 35081
;ID = 3: 35082
; ...
wParam := 35080 + id - 1
PostMessage, %WM_COMMAND%,%wParam%,0,,ahk_exe InnovyzeWC.exe
}
execute(rb,mode:=0,PID:=0){
if !this.__checkInstall(PID)
return 0
;If PID == 0, use most recent PID
if PID=0
WinGet, PID, PID, ahk_exe InnovyzeWC.exe
;Setup Interop parameters for given process
scripts[PID] := rb
modes[PID] := mode
this.__callAddon(addonAddrs[PID])
return 1
}
executeFile(file,mode:=0,PID:=0){
ruby=load('%file%')
return this.execute(ruby,mode,PID)
}
;Event - Active
rbActive(pid){
activePIDs[pid] := 1
if callbacks[pid]
callbacks[pid]("running")
}
;Event - Closing
rbClosing(pid){
activePIDs[pid] := 0
if callbacks[pid]
callbacks[pid]("closing")
}
}