summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--TODO.rst5
-rw-r--r--gamechest.glade527
-rw-r--r--gamechest.glade~527
-rwxr-xr-xgamechest.py6
-rw-r--r--gamechest/__init__.py217
-rw-r--r--gamechest/cli.py66
-rw-r--r--gamechest/conf.py25
-rw-r--r--gamechest/gamemanager.py183
-rw-r--r--gamechest/gamemanager.py.backup431
-rw-r--r--gamechest/gamemanager_method1.py252
-rw-r--r--gamechest/stepper.py69
-rw-r--r--gamechest/steps_install.py109
-rw-r--r--gamechest/steps_remove.py31
-rw-r--r--gamechest/utils.py19
14 files changed, 2467 insertions, 0 deletions
diff --git a/TODO.rst b/TODO.rst
new file mode 100644
index 0000000..6c7ca4f
--- /dev/null
+++ b/TODO.rst
@@ -0,0 +1,5 @@
+- implem with asyncio for steppers to simplify implementation
+- with cli just run asyncio.run(main())
+- with gui, run gtk in a thread and asyncio.run in main thread (or the
+ reverse) and make them communicate with a queue.
+- if not easily working, use https://blogs.gnome.org/jamesh/2019/08/05/glib-integration-for-python-asyncio/
diff --git a/gamechest.glade b/gamechest.glade
new file mode 100644
index 0000000..603f10f
--- /dev/null
+++ b/gamechest.glade
@@ -0,0 +1,527 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.2 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkTextBuffer" id="game_characteristics">
+ <property name="text" translatable="yes">Game Characteristics</property>
+ </object>
+ <object class="GtkTextBuffer" id="game_description">
+ <property name="text" translatable="yes">Game Description</property>
+ </object>
+ <object class="GtkListStore" id="gamelist_store">
+ <columns>
+ <!-- column-name installed -->
+ <column type="gboolean"/>
+ <!-- column-name title -->
+ <column type="gchararray"/>
+ <!-- column-name index -->
+ <column type="gint"/>
+ </columns>
+ <data>
+ <row>
+ <col id="0">False</col>
+ <col id="1">No game in list yet</col>
+ <col id="2">0</col>
+ </row>
+ </data>
+ </object>
+ <object class="GtkWindow" id="window2">
+ <property name="can_focus">False</property>
+ <property name="icon">org.devys.apps.gamechest.svg</property>
+ <signal name="destroy" handler="on_destroy" swapped="no"/>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="row_homogeneous">True</property>
+ <property name="column_homogeneous">True</property>
+ <child>
+ <object class="GtkTextView">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="buffer">game_characteristics</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTextView">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="editable">False</property>
+ <property name="wrap_mode">word</property>
+ <property name="justification">fill</property>
+ <property name="buffer">game_description</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="game_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixbuf">org.devys.apps.gamechest.svg</property>
+ <signal name="draw" handler="game_image_on_draw" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTreeView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_left">5</property>
+ <property name="margin_right">5</property>
+ <property name="model">gamelist_store</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection">
+ <signal name="changed" handler="gamelist_on_changed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkTreeViewColumn">
+ <property name="title" translatable="yes">Installed</property>
+ <child>
+ <object class="GtkCellRendererToggle"/>
+ <attributes>
+ <attribute name="active">0</attribute>
+ </attributes>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkTreeViewColumn">
+ <property name="title" translatable="yes">Title</property>
+ <child>
+ <object class="GtkCellRendererText"/>
+ <attributes>
+ <attribute name="text">1</attribute>
+ </attributes>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="placeholder_text" translatable="yes">Search Pattern</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="game_title">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Selected Game Title</property>
+ <attributes>
+ <attribute name="font-desc" value="Liberation Serif Bold 14"/>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSearchEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_name">edit-find-symbolic</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">False</property>
+ <property name="placeholder_text" translatable="yes">Profile</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkProgressBar" id="game_install_progress">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="text">Install Progression</property>
+ <property name="show_text">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkProgressBar" id="global_progression">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="text" translatable="yes">Global Install Progression</property>
+ <property name="show_text">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btn_install">
+ <property name="label" translatable="yes">install</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="clicked" handler="game_install_on_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btn_update">
+ <property name="label" translatable="yes">update</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="no_show_all">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btn_remove">
+ <property name="label" translatable="yes">remove</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="no_show_all">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="game_install_stop_btn">
+ <property name="label">gtk-cancel</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="no_show_all">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="game_installstop_on_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">run</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">quit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="clicked" handler="on_quit_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="window1">
+ <property name="can_focus">False</property>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="placeholder_text" translatable="yes">Search Pattern</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTreeView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Selected Game Title</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-missing-image</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTextView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTextView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">install</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">run</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">quit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/gamechest.glade~ b/gamechest.glade~
new file mode 100644
index 0000000..0549937
--- /dev/null
+++ b/gamechest.glade~
@@ -0,0 +1,527 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.2 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkTextBuffer" id="game_characteristics">
+ <property name="text" translatable="yes">Game Characteristics</property>
+ </object>
+ <object class="GtkTextBuffer" id="game_description">
+ <property name="text" translatable="yes">Game Description</property>
+ </object>
+ <object class="GtkListStore" id="gamelist_store">
+ <columns>
+ <!-- column-name installed -->
+ <column type="gboolean"/>
+ <!-- column-name title -->
+ <column type="gchararray"/>
+ <!-- column-name index -->
+ <column type="gint"/>
+ </columns>
+ <data>
+ <row>
+ <col id="0">False</col>
+ <col id="1">No game in list yet</col>
+ <col id="2">0</col>
+ </row>
+ </data>
+ </object>
+ <object class="GtkWindow" id="window2">
+ <property name="can_focus">False</property>
+ <property name="icon">org.devys.apps.gamechest.svg</property>
+ <signal name="destroy" handler="on_destroy" swapped="no"/>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="row_homogeneous">True</property>
+ <property name="column_homogeneous">True</property>
+ <child>
+ <object class="GtkTextView">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="buffer">game_characteristics</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTextView">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="editable">False</property>
+ <property name="wrap_mode">word</property>
+ <property name="justification">fill</property>
+ <property name="buffer">game_description</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="game_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="pixbuf">org.devys.apps.gamechest.svg</property>
+ <signal name="draw" handler="game_image_on_draw" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTreeView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_left">5</property>
+ <property name="margin_right">5</property>
+ <property name="model">gamelist_store</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection">
+ <signal name="changed" handler="gamelist_on_changed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkTreeViewColumn">
+ <property name="title" translatable="yes">Installed</property>
+ <child>
+ <object class="GtkCellRendererToggle"/>
+ <attributes>
+ <attribute name="active">0</attribute>
+ </attributes>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkTreeViewColumn">
+ <property name="title" translatable="yes">Title</property>
+ <child>
+ <object class="GtkCellRendererText"/>
+ <attributes>
+ <attribute name="text">1</attribute>
+ </attributes>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="placeholder_text" translatable="yes">Search Pattern</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="game_title">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Selected Game Title</property>
+ <attributes>
+ <attribute name="font-desc" value="Liberation Serif Bold 14"/>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSearchEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="primary_icon_name">edit-find-symbolic</property>
+ <property name="primary_icon_activatable">False</property>
+ <property name="primary_icon_sensitive">False</property>
+ <property name="placeholder_text" translatable="yes">Profile</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkProgressBar" id="game_install_progress">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="text">Install Progression</property>
+ <property name="show_text">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkProgressBar" id="global_progression">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="text" translatable="yes">Global Install Progression</property>
+ <property name="show_text">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="game_install">
+ <property name="label" translatable="yes">install</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="clicked" handler="game_install_on_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btn_update">
+ <property name="label" translatable="yes">update</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="no_show_all">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btn_remove">
+ <property name="label" translatable="yes">remove</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="no_show_all">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="game_install_stop_btn">
+ <property name="label">gtk-cancel</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="no_show_all">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="game_installstop_on_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">run</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">quit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="clicked" handler="on_quit_clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkWindow" id="window1">
+ <property name="can_focus">False</property>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="placeholder_text" translatable="yes">Search Pattern</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTreeView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Selected Game Title</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-missing-image</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTextView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkTextView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">install</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">run</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">quit</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/gamechest.py b/gamechest.py
new file mode 100755
index 0000000..8e4281d
--- /dev/null
+++ b/gamechest.py
@@ -0,0 +1,6 @@
+#!/usr/bin/python3
+
+import gamechest
+
+if __name__ == '__main__':
+ gamechest.main()
diff --git a/gamechest/__init__.py b/gamechest/__init__.py
new file mode 100644
index 0000000..8629624
--- /dev/null
+++ b/gamechest/__init__.py
@@ -0,0 +1,217 @@
+#!python3
+
+'''
+Usage: gamechest.py --gamelist=GAMELIST.YAML --gamedir=GAMEDIR
+'''
+
+import logging
+import os
+from logging import debug, info, warning, critical
+
+
+import docopt
+import yaml
+import gi
+from . import gamemanager
+
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk as gtk, GLib as glib, GdkPixbuf as gdkpixbuf
+from gi.repository.GdkPixbuf import Pixbuf
+
+
+class GameList:
+
+ def __init__(self, gamelist, gamedir):
+ debug(f'managing gamedir {gamedir}')
+ debug(f'reading gamelist from {gamelist}')
+ with open(gamelist) as stream:
+ self.yaml_data = yaml.safe_load(stream)
+ self.gamedir = gamedir
+ debug('loaded data: %d bytes', len(self.yaml_data))
+
+
+class Handler:
+
+ def __init__(self, builder, gamelist, gamelist_dir, gamedir):
+ self.builder = builder
+ self.gamelist = gamelist
+ self.gamelist_dir = gamelist_dir
+ self.gamedir = gamedir
+ repository_host = gamelist.yaml_data['repository']['host']
+ repository_path = gamelist.yaml_data['repository']['path']
+ self.game_installer = gamemanager.GameInstaller(
+ repository_host, repository_path, gamedir)
+ self.game_remover = gamemanager.GameRemover(
+ repository_host, repository_path, gamedir)
+ self.game_status = gamemanager.GameStatus(
+ repository_host, repository_path, gamedir)
+
+ self.game_title = builder.get_object('game_title')
+ self.game_image = builder.get_object('game_image')
+ self.game_characteristics = builder.get_object('game_characteristics')
+ self.game_description = builder.get_object('game_description')
+ self.btn_install = builder.get_object('btn_install')
+ self.btn_update = builder.get_object('btn_update')
+ self.btn_remove = builder.get_object('btn_remove')
+ self.game_install_stop_btn = builder.get_object('game_install_stop_btn')
+ self.game_install_progress = builder.get_object('game_install_progress')
+ self.global_progression = builder.get_object('global_progression')
+
+ self.game_install_timeout_id = None
+ self.current_selected_game = None
+ self.game_image_pixbuf = None
+
+ def on_destroy(self, *args):
+ gtk.main_quit()
+
+ def on_quit_clicked(self, *args):
+ print('quit')
+ gtk.main_quit()
+
+ def gamelist_on_changed(self, selection, *args):
+ model, treeiter = selection.get_selected()
+ if treeiter is None:
+ return
+ index = model[treeiter][2]
+ #print("You selected", self.gamelist['games'][model[treeiter][2]])
+ debug('selected index: %d', index)
+ game_item = self.gamelist.yaml_data['games'][index]
+ self.current_selected_game = game_item
+ info("You selected (title): %s", game_item['title'])
+
+ self.game_title.set_label(game_item['title'])
+ if image := game_item.get('image', None):
+ image_path = f'{self.gamelist_dir}/{image}'
+ debug('image path: %s', image_path)
+ if os.path.exists(image_path):
+ #self.game_image.set_from_file(image_path)
+ self.game_image_pixbuf = Pixbuf.new_from_file(image_path)
+ self.game_image_draw()
+ self.game_characteristics.set_text(
+ game_item.get('characteristics', 'Unfilled characteristics'))
+ description = game_item.get('description', 'No description')
+ self.game_description.set_text(description)
+
+ if self.game_status.exists(game_item['name']):
+ debug('game status exists')
+ self.btn_install.hide()
+ self.btn_update.show()
+ self.btn_remove.show()
+ else:
+ debug('game status does not exist')
+
+ def game_image_on_draw(self, widget, cr, *args):
+ self.game_image_draw()
+
+ def game_image_draw(self):
+ if not self.game_image_pixbuf:
+ return
+
+ orig_width = self.game_image_pixbuf.get_width()
+ orig_height = self.game_image_pixbuf.get_height()
+ dest_width = self.game_image.get_allocated_width()
+ dest_height = self.game_image.get_allocated_height()
+ ratio_width = dest_width / orig_width
+ ratio_height = dest_height / orig_height
+ ratio = min(ratio_width, ratio_height)
+
+ #width, height = self.game_image.get_allocated_size()
+ pixbuftmp = self.game_image_pixbuf.scale_simple(
+ orig_width*ratio,
+ orig_height*ratio,
+ gdkpixbuf.InterpType.BILINEAR)
+ self.game_image.set_from_pixbuf(pixbuftmp)
+
+ def on_draw(self, widget, cr, data):
+ context = widget.get_style_context()
+
+ width = widget.get_allocated_width()
+ height = widget.get_allocated_height()
+ Gtk.render_background(context, cr, 0, 0, width, height)
+
+ r, g, b, a = data["color"]
+ cr.set_source_rgba(r, g, b, a)
+ cr.rectangle(0, 0, width, height)
+ cr.fill()
+
+ ############################################################
+ # install management
+ ############################################################
+
+ def game_install_on_clicked(self, *args):
+ if not self.current_selected_game:
+ return
+
+ self.game_install_timeout_id = \
+ glib.timeout_add(250, self.game_install_on_timeout, None)
+ self.game_install_stop_btn.show()
+
+ game_item = self.current_selected_game
+ self.game_installer.start(game_item)
+ # set progress bar to 0.0001 in order to update them the first time on
+ # timeout (most useful for global advance)
+ self.global_progression.set_fraction(0.0001)
+ self.game_install_progress.set_fraction(0.0001)
+
+ def game_install_stop(self):
+ if not self.game_install_timeout_id:
+ return
+ glib.source_remove(self.game_install_timeout_id)
+ self.game_install_timeout_id = None
+ self.game_installer.stop()
+ #self.game_install_progress.set_fraction(1)
+ self.game_install_stop_btn.hide()
+
+ def game_installstop_on_clicked(self, *args):
+ self.game_install_stop()
+
+ def game_install_on_timeout(self, *args):
+ status, progress1, progress2 = self.game_installer.poll()
+ #print(f'status {status} progress1 {progress1} progress2 {progress2}')
+ old_progress1 = self.global_progression.get_fraction()
+ if (progress1/100) != old_progress1:
+ self.global_progression.set_fraction(progress1/100)
+ self.global_progression.set_text(f'global {progress1:.2f}%')
+ old_progress2 = self.game_install_progress.get_fraction()
+ if (progress2/100) != old_progress2:
+ self.game_install_progress.set_fraction(progress2/100)
+ self.game_install_progress.set_text(f'step {progress2:.2f}%')
+ # returning False value means stop timer
+ if status == self.game_installer.State.ONGOING:
+ return True
+ self.game_install_stop()
+ return False
+
+
+def main():
+ logging.basicConfig(level=logging.DEBUG)
+ logging.getLogger().handlers[0].setFormatter(logging.Formatter(
+ "gamechest: %(levelname)s: %(funcName)s:%(lineno)s: %(message)s"))
+
+ args = docopt.docopt(__doc__)
+ debug('args: %s', args)
+
+ gamelist_dir = (
+ os.path.dirname(args['--gamelist'])
+ + '/.'
+ + os.path.basename(args['--gamelist'])
+ + '.d'
+ )
+ gamelist = GameList(args['--gamelist'], args['--gamedir'])
+
+ builder = gtk.Builder()
+ builder.add_from_file('gamechest.glade')
+ builder.connect_signals(
+ Handler(builder, gamelist, gamelist_dir, args['--gamedir']))
+
+ window = builder.get_object('window2')
+ window.show_all()
+
+ gamelist_store = builder.get_object('gamelist_store')
+ gamelist_store.clear()
+ for index, game in enumerate(gamelist.yaml_data['games']):
+ gamelist_store.append((False, game['title'], index))
+ info('%d games loaded', len(gamelist.yaml_data['games']))
+
+ gtk.main()
diff --git a/gamechest/cli.py b/gamechest/cli.py
new file mode 100644
index 0000000..d0b457e
--- /dev/null
+++ b/gamechest/cli.py
@@ -0,0 +1,66 @@
+#!python3
+
+'''
+Usage: gamechest.py --gamelist=GAMELIST.YAML --gamedir=GAMEDIR
+'''
+
+import logging
+import os
+from logging import debug, info, warning, critical
+
+
+import docopt
+import yaml
+import gamemanager
+
+
+def main():
+ logging.basicConfig(level=logging.DEBUG)
+ logging.getLogger().handlers[0].setFormatter(logging.Formatter(
+ "gamechest: %(levelname)s: %(funcName)s:%(lineno)s: %(message)s"))
+
+ args = docopt.docopt(__doc__)
+ debug('args: %s', args)
+
+ gamelist_dir = (
+ os.path.dirname(args['--gamelist'])
+ + '/.'
+ + os.path.basename(args['--gamelist'])
+ + '.d'
+ )
+ gamelist = GameList(args['--gamelist'], args['--gamedir'])
+
+ builder = gtk.Builder()
+ builder.add_from_file('gamechest.glade')
+ builder.connect_signals(
+ Handler(builder, gamelist, gamelist_dir, args['--gamedir']))
+
+ window = builder.get_object('window2')
+ window.show_all()
+
+ gamelist_store = builder.get_object('gamelist_store')
+ gamelist_store.clear()
+ for index, game in enumerate(gamelist.yaml_data['games']):
+ gamelist_store.append((False, game['title'], index))
+ info('%d games loaded', len(gamelist.yaml_data['games']))
+
+ gtk.main()
+
+def main():
+ import sys
+ gamelist_path = sys.argv[1]
+ gamedir_path = sys.argv[2]
+ action_name = sys.argv[3]
+ game_name = sys.argv[4] # used to determine game_id
+
+ # TODO:
+ # - parse gamelist (conf)
+ # - parse package installed info or empty (conf)
+ # - follow action (install, remove, update)
+ # - use name on game name
+ # - update status bar from polling (with a timer, or a simple sleep)
+ # - if run, run the game.
+
+
+if __name__ == '__main__':
+ main()
diff --git a/gamechest/conf.py b/gamechest/conf.py
new file mode 100644
index 0000000..17e7737
--- /dev/null
+++ b/gamechest/conf.py
@@ -0,0 +1,25 @@
+#!python3
+
+import yaml
+
+class Config:
+ '''
+ Stores configuration for whole program run time.
+
+ Should be pretty unmuttable.
+ '''
+
+ def __init__(self, gamelist_path, gamedir):
+ self.gamelist_path = gamelist_path
+ self.gamedir = gamedir
+
+ # parse gamelist.yaml file; parsed once at init time and never written
+ # (not meat to be writtable).
+ with open(gamelist_path, 'r', encoding='utf8') as stream:
+ self.gamelist = yaml.safe_load(stream)
+
+ self.repository_host = self.gamelist['repository']['host']
+ self.repository_path = self.gamelist['repository']['path']
+
+ def get_status(self, game_id):
+ 'get the status of a game by its game_id'
diff --git a/gamechest/gamemanager.py b/gamechest/gamemanager.py
new file mode 100644
index 0000000..2e9b06c
--- /dev/null
+++ b/gamechest/gamemanager.py
@@ -0,0 +1,183 @@
+#!python3
+
+import contextlib
+from logging import debug, info, warning, critical
+from enum import Enum, auto as enum_auto
+
+import yaml
+
+#from . import utils
+from stepper import Stepper
+import steps_install, steps_remove
+
+def strip_tar_ext(name):
+ ext = '.tar.lzip'
+ return name[:-len(ext)] if name.lower().endswith(ext) else name
+
+
+
+_installer_steps_dict = {
+ 'download': steps_install.step_rsync,
+ 'extract': steps_install.step_extract,
+ 'clean': steps_install.step_clean,
+}
+_remover_steps_dict = {
+ 'remove': steps_remove.step_remove,
+}
+_updater_steps_dict = dict(**_remover_steps_dict, **_installer_steps_dict)
+
+
+class GameStepper:
+
+ def __init__(self, conf):
+ self.game_installer = Stepper(conf, installer_steps_dict)
+ self.game_remover = Stepper(conf, remover_steps_dict)
+ self.game_updater = Stepper(conf, updater_steps_dict)
+ self.curent = None
+
+ def install(self, game_id):
+ if self.current:
+ return
+ self.current = self.game_installer
+ self.game_installer.start(self.conf.get_status(game_id))
+
+ def remove(self, game_id):
+ if self.current:
+ return
+ self.current = self.game_remover
+ self.game_remover.start(self.conf.get_status(game_id))
+
+ def update(self, game_id):
+ if self.current:
+ return
+ self.current = self.game_updater
+ self.game_updater.start(self.conf.get_status(game_id))
+
+ def stop(self):
+ if self.current:
+ self.current.stop()
+ self.current = None
+
+ def poll(self):
+ if self.current:
+ return self.current.poll()
+
+
+class GameManager:
+
+ class State(Enum):
+ IDLE = enum_auto()
+ ONGOING = enum_auto()
+
+ def __init__(self, conf):
+ self.game_installer = Stepper(conf, installer_steps_dict)
+ self.game_remover = Stepper(conf, remover_steps_dict)
+ self.game_updater = Stepper(conf, updater_steps_dict)
+
+ #def __init__(self, games_data_yaml_path, games_dir):
+ # self.games_data_yaml_path = games_data_yaml_path
+ # self.games_dir = games_dir
+ # self.state = self.State.IDLE
+ # self.update_state = self.State.IDLE
+ # self.current_game_name = None
+ # self.current_game_item = None
+ # self.current_game_status = {}
+
+ # with open(games_data_yaml_path) as stream:
+ # self.games_data = yaml.safe_load(stream)
+
+ # self.game_installer = GameInstaller(
+ # self.games_data['repository']['host'],
+ # self.games_data['repository']['path'],
+ # self.games_dir,
+ # )
+ # self.game_remover = GameRemover(
+ # self.games_data['repository']['host'],
+ # self.games_data['repository']['path'],
+ # self.games_dir,
+ # )
+
+ def get_game_item(self, name):
+ self.current_game_item = next(
+ item
+ for item in self.games_data['games']
+ if item['name'] == name
+ )
+ return self.current_game_item
+
+ def get_game_status(self, name):
+ self.current_game_status = {}
+ with contextlib.suppress(FileNotFoundError):
+ with open(f'{self.conf.gamedir}/{name}.yaml') as stream:
+ self.current_game_status = yaml.safe_load(stream)
+ return self.current_game_status
+
+ def is_game_updatable(self, name):
+ #self. ### FIXME: Stopped here ?
+ if not self.current_game_name:
+ return False
+ ivers = self.current_game_status.get('installed_version', None)
+ # only call "updatable" if there was at least a version installed
+ # else, this is just an "installable" package.
+ if ivers is not None and ivers < self.current_game_item['version']:
+ return True
+ return False
+
+ def set_current_game(self, game_id):
+ if self.current_game_name == name:
+ return
+ self.current_game_name = name
+ self.get_game_item(name)
+ self.get_game_status(name)
+
+ def start_install(self, game_id):
+ if self.state != self.State.IDLE:
+ return
+ self.status = self.State.INSTALLING
+ self.set_current_game(name)
+ self.game_installer.start(
+ self.current_game_item,
+ game_status=self.current_game_status,
+ )
+
+ def start_remove(self, name):
+ if self.state != self.State.IDLE:
+ return
+ self.status = self.State.REMOVING
+ self.set_current_game(name)
+ self.game_remover.start(
+ self.current_game_item,
+ game_status=self.current_game_status,
+ )
+
+ def start_update(self, name):
+ if self.state != self.State.IDLE:
+ return
+ self.status = self.State.UPDATING_REMOVING
+ self.set_current_game(name)
+ self.game_remover.start(
+ self.current_game_item,
+ game_status=self.current_game_status,
+ )
+
+ def stop(self):
+ self.state = self.State.IDLE
+ self.game_remover.stop()
+ self.game_installer.stop()
+
+ def poll(self):
+ entry_state = self.state
+ return_value = {
+ self.State.IDLE: (lambda: GameManagerBase.State.IDLE, 0, 0),
+ self.State.INSTALLING: self.game_installer.poll,
+ self.State.REMOVING: self.game_remover.poll,
+ self.State.UPDATING_REMOVING: self.game_remover.poll,
+ self.State.UPDATING_INSTALLING: self.game_installer.poll,
+ }[entry_state]()
+ if return_value[0] == GameManagerBase.Sate.IDLE:
+ self.state = self.State.IDLE
+ if entry_state == self.State.UPDATING_REMOVING:
+ self.start_install(self.current_game_name)
+ return_value = self.poll()
+ return self.state, return_value[1], return_value[2]
+
diff --git a/gamechest/gamemanager.py.backup b/gamechest/gamemanager.py.backup
new file mode 100644
index 0000000..f8f9551
--- /dev/null
+++ b/gamechest/gamemanager.py.backup
@@ -0,0 +1,431 @@
+#!python3
+
+import contextlib
+import os
+import queue
+import re
+import selectors
+import shutil
+import subprocess
+import tarfile
+import threading
+from logging import debug, info, warning, critical
+from enum import Enum, auto as enum_auto
+
+import yaml
+
+
+def strip_tar_ext(name):
+ ext = '.tar.lzip'
+ return name[:-len(ext)] if name.lower().endswith(ext) else name
+
+
+class GameChestConfig:
+ '''
+ Stores configuration for whole program run time.
+
+ Should be pretty unmuttable.
+ '''
+
+ def __init__(self, repository_host, repository_path, gamedir, game_data):
+ self.repository_host = repository_host
+ self.repository_path = repository_path
+ self.gamedir = gamedir
+ self.game_data = game_data
+
+
+
+class LockedData:
+ 'every attribute access to this class is locked except lock and data'
+
+ def __init__(self, data={}):
+ super().__setattr__('lock', threading.Lock())
+ super().__setattr__('data', dict())
+ self.data.update(data)
+
+ def __getattr__(self, name):
+ with self.lock:
+ return self.data[name]
+
+ def __setattr__(self, name, value):
+ with self.lock:
+ self.data[name] = value
+
+
+class Progress(LockedData):
+ 'hold a progression status'
+
+ def __init__(self):
+ super().__init__(self, {
+ 'global': 0,
+ 'steps_count': 1,
+ 'step': 0,
+ 'step_index': 0,
+ 'step_name': 'unknown',
+ })
+
+ @locked
+ def set_step_index(self, index):
+ self._step_index = index
+ self._global = (index/self._steps_count)*100
+
+
+class GameManagerBase:
+ '''
+ Manage a worker processing steps in background and allows polling its
+ status (global steps progress, and current step progress).
+ '''
+
+ class State(Enum):
+ IDLE = enum_auto()
+ ONGOING = enum_auto()
+
+ class Command(Enum):
+ STOP = enum_auto()
+
+ def __init__(self, conf):
+ self.conf = conf
+ self.state_holder = LockedData()
+ self.state = self.State.IDLE
+ self.thread = None
+ self.lock = threading.Lock()
+ self.queue = queue.Queue(maxsize=10)
+ self.progress = Progress()
+
+ @property
+ def state(self):
+ return self.state_holder.state
+
+ @property.setter
+ def set_state(self, value):
+ self.state_holder.state = value
+
+ def worker(self, *args):
+ self.progress.steps_count = len(self.steps)
+ try:
+ for index, step in enumerate(self.steps):
+ self.progress.step_index = index
+ for step_progress in step():
+ self.progress.step = step_progress
+ finally:
+ self.state = self.State.IDLE
+
+ def start(self, *, game_status=None):
+ if self.steps is None:
+ raise NotImplementedError
+ if self.state == self.State.ONGOING:
+ return
+ self.progress.global = 0
+ self.progress.step = 0
+ with self.lock:
+ self.game_status = game_status
+ self.thread = threading.Thread(target=self.worker, daemon=True)
+ self.thread.start()
+ self.state = self.State.ONGOING
+
+ def stop(self):
+ if self.state != self.State.ONGOING:
+ return
+ self.state = self.State.IDLE
+ self.queue.put(self.Command.STOP)
+
+ def poll(self):
+ return self.state, self.global_progress, self.step_progress
+
+
+class GameInstaller(GameManagerBase):
+ '''
+ Defines how to install a game:
+ - download it
+ - unarchive it
+ - remove the archive
+ '''
+
+ rsync_progress_re = re.compile(r'\s(\d+)%\s')
+
+ def __init__(self, conf):
+ super().__init__(conf)
+ self.steps = (
+ self._worker_rsync,
+ self._worker_unarchive,
+ self._worker_rmarchive,
+ )
+
+ def _worker_rsync_loop(self, proc, selector):
+ while True:
+ with contextlib.suppress(queue.Empty):
+ if self.queue.get_nowait() == self.Command.STOP:
+ proc.terminate()
+ break
+
+ if proc.poll() is not None:
+ break
+
+ if not any(key
+ for key, mask in selector.select(timeout=0.250)
+ if mask & selectors.EVENT_READ and key.fileobj == proc.stdout):
+ continue
+
+ # stdout.readline is a blocking call if there is no endline in
+ # stdout outputed by the subprocess.
+ # normally rsync output should be line buffered thus at any read event
+ # a call to readline should not block (or not long enough to be
+ # able to process a queued command quick enough).
+ line = proc.stdout.readline()
+ if match := self.rsync_progress_re.search(line):
+ progress = int(match.group(1))
+ yield progress
+
+ def _worker_rsync(self):
+ package_name = self.conf.game_data['package_name']
+ command = (
+ 'rsync',
+ '-a',
+ '--partial',
+ '--info=progress2',
+ f'{self.conf.repository_host}:{self.conf.repository_path}/{package_name}',
+ f'{self.conf.gamedir}/.',
+ )
+ debug('running command %s', command)
+ with contextlib.ExitStack() as stack:
+ proc = stack.enter_context(subprocess.Popen(
+ command,
+ stdout=subprocess.PIPE,
+ encoding='utf8'))
+ selector = stack.enter_context(selectors.DefaultSelector())
+ selector.register(proc.stdout, selectors.EVENT_READ)
+ yield from self._worker_rsync_loop(proc=proc, selector=selector)
+
+ def _worker_unarchive(self):
+ with contextlib.ExitStack() as stack:
+ lzip_command = '/usr/bin/lzip'
+ plzip_command = '/usr/bin/plzip'
+ if os.path.exists(plzip_command):
+ lzip_command = plzip_command
+ lzip_proc = stack.enter_context(subprocess.Popen(
+ (lzip_command, '-d'),
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ ))
+ tar_proc = stack.enter_context(subprocess.Popen(
+ ('tar', '-C', self.output_dir, '-xvf', '-'),
+ stdin=lzip_proc.stdout,
+ stdout=subprocess.PIPE,
+ ))
+ filepath = f'{self.conf.gamedir}/{self.conf.game_data["package_name"]}'
+ filepath_status = f'{self.conf.gamedir}/{self.conf.game_data["name"]}.yaml'
+ fobj = stack.enter_context(open(filepath, 'rb'))
+ selector = stack.enter_context(selectors.DefaultSelector())
+ selector.register(tar_proc.stdout, selectors.EVENT_READ)
+ filesize = os.stat(filepath).st_size
+ def _itemscounter():
+ while ready := selector.select(0.100):
+ data = tar_proc.stdout.read1()
+ if len(data) == 0:
+ break
+ itemscount += data.count(b'\n')
+ itemscount = 0
+ while True:
+ with contextlib.suppress(queue.Empty):
+ if self.queue.get_nowait() == self.Command.STOP:
+ break
+ data = fobj.read(131072) # 128kib chunks
+ if len(data) == 0:
+ lzip_proc.stdin.close()
+ break
+ lzip_proc.stdin.write(data)
+ yield filesize / fobj.tell()
+ _itemscounter()
+ _itemscounter()
+ with open(filepath_status, 'wb') as fobj:
+ yaml.safe_dump({
+ 'installed_count': itemscount,
+ 'installed_version': self.conf.game_data['version'],
+ 'installed_package_name': self.conf.game_data['package_name'],
+ }, fobj)
+
+ def _worker_rmarchive(self):
+ os.unlink(f'{self.conf.gamedir}/{self.conf.game_data["package_name"]}')
+
+
+class GameRemover(GameManagerBase):
+ '''
+ Defines how to remove a game. Basically removes a directory, with progress
+ report.
+ '''
+
+ def __init__(self, conf):
+ super().__init__(conf)
+ self.steps = (self._worker_remove, self._worker_rm_data)
+
+ def _worker_remove(self):
+ if self.game_status is None:
+ return
+ extracted_path = '/'.join((
+ self.conf.gamedir,
+ strip_tar_ext(self.game_status['installed_package_name']),
+ ))
+ filepath_status = f'{self.conf.gamedir}/{self.conf.game_data["name"]}.yaml'
+ with open(filepath_status) as stream:
+ pkg_status = yaml.safe_load(stream)
+ with contextlib.ExitStack() as stack:
+ proc = stack.enter_context(subprocess.Popen(
+ ('rm', '-rvf', '--', extracted_path),
+ stdout=subprocess.PIPE,
+ ))
+ selector = stack.enter_context(selectors.DefaultSelector())
+ selector.register(proc.stdout, selectors.EVENT_READ)
+ itemscount = 0
+ while True:
+ with contextlib.suppress(queue.Empty):
+ if self.queue.get_nowait() == self.Command.STOP:
+ proc.terminate()
+ break
+ if ready := selector.select(0.250):
+ data = proc.stdout.read1()
+ if len(data) == 0:
+ break
+ itemscount += data.count(b'\n')
+ yield itemscount*100/pkg_status['installed_count']
+
+ def _worker_rm_data(self):
+ if self.game_status is None:
+ return
+ os.unlink(f'{self.conf.gamedir}/{self.conf.game_data["name"]}.yaml')
+
+
+class GameUpdater(GameManagerBase):
+ '''
+ Combine GameRemover and GameInstaller to form a consistent updating unit.
+ '''
+
+ def __init__(self, conf):
+ super().__init__(conf)
+ self._remover = GameRemover(conf)
+ self._installer = GameInstaller(conf)
+ self.steps = self._remover.steps + self._installer.steps
+
+
+ def _worker(self):
+
+
+#class GameData:
+#
+# class
+
+
+class GameManager:
+
+ class State(Enum):
+ IDLE = enum_auto()
+ INSTALLING = enum_auto()
+ REMOVING = enum_auto()
+ UPDATING_REMOVING = enum_auto()
+ UPDATING_INSTALLING = enum_auto()
+
+ def __init__(self, games_data_yaml_path, games_dir):
+ self.games_data_yaml_path = games_data_yaml_path
+ self.games_dir = games_dir
+ self.state = self.State.IDLE
+ self.update_state = self.State.IDLE
+ self.current_game_name = None
+ self.current_game_item = None
+ self.current_game_status = {}
+
+ with open(games_data_yaml_path) as stream:
+ self.games_data = yaml.safe_load(stream)
+
+ self.game_installer = GameInstaller(
+ self.games_data['repository']['host'],
+ self.games_data['repository']['path'],
+ self.games_dir,
+ )
+ self.game_remover = GameRemover(
+ self.games_data['repository']['host'],
+ self.games_data['repository']['path'],
+ self.games_dir,
+ )
+
+ def get_game_item(self, name):
+ self.current_game_item = next(
+ item
+ for item in self.games_data['games']
+ if item['name'] == name
+ )
+ return self.current_game_item
+
+ def get_game_status(self, name):
+ self.current_game_status = {}
+ with contextlib.suppress(FileNotFoundError):
+ with open(f'{self.conf.gamedir}/{name}.yaml') as stream:
+ self.current_game_status = yaml.safe_load(stream)
+ return self.current_game_status
+
+ def is_game_updatable(self, name):
+ #self. ### FIXME: Stopped here ?
+ if not self.current_game_name:
+ return False
+ ivers = self.current_game_status.get('installed_version', None)
+ # only call "updatable" if there was at least a version installed
+ # else, this is just an "installable" package.
+ if ivers is not None and ivers < self.current_game_item['version']:
+ return True
+ return False
+
+ def set_current_game(self, name):
+ if self.current_game_name == name:
+ return
+ self.current_game_name = name
+ self.get_game_item(name)
+ self.get_game_status(name)
+
+ def start_install(self, name):
+ if self.state != self.State.IDLE:
+ return
+ self.status = self.State.INSTALLING
+ self.set_current_game(name)
+ self.game_installer.start(
+ self.current_game_item,
+ game_status=self.current_game_status,
+ )
+
+ def start_remove(self, name):
+ if self.state != self.State.IDLE:
+ return
+ self.status = self.State.REMOVING
+ self.set_current_game(name)
+ self.game_remover.start(
+ self.current_game_item,
+ game_status=self.current_game_status,
+ )
+
+ def start_update(self, name):
+ if self.state != self.State.IDLE:
+ return
+ self.status = self.State.UPDATING_REMOVING
+ self.set_current_game(name)
+ self.game_remover.start(
+ self.current_game_item,
+ game_status=self.current_game_status,
+ )
+
+ def stop(self):
+ self.state = self.State.IDLE
+ self.game_remover.stop()
+ self.game_installer.stop()
+
+ def poll(self):
+ entry_state = self.state
+ return_value = {
+ self.State.IDLE: (lambda: GameManagerBase.State.IDLE, 0, 0),
+ self.State.INSTALLING: self.game_installer.poll,
+ self.State.REMOVING: self.game_remover.poll,
+ self.State.UPDATING_REMOVING: self.game_remover.poll,
+ self.State.UPDATING_INSTALLING: self.game_installer.poll,
+ }[entry_state]()
+ if return_value[0] == GameManagerBase.Sate.IDLE:
+ self.state = self.State.IDLE
+ if entry_state == self.State.UPDATING_REMOVING:
+ self.start_install(self.current_game_name)
+ return_value = self.poll()
+ return self.state, return_value[1], return_value[2]
diff --git a/gamechest/gamemanager_method1.py b/gamechest/gamemanager_method1.py
new file mode 100644
index 0000000..9e2d4ac
--- /dev/null
+++ b/gamechest/gamemanager_method1.py
@@ -0,0 +1,252 @@
+#!python3
+
+import contextlib
+import os
+import queue
+import re
+import selectors
+import shutil
+import subprocess
+import tarfile
+import threading
+from logging import debug, info, warning, critical
+from enum import Enum, auto as enum_auto
+
+
+class TarLzipExtractor:
+
+ class State(Enum):
+ IDLE = enum_auto()
+ ONGOING = enum_auto()
+
+ class Command(Enum):
+ STOP = enum_auto()
+
+ def __init__(self, output_dir):
+ self.output_dir = output_dir
+ self.state_lock = threading.Lock()
+ self.state = self.State.IDLE
+ self.queue = queue.Queue(maxsize=10)
+ self.progress_lock = threading.Lock()
+ self.progress = 0
+
+ def _thread_worker(self, *args):
+ lzip_proc = tar_proc = None
+ try:
+ lzip_command = '/usr/bin/lzip'
+ plzip_command = '/usr/bin/plzip'
+ if os.path.exists(plzip_command):
+ lzip_command = plzip_command
+ lzip_proc = subprocess.Popen(
+ (lzip_command, '-d'),
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ )
+ tar_proc = subprocess.Popen(
+ ('tar', '-C', self.output_dir, '-xf', '-'),
+ stdin=lzip_proc.stdout,
+ )
+ filepath = f'{self.output_dir}/{self.filename}'
+ filesize = os.stat(filepath).st_size
+ progress = 0
+ with open(filepath, 'rb') as fobj:
+ while True:
+ with contextlib.suppress(queue.Empty):
+ queue_command = self.queue.get_nowait()
+ if queue_command == self.Command.STOP:
+ return
+ data = fobj.read(131072) # 128kib chunks
+ if len(data) == 0:
+ lzip_proc.stdin.close()
+ return
+ lzip_proc.stdin.write(data)
+ with self.progress_lock:
+ self.progress = filesize / fobj.tell()
+ finally:
+ with self.state_lock:
+ self.state = self.State.IDLE
+ # first terminate all, then wait all, quicker than terminate/wait
+ # for each.
+ lzip_proc or lzip_proc.terminate()
+ tar_proc or tar_proc.terminate()
+ lzip_proc or lzip_proc.wait()
+ tar_proc or tar_proc.wait()
+
+ def start(self, filename):
+
+ with self.state_lock:
+ if self.state == self.State.ONGOING:
+ return
+ self.state = self.State.ONGOING
+
+ self.filename = filename
+
+ self.thread = threading.Thread(
+ target=self._thread_worker,
+ daemon=True)
+ self.thread.start()
+
+ def stop(self):
+
+ with self.state_lock:
+ if self.state != self.State.ONGOING:
+ return
+ self.state = self.State.IDLE
+
+ self.queue.put(self.Command.STOP)
+
+ def poll(self):
+ with self.state_lock, self.progress_lock:
+ return self.state, self.progress
+
+
+class RsyncManager:
+
+ rsync_progress_re = re.compile(r'\s(\d+)%\s')
+
+ class State(Enum):
+ IDLE = enum_auto()
+ ONGOING = enum_auto()
+
+ def __init__(self, repository_host, repository_path, gamedir):
+ self.selector = selectors.DefaultSelector()
+ self.repository_host = repository_host
+ self.repository_path = repository_path
+ self.gamedir = gamedir
+
+ self.state = self.State.IDLE
+ self.proc = None
+ self.package_name = None
+ self.last_progress = 0
+
+ def start(self, package_name):
+ 'returns True when package_name is being installed'
+
+ if self.state == self.State.ONGOING:
+ return
+
+ self.package_name = package_name
+ command = (
+ 'rsync',
+ '-a',
+ '--partial',
+ '--info=progress2',
+ f'{self.repository_host}:{self.repository_path}/{package_name}',
+ f'{self.gamedir}/.',
+ )
+ debug('running command %s', command)
+ self.proc = subprocess.Popen(
+ command,
+ stdout=subprocess.PIPE,
+ encoding='utf8')
+ self.selector.register(self.proc.stdout, selectors.EVENT_READ)
+ self.last_progress = 0
+ self.state = self.State.ONGOING
+
+ return True
+
+ def stop(self):
+ if self.state == self.State.IDLE:
+ return
+ if self.proc.poll() is None:
+ debug('terminating unfinished game install process for %s',
+ self.package_name)
+ self.proc.terminate()
+ proc_return_code = self.proc.wait()
+ debug('game install process return status %d', proc_return_code)
+ self.selector.unregister(self.proc.stdout)
+ self.proc = None
+ self.install_ongoing = False
+
+ def poll(self):
+ if self.state == self.State.IDLE:
+ return self.state, self.last_progress
+
+ proc = self.proc
+ if proc.poll() is not None:
+ self.stop()
+ self.state = self.State.IDLE
+ return self.state, 100
+
+ if not any(key
+ for key, mask in self.selector.select(timeout=0)
+ if mask & selectors.EVENT_READ and key.fileobj == proc.stdout):
+ return self.state, self.last_progress
+
+ # stdout.readline is a blocking call if there is no endline in
+ # stdout outputed by the subprocess.
+ # normally rsync output should be line buffered thus at any read event
+ # a call to readline should not block (or not long enough to be
+ # visible in a gui).
+ line = proc.stdout.readline()
+ if match := self.rsync_progress_re.search(line):
+ progress = int(match.group(1))
+ self.last_progress = progress
+
+ return self.state, self.last_progress
+
+
+class GameArchiveRemover:
+
+ def __init__(self, gamedir):
+ self.gamedir = gamedir
+
+ def start(self, package_name):
+ pass
+
+ def stop(self):
+ pass
+
+ def poll(self):
+ pass
+
+
+class GameInstaller:
+
+ class State(Enum):
+ IDLE = enum_auto()
+ ONGOING = enum_auto()
+
+ def __init__(self, repository_host, repository_path, gamedir):
+ self.state = self.State.IDLE
+ self.step = None
+ self.steps = (
+ RsyncManager(
+ repository_host=repository_host,
+ repository_path=repository_path,
+ gamedir=gamedir
+ ),
+ TarLzipExtractor(output_dir=gamedir),
+ )
+
+ def start(self, package_name):
+ if self.state == self.State.ONGOING:
+ return
+ self.step = 0
+ self.steps[self.step].start(package_name)
+ self.package_name = package_name
+ self.state = self.State.ONGOING
+ #self.rsync_manager.start(package_name)
+ #self.current_sub_process = self.rsync_manager
+ return True
+
+ def stop(self):
+ if self.state == self.State.IDLE:
+ return
+ self.steps[self.step].stop()
+ #self.current_sub_process.stop()
+ self.state = self.State.IDLE
+
+ def poll(self):
+ 'update internal state and returns current state'
+ if self.state == self.State.IDLE:
+ return self.State.IDLE, 100, 100
+ sub_state, sub_progress = self.steps[self.step].poll()
+ if sub_state == self.steps[self.step].State.IDLE:
+ self.step += 1
+ if self.step >= len(self.steps):
+ self.state = self.State.IDLE
+ return self.State.IDLE, 100, 100
+ self.steps[self.step].start(self.package_name)
+ sub_progress = 0
+ return self.state, (self.step/len(self.steps))*100, sub_progress
diff --git a/gamechest/stepper.py b/gamechest/stepper.py
new file mode 100644
index 0000000..b6dd335
--- /dev/null
+++ b/gamechest/stepper.py
@@ -0,0 +1,69 @@
+#!python3
+
+import collections
+import copy
+from enum import Enum, auto as enum_auto
+
+Progress = collections.namedtuple('Progress', 'step index count percent')
+Progress.step.__doc__ = 'name of the current step'
+Progress.index.__doc__ = 'current step index out of count steps'
+Progress.count.__doc__ = 'total number of steps'
+Progress.percent.__doc__ = 'advance in percent of the current step'
+
+class Stepper:
+ '''
+ Manage a worker processing steps in background and allows polling its
+ progress.
+
+ Takes steps as a dict of step_names/step_worker.
+ A step_worker is a function generator taking a queue waiting for a stop
+ command to abort the step and yielding progress of the step as
+ a percentage, and as second argument it takes a GameChestConfig.
+ '''
+
+ class Command(Enum):
+ STOP = enum_auto()
+
+ def _create_thread(self):
+ return threading.Thread(target=self.worker, daemon=True)
+
+ def __init__(self, conf, steps):
+ self.conf = conf
+ self.thread = None
+ self.commandq = queue.Queue(maxsize=10)
+ self.progressq = queue.Queue(maxsize=10)
+ self.steps = steps
+ self.count = len(steps)
+ self.steps.__doc__ = 'steps are a key/func-value dict'
+ self.thread = self._create_thread()
+ self.last_progress = Progress('unknown', 0, self.count, 0)
+ self.status = {}
+
+ def worker(self, *args):
+ for index, step, step_worker in enumerate(self.steps.items()):
+ self.progressq.put(Progress(step, index, self.count, 0))
+ for percent in step_worker(self.commandq, self.conf, self.status):
+ self.progressq.put(Progress(step, index, self.count, percent))
+
+ def start(self, status):
+ if self.is_alive():
+ return
+ self.status = copy.deepcopy(status)
+ self.thread.start()
+
+ def stop(self):
+ if not self.is_alive():
+ return
+ self.commandq.put(self.Command.STOP)
+ self.thread.join()
+ self.thread = self._create_thread()
+
+ def is_alive(self):
+ return self.thread.is_alive()
+
+ def poll(self):
+ with contextlib.suppress(queue.Empty):
+ progress = self.progress_queue.get_nowait()
+ self.last_progress = progress
+ return self.last_progress
+
diff --git a/gamechest/steps_install.py b/gamechest/steps_install.py
new file mode 100644
index 0000000..7c508bb
--- /dev/null
+++ b/gamechest/steps_install.py
@@ -0,0 +1,109 @@
+#!python3
+
+import contextlib
+import os
+import re
+import selectors
+import subprocess
+
+from stepper import Stepper
+
+_rsync_progress_re = re.compile(r'\s(\d+)%\s')
+
+def _rsync_loop(queue, proc, selector):
+ while True:
+ with contextlib.suppress(queue.Empty):
+ if queue.get_nowait() == Stepper.Command.STOP:
+ proc.terminate()
+ break
+
+ if proc.poll() is not None:
+ break
+
+ if not any(key
+ for key, mask in selector.select(timeout=0.250)
+ if mask & selectors.EVENT_READ and key.fileobj == proc.stdout):
+ continue
+
+ # stdout.readline is a blocking call if there is no endline in
+ # stdout outputed by the subprocess.
+ # normally rsync output should be line buffered thus at any read event
+ # a call to readline should not block (or not long enough to be
+ # able to process a queued command quick enough).
+ line = proc.stdout.readline()
+ if match := _rsync_progress_re.search(line):
+ progress = int(match.group(1))
+ yield progress
+
+def step_rsync(queue, conf, status):
+ package_name = conf.game_data['package_name']
+ command = (
+ 'rsync',
+ '-a',
+ '--partial',
+ '--info=progress2',
+ f'{conf.repository_host}:{conf.repository_path}/{package_name}',
+ f'{conf.gamedir}/.',
+ )
+ debug('running command %s', command)
+ with contextlib.ExitStack() as stack:
+ proc = stack.enter_context(subprocess.Popen(
+ command,
+ stdout=subprocess.PIPE,
+ encoding='utf8'))
+ selector = stack.enter_context(selectors.DefaultSelector())
+ selector.register(proc.stdout, selectors.EVENT_READ)
+ yield from _rsync_loop(queue, proc=proc, selector=selector)
+
+def step_extract(queue, conf, status):
+ with contextlib.ExitStack() as stack:
+ lzip_command = '/usr/bin/lzip'
+ plzip_command = '/usr/bin/plzip'
+ if os.path.exists(plzip_command):
+ lzip_command = plzip_command
+ lzip_proc = stack.enter_context(subprocess.Popen(
+ (lzip_command, '-d'),
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ ))
+ tar_proc = stack.enter_context(subprocess.Popen(
+ ('tar', '-C', conf.gamedir, '-xvf', '-'),
+ stdin=lzip_proc.stdout,
+ stdout=subprocess.PIPE,
+ ))
+ filepath = f'{conf.gamedir}/{conf.game_data["package_name"]}'
+ filepath_status = f'{conf.gamedir}/{conf.game_data["name"]}.yaml'
+ fobj = stack.enter_context(open(filepath, 'rb'))
+ selector = stack.enter_context(selectors.DefaultSelector())
+ selector.register(tar_proc.stdout, selectors.EVENT_READ)
+ filesize = os.stat(filepath).st_size
+ def _itemscounter():
+ while ready := selector.select(0.100):
+ data = tar_proc.stdout.read1()
+ if len(data) == 0:
+ break
+ itemscount += data.count(b'\n')
+ itemscount = 0
+ while True:
+ with contextlib.suppress(queue.Empty):
+ if queue.get_nowait() == Stepper.Command.STOP:
+ break
+ data = fobj.read(131072) # 128kib chunks
+ if len(data) == 0:
+ lzip_proc.stdin.close()
+ break
+ lzip_proc.stdin.write(data)
+ yield filesize / fobj.tell()
+ _itemscounter()
+ _itemscounter()
+ with open(filepath_status, 'wb') as fobj:
+ yaml.safe_dump({
+ 'installed_count': itemscount,
+ 'installed_version': conf.game_data['version'],
+ 'installed_package_name': conf.game_data['package_name'],
+ }, fobj)
+
+def step_clean(queue, conf, status):
+ os.unlink(f'{conf.gamedir}/{conf.game_data["package_name"]}')
+ yield 100
+
diff --git a/gamechest/steps_remove.py b/gamechest/steps_remove.py
new file mode 100644
index 0000000..6b9144a
--- /dev/null
+++ b/gamechest/steps_remove.py
@@ -0,0 +1,31 @@
+#!python3
+
+import contextlib
+import subprocess
+
+from stepper import Stepper
+
+def step_remove(queue, conf, status):
+ extracted_path = '/'.join((
+ conf.gamedir,
+ strip_tar_ext(status['installed_package_name']),
+ ))
+ with contextlib.ExitStack() as stack:
+ proc = stack.enter_context(subprocess.Popen(
+ ('rm', '-rvf', '--', extracted_path),
+ stdout=subprocess.PIPE,
+ ))
+ selector = stack.enter_context(selectors.DefaultSelector())
+ selector.register(proc.stdout, selectors.EVENT_READ)
+ itemscount = 0
+ while True:
+ with contextlib.suppress(queue.Empty):
+ if queue.get_nowait() == Stepper.Command.STOP:
+ proc.terminate()
+ break
+ if ready := selector.select(0.250):
+ data = proc.stdout.read1()
+ if len(data) == 0:
+ break
+ itemscount += data.count(b'\n')
+ yield itemscount*100/status['installed_count']
diff --git a/gamechest/utils.py b/gamechest/utils.py
new file mode 100644
index 0000000..adc55e7
--- /dev/null
+++ b/gamechest/utils.py
@@ -0,0 +1,19 @@
+#!python3
+
+import threading
+
+class LockedData:
+ 'every attribute access to this class is locked except lock and data'
+
+ def __init__(self, data={}):
+ super().__setattr__('lock', threading.Lock())
+ super().__setattr__('data', dict())
+ self.data.update(data)
+
+ def __getattr__(self, name):
+ with self.lock:
+ return self.data[name]
+
+ def __setattr__(self, name, value):
+ with self.lock:
+ self.data[name] = value