Script Interface - gängige Praxis?

jb_alvarado

Lt. Junior Grade
Registriert
Sep. 2015
Beiträge
492
Hallo Allerseits,
ich wollte mal eure Meinung lesen, wie ihr darüber denkt und was in diesem Bereich die gängige Praxis ist.

Zur Vorgeschichte:
Ich entwickle auf Github ein Streaming-Programm, mit dem man entweder Ordnerinhalte, oder Playlisten mittels ffmpeg streamen kann. Sprache ist Rust, aber das ist hier weniger relevant.

Ein Nutzer hatte den Wunsch, dass ich eine Möglichkeit einbaue ein Script zu startet wenn der Videoclip sich ändert. Er streamt zu Twitch und würde gerne mittels Script den Titel auf Twitch ändern.

Das ganze ist mir so zu speziell, aber ich könnte mir vorstellen ein Interface zu integrieren, welches dann vom Benutzer mittels Scripting genutzt werden kann.

Für Rust gibt es die Crate PyO3, mit der man zur Laufzeit Python Code ausführen kann. Daher dachte ich mir, könnte ich das so benutzen. Würde auch erst mal nur in eine Richtung planen, also dass meine Software Python Code ausführen kann, und nicht umgekehrt von Python aus die Software zu steuern.

Die Frage ist jetzt wie geht man hier am besten vor, so dass ich das in Zukunft gut pflegen und erweitern kann und auch für den Benutzer keine zu große Hürde ist.

Auf die Schnelle dachte ich, ich könnte einen festen Pfad definieren, z.B.: /usr/lib/ffplayout/ffevents.py und in dieser Datei würde ich leere Funktionen definieren:

Python:
def next_clip_event(media_obj):
    """
    add your code to do something with media_obj,
    for example calling requests.post(url, json = media_obj)
    """
   
    pass

In Rust würde ich nun an entsprechender Stelle diese Funktion ausführen.

Wie würdet ihr denn so was lösen? Macht es Sinn einen festen Pfad zu definieren, oder soll ich lieber in der Konfig eine anpassbare Variable einfügen? Lieber nur einen Script Ordner angeben und diesen einlesen, oder eine einzelne Datei?
 
  • Gefällt mir
Reaktionen: BeBur
Klingt gut. Ich hab selbst auch mal mit Rust und PyO3 gespielt. Das hat soweit gut funktioniert.
Wenn möglich solltest du in dem Skript noch Variable Annotations, sprich Typhinweise für Argumente und Rückgabe benutzen. Dann weiß man gleich, was für ein Typ media_obj ist.

Dann solltest du dir eine Liste an Events überlegen und zu jeden so eine Funktion wie oben definieren. Falls kein Skript existierst oder die Funktion nicht, dann ignorieren, etc.

Python ist auf jeden Fall schonmal eine Skriptsprache welche recht gut verbreitet ist und sich wohl hier gut eignet. Was auch ginge, wäre "Shellskripte" sprich Prozesse mit Argumenten aufzurufen, welche in der Config definiert werden können. Aber dabei ginge viel vom media_obj verloren oder wäre umständlich. Ich glaube schon, dass PyO3 ein guter Weg ist.

Was das Pflegen angeht, sollte sie die API natürlich möglichst wenig ändern, speziell keine bestehenden Skripte brechen. Neue Events hinzufügen und in Python neue Funktionen/Variablen in das Argument packen sollte aber gehen.

Sofern das Dinge als Daemon läuft, ist /usr/lib/ffplayout/ffevents.py vermutlich nicht verkehrt. Von dort aus kann man ja immernoch andere Python-Dateien importieren. Wobei, ist die andere Config in /etc/ffplayout/ffplayout.yml? Weil dann wäre vielleicht /etc/ffplayout/ffevents.py der bessere Ort. Schön am selben Fleck. Andererseits ist es mehr Programm/Skript als Config... Gibt es denn schon was in /usr/lib/ffplayout/?
Bin jetzt aber auch kein Linux-Experte. Gibt da bestimmt Regeln wo was hin soll. Am besten ist es vermutlich, wenn man den Pfad zumindest in der Config verändern kann.

Dann gibt es noch das Security-Thema. Man kann mit den Skripten natürlich dann alles mögliche tun (auch andere Prozesse starten) mit den selben Rechten wie das Rust-Programm. Ich weiß gerade nicht, ob man das irgendwie einschränken kann, aber möglicherweise will man das ja auch in manchen Fällen genau so.
 
  • Gefällt mir
Reaktionen: jb_alvarado
Du kannst natürlich einen Skriptinterpreter in dein Programm einbauen, aber wenn du es simpel halten möchtest, reicht doch eigentlich eine Konfig-Datei, die dein Programm entsprechende Aufrufe tätigen lässt:
JSON:
[
    {"event":"NextClip", "command":"pfad/zum/programm %title%"},
   ...
]
Hätte auch den Vorteil, dass das Ganze technologieneutral wäre.
 
  • Gefällt mir
Reaktionen: kuddlmuddl und jb_alvarado
#plugin #add-on
jb_alvarado schrieb:
Das ganze ist mir so zu speziell, aber ich könnte mir vorstellen ein Interface zu integrieren, welches dann vom Benutzer ... genutzt werden kann. ... In Rust würde ich nun an entsprechender Stelle diese Funktion ausführen.
Genau. In deinem Fall ein python interface.
jb_alvarado schrieb:
Wie würdet ihr denn so was lösen? Macht es Sinn einen festen Pfad zu definieren, oder soll ich lieber in der Konfig eine
anpassbare Variable einfügen? Lieber nur einen Script Ordner angeben und diesen einlesen, oder eine einzelne Datei?
Fester Ordner, üblicherweise "Plugins". Darin alle Dateien inkl. Unterordner einlesen, die das Interface implementieren.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: jb_alvarado
Ich danke euch für die Anregungen!

@SirKhan, über den Speicherort bin ich auch noch unschlüssig. Unter etc ein Script packen ist unüblich, aber unter /usr/lib etwas packen, was anpassbar sein soll finde ich auch etwas komisch :-). Vielleicht mache ich einfach noch einen Unterordner unter /etc/ffplayout/plugin. Und zusätzlich kann der User dann auch in der Konfigurationsdatei den Pfad anpassen.

Was das Ausführen in Bezug auf Sicherheit angeht: Wenn das Programm ordnungsgemäß installiert wird (*.deb) läuft es unter seinem eigenen Benutzer, also wird auch der Python Code unter diesem Benutzer ausgeführt. Finde das vertretbar.

@Ack der III, ja das wäre auch eine Möglichkeit, aber wenn ich dann in Zukunft mehr Events einbaue wird das dann doch etwas unübersichtlich.

@Micke, den Ansatz mit einem Ordner übergeben und diesen rekursive einlesen, finde ich auch gut. Nur wie komm ich dann an meine gewünschten Funktionen ran? Am einfachsten wäre es, wenn ich dazu Dateinamen vordefiniere, sonst müsste ich erst mal alle Scripte einlesen um zu schauen wo meine Funktion ist.
 
jb_alvarado schrieb:
Nur wie komm ich dann an meine gewünschten Funktionen ran?
Ich würde mich an typischen PlugIn-Systemen orientieren. Deine Anwendung würde ein paar Funktionen bereitstellen, die von Skripten verwendet werden können. Deine Anwendung definiert dann eine Funktion register_event_handler(event, callback), mit der sich Skripte für die Events registrieren können:
Python:
def my_callback(event):
    # my code here
    print('Next title is : ', event.title)

# register callback in app
register_event_handler("NextClip", my_callback)
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: jb_alvarado
jb_alvarado schrieb:
Nur wie komm ich dann an meine gewünschten Funktionen ran? Am einfachsten wäre es, wenn ich dazu Dateinamen vordefiniere, sonst müsste ich erst mal alle Scripte einlesen um zu schauen wo meine Funktion ist.
Keine DateiNamen-Vorgaben. Klar musst du kurz einlesen, und :) ?
Etwas pseudo Code zum veranschaulichen:
Code:
Interface MeinInterface
{
void OnNextClip()
void usw.()
}

StreamingProgramm:
function PlayNextClip()
{
for each datei in folder["Plugins"].GetFiles()
for each pluginKlasse in datei.GetClassesByInterface("MeinInterface")
        MeinInterface obj = CreateInstance (klasse)
        obj.OnNextClip()
}
Wie die Funktionen genau heißen ist nebensächlich, viel wesentlicher ist, daß python das alles bereitstellen wird, weil es das Konzept Interfaces kennt.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: jb_alvarado
Eine weitere denkbare Idee wäre, einen TCP-Server in deine Anwendung einzubauen. Jeder, der sich dann dort verbindet, würde einen kontinuierlichen Stream mit Events im JSON-Format bekommen.

Das wäre dann technologieneutral und würde mit jeder anderen Sprache funktionieren. Außerdem wäre dann sogar bidirektionale Kommunikation möglich, um die Anwendung fernzusteuern.

Nachteil ist, dass die Entwicklung eines Clients dann natürlich etwas aufwändiger ist, als nur ein einfaches Shell-Skript zu schreiben.
 
Micke schrieb:
Eben weil sich das Plugin nicht selbst für Events registrieren kann
Dein Pseudocode zeigt ein übliches Vorgehen bei kompilierenden Sprachen. Bei Skriptsprachen wird in der Regel das gesamte Skript beim Laden in den Interpreter einmal ausgeführt. Dadurch ist es dem Plugin-Skript möglich, sich selbst bei der Anwendung zu registrieren, wie in meinem Beispielcode gezeigt.
 
cx01 schrieb:
Eine weitere denkbare Idee wäre, einen TCP-Server in deine Anwendung einzubauen. Jeder, der sich dann dort verbindet, würde einen kontinuierlichen Stream mit Events im JSON-Format bekommen.
Also was in Richtung Websockets/HTTP2? Wäre auch eine Möglichkeit. Die Anwendung hat schon einen JSON RPC Server am laufen, vielleicht kann ich den erweitern. Bin da noch etwas unschlüssig, das aktuelle Paket was ich dazu nutze wird von den Entwicklern bald ersetzt und das neue wird auf Async ausgelegt sein. Entweder muss ich meine ganze Anwendung umbauen, order mir Helfer schreiben die async im synchronen Context ausführen. Oder ich verwende was andere, gRPC wäre hier noch eine Option.

Generell gefällt mir der Ansatz über Plugins mehr, weil man in Zukunft noch andere Spielereien einbauen kann.
Micke schrieb:
Wie die Funktionen genau heißen ist nebensächlich, viel wesentlicher ist, daß python das alles bereitstellen wird, weil es das Konzept Interfaces kennt.
Muss ich mir mal genauer anschauen, wie das in Rust gehen würde. Es gibt ein Beispiel was in diese Richtung geht. Ob ich auch diese Plugin API brauche, muss ich noch erwägen.

Mir ist jetzt auch erst gekommen, dass man für PyO3 auch einen Python Interpreter braucht. Unter Linux kein Problem, aber auf andere Systemen schon. Es ist mal angesprochen worden, das optional zu gestalten, aber ist wohl unter Rust nicht so einfach. Könnte auch versuchen, Plugins nur unter Linux zu unterstützten. Werde mal noch ein paar Tage drüber schlafen.
 
Ack der III schrieb:
Du kannst natürlich einen Skriptinterpreter in dein Programm einbauen, aber wenn du es simpel halten möchtest, reicht doch eigentlich eine Konfig-Datei, die dein Programm entsprechende Aufrufe tätigen lässt:
JSON:
[
    {"event":"NextClip", "command":"pfad/zum/programm %title%"},
   ...
]
Hätte auch den Vorteil, dass das Ganze technologieneutral wäre.
Die Lösung finde ich auch am besten!
Einfach ein "systemcall" statt direkt zu entscheiden, dass es Python sein muss - nicht komplizierter aber flexibler und für dich auch einfach(er?) umzusetzen.
Eine liste von potentiellen Events, die eben von Benutzern genutzt werden steht dann einfach in der Doku.

"aber wenn ich dann in Zukunft mehr Events einbaue wird das dann doch etwas unübersichtlich."
Wieso? Zum einen würd ichs nicht übertreben mit solchen Argumenten: Selbst wenn deine SW mal 100 Events bereitstellt und jemand tatsächlich 20 nutzt - dann isses halt ne JSON mit 20 Zeilen - so what?
Zum anderen sind andere Lösungen auch nicht automatisch "cleaner". So ein systemcall is wenigstens ne abgeschlossene Sache und gut nachvollziehbar. TCP/IP Server mit Protokoll kann beliebig kompliziert werden, wenn der Nutzer nur bei einem der Events das Parsen falsch umsetzt.

Is ja wahrscheinlich ein Feature, was nur wenige nutzen werden. Aber mit einem systemcall ist man für alle Betriebssysteme und Script-Vorlieben gewappnet und wird es wahrsch nie wieder ändern müssen außer weitere Events hinzufügen.
 
kuddlmuddl schrieb:
wird es wahrsch nie wieder ändern müssen außer weitere Events hinzufügen
oder die Argumente erweitern.

Das ist auch der einzige wirkliche Punkt, welcher meiner Meinung nach etwas gegen diese Lösung ist. Dass man "nur" Text als Argument an den Systemcall übergeben kann und das Erweitern leichter etwas bricht, als wenn ein Python/JSON-Dict ein weiteres Element enthält.

Ich würde für next_clip_event vermutlich den Pfad zur nächsten Datei, sowie dessen Titel übergeben. Evtl. die Länge noch? Aber Coverbild (falls vorhanden) wird schon wieder schwieriger als mit PyO3 oder JSON mittels TCP/IP.

Vermutlich jedoch für die meisten Anwendungsfälle völlig ausreichend und definitiv am einfachsten umzusetzen. Auch von Benutzerseite.
 
Zurück
Oben