glitchassassin / rainmeter-python

Plugin for Rainmeter enabling Python 3 scripting
GNU General Public License v2.0
15 stars 5 forks source link

Possible to use multiple instances? #2

Open Enteleform opened 7 years ago

Enteleform commented 7 years ago
System:     Windows 10 Pro x64 Build 15063.413
Rainmeter:  4.0.0 r2746 x64

 
I just tested this plugin out successfully with a single measure + meter combo. However, Rainmeter crashes immediately without logging any errors when I try to create a second set (separate measure + meter).

Both python-script instances are essentially identical aside from the file & class names, which are unique & properly isolated.

Here's a simplified version of the .ini file:

[PY_Test_A]
Measure       = Plugin
Plugin        = Python
PythonHome    = C:\Python\3
ScriptPath    = Test_A.py
ClassName     = Test_A
UpdateDivider = 1

[Test_A]
Meter       = String
MeasureName = PY_Test_A

[PY_Test_B]
Measure       = Plugin
Plugin        = Python
PythonHome    = C:\Python\3
ScriptPath    = Test_B.py
ClassName     = Test_B
UpdateDivider = 1

[Test_B]
Meter       = String
MeasureName = PY_Test_B

 
And here is the simplified code for Test_A.py & Test_B.py:

class Test_X:

    def GetString(self):
        return f"SUCCESS @ {self.__class__.__name__}"

    def Reload(self, rm, maxValue): pass
    def ExecuteBang(self, command): pass
    def Update(self):               pass
    def Finalize(self):             pass
glitchassassin commented 7 years ago

This is something that I broke by removing the sub-interpreter code when trying to fix the original version. I was able to get it to run, but with severely limited functionality - i.e., with only one plugin.

Looking back, I suspect the correct way to deal with this is to spawn a new process for each Python interpreter. Python is really bad at handling multiple interpreters in the same process. I am just not familiar enough with multiprocessing in C++ to be able to do that right off the bat.

I'll add this to my rainy-day projects list, but if someone else wants to take a crack at it first, feel free to submit a pull request!

Enteleform commented 7 years ago

@glitchassassin

Any idea what the performance would be like with multiple interpreters? (around 10-100ish)

I set up a small framework last night based on using a single instance, but then it occurred to me that I could also subclass it for some minimal single-purpose measures (which is when I ran into the issue).  

Single Instance

This setup's intention is to outsource all data-work to Python, and allow a single line point of usage (python_utils.execute) to retreive data in Lua. This is acheived with a string property which is paired to GetString, an output decorator which sets string, and Bang("!CommandMeasure"...) passing an output-wrapped function string to be run by exec @ ExecuteBang.

@ measure.py

###  StdLib  ###
import subprocess
from functools import wraps

SUBLIME_TEXT = "C:/Program Files/Sublime Text/sublime_text.exe"

class Measure:
  string = ""

#######   Rainmeter Functions   ################################################################################

  def Reload(self, rainmeter, maxValue):
    self.rainmeter = rainmeter

  def ExecuteBang(self, command):
    try:
      exec(f"self.{command}")
    except Exception as e:
      self.notify(e)

  def GetString(self):
    return self.string

  def Update(self):
    pass

  def Finalize(self):
    pass

#######   Output Functions   ###################################################################################

  @output
  def get_CurrentTime(self):
    return datetime.now().strftime("%I:%M:%S %p")

#######   Utils   ##############################################################################################

  def log(self, message, ):
    self.rainmeter.RmLog(self.rainmeter.LOG_ERROR, str(message))

  def notify(self, message):
    subprocess.Popen([SUBLIME_TEXT, "--new-window", "--command", f"insert {{\"characters\": \"{message}\"}}"])

#######   Output Decorator   ###################################################################################

def output(function):
  @wraps(function)
  def wrapped(self, *args, **kwargs):
    result = function(self, *args, **kwargs)
    self.string = str(result)
  return wrapped

 

@ MySkin.lua

---------   Example Usage   ------------------------------------------------------------------------------------

function Update()
  time = python_utils.execute{script="PyMeasure", command="get_CurrentTime()"}
  meter_utils.set_Text{meter="CurrentTime", text=time}
end

---------   Utils   --------------------------------------------------------------------------------------------

python_utils = {}

function python_utils.execute(ARGS) -- script, command
  measure = SKIN:GetMeasure(ARGS.script)
  SKIN:Bang("!CommandMeasure", ARGS.script, ARGS.command)
  return measure:GetStringValue()
end

meter_utils = {}

function meter_utils.set_Text(ARGS) -- meter, text
  SKIN:Bang("!SetOption", ARGS.meter, "Text", ARGS.text)
end

 

Multiple Instances

This setup builds upon the single instance setup by subclassing Measure, and allows concise python implementations that can be baked into INI files without the need for Lua.

@ CurrentTime.py:

###  Skin Framework  ###
from measure import Measure, output

###  StdLib  ###
from datetime import datetime

class CurrentTime(Measure):

  @output
  def Update(self):
    return datetime.now().strftime("%I:%M:%S %p")