pendify 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pendify-0.1.0/LICENSE +21 -0
- pendify-0.1.0/MANIFEST.in +1 -0
- pendify-0.1.0/PKG-INFO +155 -0
- pendify-0.1.0/README.md +139 -0
- pendify-0.1.0/pyproject.toml +27 -0
- pendify-0.1.0/setup.cfg +4 -0
- pendify-0.1.0/src/pendify/__init__.py +1 -0
- pendify-0.1.0/src/pendify/__main__.py +265 -0
- pendify-0.1.0/src/pendify/alert.py +132 -0
- pendify-0.1.0/src/pendify/autostart.py +105 -0
- pendify-0.1.0/src/pendify/client.py +223 -0
- pendify-0.1.0/src/pendify/codes.py +38 -0
- pendify-0.1.0/src/pendify/config.py +223 -0
- pendify-0.1.0/src/pendify/icon.py +14 -0
- pendify-0.1.0/src/pendify/page.py +1136 -0
- pendify-0.1.0/src/pendify/pairing.py +192 -0
- pendify-0.1.0/src/pendify/pendify.ico +0 -0
- pendify-0.1.0/src/pendify/plate_almena.py +348 -0
- pendify-0.1.0/src/pendify/qr.py +433 -0
- pendify-0.1.0/src/pendify/runfile.py +246 -0
- pendify-0.1.0/src/pendify/watcher.py +348 -0
- pendify-0.1.0/src/pendify/worker.py +159 -0
- pendify-0.1.0/src/pendify.egg-info/PKG-INFO +155 -0
- pendify-0.1.0/src/pendify.egg-info/SOURCES.txt +24 -0
- pendify-0.1.0/src/pendify.egg-info/dependency_links.txt +1 -0
- pendify-0.1.0/src/pendify.egg-info/top_level.txt +1 -0
pendify-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ElkinDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prune tests
|
pendify-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pendify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pairs this PC with a phone account by a QR on a loopback page and sends it two kinds of alerts.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ElkinDev/PendiFy
|
|
7
|
+
Project-URL: Source, https://github.com/ElkinDev/PendiFy
|
|
8
|
+
Keywords: notifications,alerts,phone,qr,pairing,windows
|
|
9
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# PendiFy
|
|
18
|
+
|
|
19
|
+
## Español
|
|
20
|
+
|
|
21
|
+
Un programa pequeño para Windows que avisa en tu teléfono cuando se encuentra tu partida y cuando empieza.
|
|
22
|
+
Se enlaza una sola vez con tu cuenta de Pendi leyendo un código QR, sin crear ninguna cuenta nueva.
|
|
23
|
+
Es gratis, no pide permisos de administrador y solo usa Python.
|
|
24
|
+
|
|
25
|
+
### Instalar
|
|
26
|
+
|
|
27
|
+
Pulsa Windows + R, pega esta línea completa y pulsa Enter. También sirve en PowerShell o en el Símbolo del sistema.
|
|
28
|
+
|
|
29
|
+
powershell -NoExit -NoProfile -ExecutionPolicy Bypass -Command "ri ~\pendify-install.ps1 -ea 0; irm https://raw.githubusercontent.com/ElkinDev/PendiFy/main/install.ps1 -OutFile ~\pendify-install.ps1; ~\pendify-install.ps1"
|
|
30
|
+
|
|
31
|
+
La línea guarda el instalador como `pendify-install.ps1` en tu carpeta de usuario y lo ejecuta desde ahí; puedes borrar ese archivo cuando termine.
|
|
32
|
+
|
|
33
|
+
La ventana queda abierta al terminar para que leas el resultado; ciérrala cuando acabe.
|
|
34
|
+
|
|
35
|
+
Si Windows dice que no puede acceder al archivo o el antivirus detiene la línea, no se instaló nada: actualiza las definiciones del antivirus y vuelve a intentarlo, o descarga `install.ps1` desde la página del repositorio, haz clic en él con el botón derecho y elige «Ejecutar con PowerShell».
|
|
36
|
+
|
|
37
|
+
Si ves "'irm' no se reconoce como un comando interno o externo", pegaste solo la parte corta en el Símbolo del sistema: usa la línea completa de arriba.
|
|
38
|
+
|
|
39
|
+
El instalador busca Python 3.10 o más nuevo; si no lo hay lo instala con winget solo para tu usuario, o te indica https://www.python.org/downloads/ cuando winget no existe. Después instala PendiFy con pip, deja un acceso directo `PendiFy` en el Escritorio y en el menú Inicio, y lo inicia.
|
|
40
|
+
|
|
41
|
+
En PowerShell: para ver qué haría sin cambiar nada: `$env:PENDIFY_DRYRUN = "1"` antes de pegar la línea. Para no iniciarlo al final: `$env:PENDIFY_NOSTART = "1"`.
|
|
42
|
+
|
|
43
|
+
Otra forma, sin ejecutar ningún script: si el PC ya tiene Python 3.10 o más nuevo, o si en él no se permite ejecutar scripts, instálalo con pip e inícialo:
|
|
44
|
+
|
|
45
|
+
python -m pip install --upgrade pendify
|
|
46
|
+
python -m pendify
|
|
47
|
+
|
|
48
|
+
Si Windows responde que no encuentra `python`, usa `py` en su lugar: `py -m pip install --upgrade pendify` y `py -m pendify`.
|
|
49
|
+
|
|
50
|
+
Así no se crean los accesos directos en el Escritorio ni en el menú Inicio, y el inicio con Windows queda en el interruptor de la página. Para actualizar, pulsa «Salir» en la página, ejecuta otra vez la misma línea de pip y vuelve a iniciarlo. Para quitarlo, apaga primero el interruptor «Iniciar con Windows» en la página, pulsa «Salir» y después ejecuta `python -m pip uninstall pendify`. La configuración en `%APPDATA%\pendify`, con el enlace con el teléfono, se queda; puedes borrar esa carpeta a mano.
|
|
51
|
+
|
|
52
|
+
### Enlazar
|
|
53
|
+
|
|
54
|
+
Al iniciarse, el navegador abre una página local con un código QR. Escanéalo con la cámara del teléfono: se abre la app Pendi y te pide confirmar el enlace. La página muestra cuando el enlace quedó hecho.
|
|
55
|
+
|
|
56
|
+
El código QR y la clave se muestran durante un minuto al pulsar «Mostrar el código» y luego se ocultan de nuevo.
|
|
57
|
+
|
|
58
|
+
El botón de arriba a la derecha cambia entre el tema claro y el oscuro, y el programa guarda la elección para la próxima vez.
|
|
59
|
+
|
|
60
|
+
### Iniciar, detener, desinstalar
|
|
61
|
+
|
|
62
|
+
- Iniciar: el acceso directo `PendiFy`, o `pythonw -m pendify`, que es lo que ejecuta el acceso directo; sin consola, si ya está abierto o no puede iniciarse te lo dice en una ventana. Para verlo en una consola: `python -m pendify`.
|
|
63
|
+
- Detener: pulsa «Salir» en la página del programa; si no la tienes abierta, iniciarlo otra vez la abre. Como último recurso, cierra el proceso `pythonw.exe` en el Administrador de tareas.
|
|
64
|
+
- Pausar: «Pausar avisos», en la tarjeta «Este PC» de la página, deja de leer el cliente del juego: no acepta partidas ni avisa a tu teléfono, y la página sigue abierta. «Reanudar avisos» vuelve a leerlo. La pausa no se guarda: cada vez que el programa se inicia, empieza activo.
|
|
65
|
+
- Iniciar con Windows: el interruptor «Iniciar con Windows», en la misma tarjeta, está apagado hasta que lo enciendas. Encendido, el programa empieza solo al iniciar sesión en Windows, solo para tu usuario y sin abrir el navegador ni la página.
|
|
66
|
+
- Actividad: la tarjeta «Actividad» de la página lista lo que el programa hizo desde que empezó: el cliente del juego encontrado y perdido, cada fase, la partida aceptada y el aviso enviado. Guarda las últimas 50 líneas, solo en memoria.
|
|
67
|
+
- Idioma: la página sigue el idioma del navegador hasta que eliges ES o EN con el selector de arriba, y la elección se guarda para este PC.
|
|
68
|
+
- Desinstalar: pega `powershell -NoExit -NoProfile -ExecutionPolicy Bypass -Command "ri ~\pendify-uninstall.ps1 -ea 0; irm https://raw.githubusercontent.com/ElkinDev/PendiFy/main/uninstall.ps1 -OutFile ~\pendify-uninstall.ps1; ~\pendify-uninstall.ps1"` igual que la línea de instalación; deja `pendify-uninstall.ps1` en tu carpeta de usuario, que puedes borrar. Quita el paquete del mismo Python que lo instaló, comprueba que ya no está, quita los dos accesos directos y quita el inicio con Windows si estaba encendido; la configuración en `%APPDATA%\pendify` se queda y te dice dónde está.
|
|
69
|
+
- Actualizar: pulsa «Salir» en la página y vuelve a pegar la línea de instalación; instala la última versión y conserva la configuración y el enlace.
|
|
70
|
+
|
|
71
|
+
### Qué envía y a quién
|
|
72
|
+
|
|
73
|
+
Solo envía, al servicio de enlace de Pendi, el tipo de aviso (partida encontrada o partida empezada) junto con el identificador del enlace y el secreto de este PC, que viven en `%APPDATA%\pendify\config.json`. El servicio lo entrega a la cuenta que enlazaste. No envía tu nombre, tus partidas ni nada más del juego. Para saber cuándo empieza la partida lee el reloj del juego en este mismo PC, y no lo guarda ni lo envía.
|
|
74
|
+
|
|
75
|
+
### `--dry`
|
|
76
|
+
|
|
77
|
+
Con `--dry` el programa solo avisa y no acepta la partida por ti.
|
|
78
|
+
|
|
79
|
+
Licencia: MIT
|
|
80
|
+
|
|
81
|
+
## English
|
|
82
|
+
|
|
83
|
+
A small Windows program that alerts your phone when your match is found and when it starts.
|
|
84
|
+
It links once to your Pendi account by scanning a QR code, with no new account of any kind.
|
|
85
|
+
It is free, needs no administrator rights and only uses Python.
|
|
86
|
+
|
|
87
|
+
### Install
|
|
88
|
+
|
|
89
|
+
Press Windows + R, paste this whole line and press Enter. It also works in PowerShell or the Command Prompt.
|
|
90
|
+
|
|
91
|
+
powershell -NoExit -NoProfile -ExecutionPolicy Bypass -Command "ri ~\pendify-install.ps1 -ea 0; irm https://raw.githubusercontent.com/ElkinDev/PendiFy/main/install.ps1 -OutFile ~\pendify-install.ps1; ~\pendify-install.ps1"
|
|
92
|
+
|
|
93
|
+
The line saves the installer as `pendify-install.ps1` in your user folder and runs it from there; you can delete that file when it is done.
|
|
94
|
+
|
|
95
|
+
The window stays open at the end so you can read the result; close it when it is done.
|
|
96
|
+
|
|
97
|
+
If Windows says it cannot access the file or the antivirus stops the line, nothing was installed: update the antivirus definitions and try again, or download `install.ps1` from the repository page, click it with the right button and choose "Run with PowerShell".
|
|
98
|
+
|
|
99
|
+
If you see "'irm' is not recognized as an internal or external command", you pasted only the short part into the Command Prompt: use the whole line above.
|
|
100
|
+
|
|
101
|
+
The installer looks for Python 3.10 or newer; when there is none it installs it with winget for your user only, or points you to https://www.python.org/downloads/ when winget is missing. Then it installs PendiFy with pip, leaves a `PendiFy` shortcut on the Desktop and in the Start menu, and starts it.
|
|
102
|
+
|
|
103
|
+
In PowerShell: to see what it would do without changing anything, set `$env:PENDIFY_DRYRUN = "1"` before pasting the line. To leave it stopped at the end, set `$env:PENDIFY_NOSTART = "1"`.
|
|
104
|
+
|
|
105
|
+
Another way, with no script at all: on a PC that already has Python 3.10 or newer, or where scripts are not allowed, install it with pip and start it:
|
|
106
|
+
|
|
107
|
+
python -m pip install --upgrade pendify
|
|
108
|
+
python -m pendify
|
|
109
|
+
|
|
110
|
+
If Windows answers that `python` was not found, use `py` in its place: `py -m pip install --upgrade pendify` and `py -m pendify`.
|
|
111
|
+
|
|
112
|
+
This way leaves no shortcut on the Desktop or in the Start menu, and the start with Windows is the switch on the page. To update, press «Quit» on the page, run the same pip line again and start it again. To remove it, first turn off the «Start with Windows» switch on the page, press «Quit», then run `python -m pip uninstall pendify`. The configuration in `%APPDATA%\pendify`, with the link to the phone, stays; you can delete that folder by hand.
|
|
113
|
+
|
|
114
|
+
### Link
|
|
115
|
+
|
|
116
|
+
When it starts, the browser opens a local page with a QR code. Scan it with the phone's camera: the Pendi app opens and asks you to confirm the link. The page shows when the link is done.
|
|
117
|
+
|
|
118
|
+
The QR code and the key show for a minute when «Show the code» is pressed, and then hide again.
|
|
119
|
+
|
|
120
|
+
The button at the top right switches between the light and the dark theme, and the program keeps the choice for the next time.
|
|
121
|
+
|
|
122
|
+
### Start, stop, uninstall
|
|
123
|
+
|
|
124
|
+
- Start: the `PendiFy` shortcut, or `pythonw -m pendify`, which is what the shortcut runs; with no console, it tells you in a window when it is already running or cannot start. To see it in a console: `python -m pendify`.
|
|
125
|
+
- Stop: press «Quit» on the program's page; if it is not open, starting the program again opens it. As a last resort, end the `pythonw.exe` process in Task Manager.
|
|
126
|
+
- Pause: «Pause alerts», on the page's «This PC» card, stops reading the game client: it accepts no match and sends no alert to your phone, and the page stays open. «Resume alerts» reads it again. The pause is not kept: every start of the program is active again.
|
|
127
|
+
- Start with Windows: the «Start with Windows» switch, on the same card, is off until you turn it on. When it is on, the program starts by itself when you sign in to Windows, for your user only, without opening the browser or the page.
|
|
128
|
+
- Activity: the page's «Activity» card lists what the program did since it started: the game client found and lost, each phase, the match accepted and the alert sent. It keeps the last 50 lines, in memory only.
|
|
129
|
+
- Language: the page follows the browser's language until you choose ES or EN with the switch at the top, and the choice is kept for this PC.
|
|
130
|
+
- Uninstall: paste `powershell -NoExit -NoProfile -ExecutionPolicy Bypass -Command "ri ~\pendify-uninstall.ps1 -ea 0; irm https://raw.githubusercontent.com/ElkinDev/PendiFy/main/uninstall.ps1 -OutFile ~\pendify-uninstall.ps1; ~\pendify-uninstall.ps1"` the same way as the install line; it leaves `pendify-uninstall.ps1` in your user folder, which you can delete. It removes the package from the same Python that installed it, checks that it is gone, removes both shortcuts and removes the start with Windows when it is on; the configuration in `%APPDATA%\pendify` stays, and it tells you where it is.
|
|
131
|
+
- Update: press "Quit" on the page and paste the install line again; it installs the latest version and keeps the configuration and the link.
|
|
132
|
+
|
|
133
|
+
### What it sends and to whom
|
|
134
|
+
|
|
135
|
+
It only sends, to Pendi's link service, the alert kind (match found or match started) together with the link id and this PC's secret, which live in `%APPDATA%\pendify\config.json`. The service delivers it to the account you linked. It sends no name, no match history and nothing else from the game. To tell when the match starts it reads the game's clock on this same PC, and neither keeps nor sends it.
|
|
136
|
+
|
|
137
|
+
### `--dry`
|
|
138
|
+
|
|
139
|
+
With `--dry` the program only alerts and never accepts the match for you.
|
|
140
|
+
|
|
141
|
+
License: MIT
|
|
142
|
+
|
|
143
|
+
## Developer commands
|
|
144
|
+
|
|
145
|
+
python -m pendify
|
|
146
|
+
python -m pendify ping <kind>
|
|
147
|
+
|
|
148
|
+
The first loads the config, starts the page and opens it in the default browser, paired or not.
|
|
149
|
+
It also watches the game client on this PC: when a match is found it accepts after a short random delay, beeps, and sends the found-match alert; when the loading screen starts it beeps, and when the match itself starts it beeps and sends the started alert.
|
|
150
|
+
Add `--dry` to watch and alert without accepting. Add `--quiet` for the start by the system at logon, the line the start with Windows switch writes (`pythonw -m pendify --quiet`): it never opens the browser, a start that ends well shows no window, and a second quiet start prints `already running` and exits. One copy runs per config folder: a second start opens the page of the one that runs and exits. Ctrl+C stops the page and the watcher together.
|
|
151
|
+
The second sends one alert with the stored pair and prints one line with the answer.
|
|
152
|
+
|
|
153
|
+
Both commands take `--data-dir <dir>`, used instead of `%APPDATA%` (the config file lives in a folder under it), and `--worker <address>`, used instead of the pairing service's address and accepted only as `http://127.0.0.1:<port>` or `http://localhost:<port>`, so a test run never reaches the real service. `--client-lockfile <file>` replaces the game client's lockfile, with no read of the running processes, and the client it names is reached over plain http on 127.0.0.1, so a test run never reaches a real client.
|
|
154
|
+
|
|
155
|
+
python -m unittest discover -s tests -v
|
pendify-0.1.0/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# PendiFy
|
|
2
|
+
|
|
3
|
+
## Español
|
|
4
|
+
|
|
5
|
+
Un programa pequeño para Windows que avisa en tu teléfono cuando se encuentra tu partida y cuando empieza.
|
|
6
|
+
Se enlaza una sola vez con tu cuenta de Pendi leyendo un código QR, sin crear ninguna cuenta nueva.
|
|
7
|
+
Es gratis, no pide permisos de administrador y solo usa Python.
|
|
8
|
+
|
|
9
|
+
### Instalar
|
|
10
|
+
|
|
11
|
+
Pulsa Windows + R, pega esta línea completa y pulsa Enter. También sirve en PowerShell o en el Símbolo del sistema.
|
|
12
|
+
|
|
13
|
+
powershell -NoExit -NoProfile -ExecutionPolicy Bypass -Command "ri ~\pendify-install.ps1 -ea 0; irm https://raw.githubusercontent.com/ElkinDev/PendiFy/main/install.ps1 -OutFile ~\pendify-install.ps1; ~\pendify-install.ps1"
|
|
14
|
+
|
|
15
|
+
La línea guarda el instalador como `pendify-install.ps1` en tu carpeta de usuario y lo ejecuta desde ahí; puedes borrar ese archivo cuando termine.
|
|
16
|
+
|
|
17
|
+
La ventana queda abierta al terminar para que leas el resultado; ciérrala cuando acabe.
|
|
18
|
+
|
|
19
|
+
Si Windows dice que no puede acceder al archivo o el antivirus detiene la línea, no se instaló nada: actualiza las definiciones del antivirus y vuelve a intentarlo, o descarga `install.ps1` desde la página del repositorio, haz clic en él con el botón derecho y elige «Ejecutar con PowerShell».
|
|
20
|
+
|
|
21
|
+
Si ves "'irm' no se reconoce como un comando interno o externo", pegaste solo la parte corta en el Símbolo del sistema: usa la línea completa de arriba.
|
|
22
|
+
|
|
23
|
+
El instalador busca Python 3.10 o más nuevo; si no lo hay lo instala con winget solo para tu usuario, o te indica https://www.python.org/downloads/ cuando winget no existe. Después instala PendiFy con pip, deja un acceso directo `PendiFy` en el Escritorio y en el menú Inicio, y lo inicia.
|
|
24
|
+
|
|
25
|
+
En PowerShell: para ver qué haría sin cambiar nada: `$env:PENDIFY_DRYRUN = "1"` antes de pegar la línea. Para no iniciarlo al final: `$env:PENDIFY_NOSTART = "1"`.
|
|
26
|
+
|
|
27
|
+
Otra forma, sin ejecutar ningún script: si el PC ya tiene Python 3.10 o más nuevo, o si en él no se permite ejecutar scripts, instálalo con pip e inícialo:
|
|
28
|
+
|
|
29
|
+
python -m pip install --upgrade pendify
|
|
30
|
+
python -m pendify
|
|
31
|
+
|
|
32
|
+
Si Windows responde que no encuentra `python`, usa `py` en su lugar: `py -m pip install --upgrade pendify` y `py -m pendify`.
|
|
33
|
+
|
|
34
|
+
Así no se crean los accesos directos en el Escritorio ni en el menú Inicio, y el inicio con Windows queda en el interruptor de la página. Para actualizar, pulsa «Salir» en la página, ejecuta otra vez la misma línea de pip y vuelve a iniciarlo. Para quitarlo, apaga primero el interruptor «Iniciar con Windows» en la página, pulsa «Salir» y después ejecuta `python -m pip uninstall pendify`. La configuración en `%APPDATA%\pendify`, con el enlace con el teléfono, se queda; puedes borrar esa carpeta a mano.
|
|
35
|
+
|
|
36
|
+
### Enlazar
|
|
37
|
+
|
|
38
|
+
Al iniciarse, el navegador abre una página local con un código QR. Escanéalo con la cámara del teléfono: se abre la app Pendi y te pide confirmar el enlace. La página muestra cuando el enlace quedó hecho.
|
|
39
|
+
|
|
40
|
+
El código QR y la clave se muestran durante un minuto al pulsar «Mostrar el código» y luego se ocultan de nuevo.
|
|
41
|
+
|
|
42
|
+
El botón de arriba a la derecha cambia entre el tema claro y el oscuro, y el programa guarda la elección para la próxima vez.
|
|
43
|
+
|
|
44
|
+
### Iniciar, detener, desinstalar
|
|
45
|
+
|
|
46
|
+
- Iniciar: el acceso directo `PendiFy`, o `pythonw -m pendify`, que es lo que ejecuta el acceso directo; sin consola, si ya está abierto o no puede iniciarse te lo dice en una ventana. Para verlo en una consola: `python -m pendify`.
|
|
47
|
+
- Detener: pulsa «Salir» en la página del programa; si no la tienes abierta, iniciarlo otra vez la abre. Como último recurso, cierra el proceso `pythonw.exe` en el Administrador de tareas.
|
|
48
|
+
- Pausar: «Pausar avisos», en la tarjeta «Este PC» de la página, deja de leer el cliente del juego: no acepta partidas ni avisa a tu teléfono, y la página sigue abierta. «Reanudar avisos» vuelve a leerlo. La pausa no se guarda: cada vez que el programa se inicia, empieza activo.
|
|
49
|
+
- Iniciar con Windows: el interruptor «Iniciar con Windows», en la misma tarjeta, está apagado hasta que lo enciendas. Encendido, el programa empieza solo al iniciar sesión en Windows, solo para tu usuario y sin abrir el navegador ni la página.
|
|
50
|
+
- Actividad: la tarjeta «Actividad» de la página lista lo que el programa hizo desde que empezó: el cliente del juego encontrado y perdido, cada fase, la partida aceptada y el aviso enviado. Guarda las últimas 50 líneas, solo en memoria.
|
|
51
|
+
- Idioma: la página sigue el idioma del navegador hasta que eliges ES o EN con el selector de arriba, y la elección se guarda para este PC.
|
|
52
|
+
- Desinstalar: pega `powershell -NoExit -NoProfile -ExecutionPolicy Bypass -Command "ri ~\pendify-uninstall.ps1 -ea 0; irm https://raw.githubusercontent.com/ElkinDev/PendiFy/main/uninstall.ps1 -OutFile ~\pendify-uninstall.ps1; ~\pendify-uninstall.ps1"` igual que la línea de instalación; deja `pendify-uninstall.ps1` en tu carpeta de usuario, que puedes borrar. Quita el paquete del mismo Python que lo instaló, comprueba que ya no está, quita los dos accesos directos y quita el inicio con Windows si estaba encendido; la configuración en `%APPDATA%\pendify` se queda y te dice dónde está.
|
|
53
|
+
- Actualizar: pulsa «Salir» en la página y vuelve a pegar la línea de instalación; instala la última versión y conserva la configuración y el enlace.
|
|
54
|
+
|
|
55
|
+
### Qué envía y a quién
|
|
56
|
+
|
|
57
|
+
Solo envía, al servicio de enlace de Pendi, el tipo de aviso (partida encontrada o partida empezada) junto con el identificador del enlace y el secreto de este PC, que viven en `%APPDATA%\pendify\config.json`. El servicio lo entrega a la cuenta que enlazaste. No envía tu nombre, tus partidas ni nada más del juego. Para saber cuándo empieza la partida lee el reloj del juego en este mismo PC, y no lo guarda ni lo envía.
|
|
58
|
+
|
|
59
|
+
### `--dry`
|
|
60
|
+
|
|
61
|
+
Con `--dry` el programa solo avisa y no acepta la partida por ti.
|
|
62
|
+
|
|
63
|
+
Licencia: MIT
|
|
64
|
+
|
|
65
|
+
## English
|
|
66
|
+
|
|
67
|
+
A small Windows program that alerts your phone when your match is found and when it starts.
|
|
68
|
+
It links once to your Pendi account by scanning a QR code, with no new account of any kind.
|
|
69
|
+
It is free, needs no administrator rights and only uses Python.
|
|
70
|
+
|
|
71
|
+
### Install
|
|
72
|
+
|
|
73
|
+
Press Windows + R, paste this whole line and press Enter. It also works in PowerShell or the Command Prompt.
|
|
74
|
+
|
|
75
|
+
powershell -NoExit -NoProfile -ExecutionPolicy Bypass -Command "ri ~\pendify-install.ps1 -ea 0; irm https://raw.githubusercontent.com/ElkinDev/PendiFy/main/install.ps1 -OutFile ~\pendify-install.ps1; ~\pendify-install.ps1"
|
|
76
|
+
|
|
77
|
+
The line saves the installer as `pendify-install.ps1` in your user folder and runs it from there; you can delete that file when it is done.
|
|
78
|
+
|
|
79
|
+
The window stays open at the end so you can read the result; close it when it is done.
|
|
80
|
+
|
|
81
|
+
If Windows says it cannot access the file or the antivirus stops the line, nothing was installed: update the antivirus definitions and try again, or download `install.ps1` from the repository page, click it with the right button and choose "Run with PowerShell".
|
|
82
|
+
|
|
83
|
+
If you see "'irm' is not recognized as an internal or external command", you pasted only the short part into the Command Prompt: use the whole line above.
|
|
84
|
+
|
|
85
|
+
The installer looks for Python 3.10 or newer; when there is none it installs it with winget for your user only, or points you to https://www.python.org/downloads/ when winget is missing. Then it installs PendiFy with pip, leaves a `PendiFy` shortcut on the Desktop and in the Start menu, and starts it.
|
|
86
|
+
|
|
87
|
+
In PowerShell: to see what it would do without changing anything, set `$env:PENDIFY_DRYRUN = "1"` before pasting the line. To leave it stopped at the end, set `$env:PENDIFY_NOSTART = "1"`.
|
|
88
|
+
|
|
89
|
+
Another way, with no script at all: on a PC that already has Python 3.10 or newer, or where scripts are not allowed, install it with pip and start it:
|
|
90
|
+
|
|
91
|
+
python -m pip install --upgrade pendify
|
|
92
|
+
python -m pendify
|
|
93
|
+
|
|
94
|
+
If Windows answers that `python` was not found, use `py` in its place: `py -m pip install --upgrade pendify` and `py -m pendify`.
|
|
95
|
+
|
|
96
|
+
This way leaves no shortcut on the Desktop or in the Start menu, and the start with Windows is the switch on the page. To update, press «Quit» on the page, run the same pip line again and start it again. To remove it, first turn off the «Start with Windows» switch on the page, press «Quit», then run `python -m pip uninstall pendify`. The configuration in `%APPDATA%\pendify`, with the link to the phone, stays; you can delete that folder by hand.
|
|
97
|
+
|
|
98
|
+
### Link
|
|
99
|
+
|
|
100
|
+
When it starts, the browser opens a local page with a QR code. Scan it with the phone's camera: the Pendi app opens and asks you to confirm the link. The page shows when the link is done.
|
|
101
|
+
|
|
102
|
+
The QR code and the key show for a minute when «Show the code» is pressed, and then hide again.
|
|
103
|
+
|
|
104
|
+
The button at the top right switches between the light and the dark theme, and the program keeps the choice for the next time.
|
|
105
|
+
|
|
106
|
+
### Start, stop, uninstall
|
|
107
|
+
|
|
108
|
+
- Start: the `PendiFy` shortcut, or `pythonw -m pendify`, which is what the shortcut runs; with no console, it tells you in a window when it is already running or cannot start. To see it in a console: `python -m pendify`.
|
|
109
|
+
- Stop: press «Quit» on the program's page; if it is not open, starting the program again opens it. As a last resort, end the `pythonw.exe` process in Task Manager.
|
|
110
|
+
- Pause: «Pause alerts», on the page's «This PC» card, stops reading the game client: it accepts no match and sends no alert to your phone, and the page stays open. «Resume alerts» reads it again. The pause is not kept: every start of the program is active again.
|
|
111
|
+
- Start with Windows: the «Start with Windows» switch, on the same card, is off until you turn it on. When it is on, the program starts by itself when you sign in to Windows, for your user only, without opening the browser or the page.
|
|
112
|
+
- Activity: the page's «Activity» card lists what the program did since it started: the game client found and lost, each phase, the match accepted and the alert sent. It keeps the last 50 lines, in memory only.
|
|
113
|
+
- Language: the page follows the browser's language until you choose ES or EN with the switch at the top, and the choice is kept for this PC.
|
|
114
|
+
- Uninstall: paste `powershell -NoExit -NoProfile -ExecutionPolicy Bypass -Command "ri ~\pendify-uninstall.ps1 -ea 0; irm https://raw.githubusercontent.com/ElkinDev/PendiFy/main/uninstall.ps1 -OutFile ~\pendify-uninstall.ps1; ~\pendify-uninstall.ps1"` the same way as the install line; it leaves `pendify-uninstall.ps1` in your user folder, which you can delete. It removes the package from the same Python that installed it, checks that it is gone, removes both shortcuts and removes the start with Windows when it is on; the configuration in `%APPDATA%\pendify` stays, and it tells you where it is.
|
|
115
|
+
- Update: press "Quit" on the page and paste the install line again; it installs the latest version and keeps the configuration and the link.
|
|
116
|
+
|
|
117
|
+
### What it sends and to whom
|
|
118
|
+
|
|
119
|
+
It only sends, to Pendi's link service, the alert kind (match found or match started) together with the link id and this PC's secret, which live in `%APPDATA%\pendify\config.json`. The service delivers it to the account you linked. It sends no name, no match history and nothing else from the game. To tell when the match starts it reads the game's clock on this same PC, and neither keeps nor sends it.
|
|
120
|
+
|
|
121
|
+
### `--dry`
|
|
122
|
+
|
|
123
|
+
With `--dry` the program only alerts and never accepts the match for you.
|
|
124
|
+
|
|
125
|
+
License: MIT
|
|
126
|
+
|
|
127
|
+
## Developer commands
|
|
128
|
+
|
|
129
|
+
python -m pendify
|
|
130
|
+
python -m pendify ping <kind>
|
|
131
|
+
|
|
132
|
+
The first loads the config, starts the page and opens it in the default browser, paired or not.
|
|
133
|
+
It also watches the game client on this PC: when a match is found it accepts after a short random delay, beeps, and sends the found-match alert; when the loading screen starts it beeps, and when the match itself starts it beeps and sends the started alert.
|
|
134
|
+
Add `--dry` to watch and alert without accepting. Add `--quiet` for the start by the system at logon, the line the start with Windows switch writes (`pythonw -m pendify --quiet`): it never opens the browser, a start that ends well shows no window, and a second quiet start prints `already running` and exits. One copy runs per config folder: a second start opens the page of the one that runs and exits. Ctrl+C stops the page and the watcher together.
|
|
135
|
+
The second sends one alert with the stored pair and prints one line with the answer.
|
|
136
|
+
|
|
137
|
+
Both commands take `--data-dir <dir>`, used instead of `%APPDATA%` (the config file lives in a folder under it), and `--worker <address>`, used instead of the pairing service's address and accepted only as `http://127.0.0.1:<port>` or `http://localhost:<port>`, so a test run never reaches the real service. `--client-lockfile <file>` replaces the game client's lockfile, with no read of the running processes, and the client it names is reached over plain http on 127.0.0.1, so a test run never reaches a real client.
|
|
138
|
+
|
|
139
|
+
python -m unittest discover -s tests -v
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pendify"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pairs this PC with a phone account by a QR on a loopback page and sends it two kinds of alerts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = []
|
|
14
|
+
keywords = ["notifications", "alerts", "phone", "qr", "pairing", "windows"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Operating System :: Microsoft :: Windows",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/ElkinDev/PendiFy"
|
|
23
|
+
Source = "https://github.com/ElkinDev/PendiFy"
|
|
24
|
+
|
|
25
|
+
# pip lays the icon beside the modules; install.ps1 points the shortcuts at it.
|
|
26
|
+
[tool.setuptools.package-data]
|
|
27
|
+
pendify = ["*.ico"]
|
pendify-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pairs this PC with a phone account by a QR on a loopback page, standard library only."""
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""The commands.
|
|
2
|
+
|
|
3
|
+
python -m <package> the pairing page and the watcher of the game client, together
|
|
4
|
+
python -m <package> --dry the same, but the watcher never accepts: it alerts at the queue pop
|
|
5
|
+
python -m <package> --quiet the same, started by the system at logon: no browser, and no message box
|
|
6
|
+
unless the start fails
|
|
7
|
+
python -m <package> ping <kind> one alert with the stored pair, one fixed line per answer
|
|
8
|
+
|
|
9
|
+
The page and the watcher stop together on Ctrl+C, on Ctrl+Break and on the page's quit button, which
|
|
10
|
+
removes the run file and exits 0. One instance per config folder: a second start opens the running page
|
|
11
|
+
and exits 0, and one made while that program is still closing after its quit waits for it to end and starts.
|
|
12
|
+
Where no console is attached (a pythonw start), the line a start ends on is also shown in a
|
|
13
|
+
message box. `--data-dir`, `--worker` and `--client-lockfile` are the
|
|
14
|
+
test overrides, loopback only, so a test run reads neither the profile, the Worker nor the client's files.
|
|
15
|
+
"""
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import signal
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import webbrowser
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from . import alert, client, config, page, pairing, runfile, watcher, worker
|
|
26
|
+
from .autostart import Autostart
|
|
27
|
+
|
|
28
|
+
TICK_SECONDS = 1.0
|
|
29
|
+
STOP_SECONDS = 5.0
|
|
30
|
+
# How long a start waits for a copy that is closing to let its run file go: one tick and the two STOP_SECONDS waits
|
|
31
|
+
# of its stop, with room.
|
|
32
|
+
CLOSING_WAIT_SECONDS = 15.0
|
|
33
|
+
REFUSED_LINE = "refused: the pairing of this PC was not accepted; link it again from the page"
|
|
34
|
+
NOT_LINKED_LINE = "not linked: start the program without arguments and link this PC first"
|
|
35
|
+
ALREADY_RUNNING_LINE = "already running: opening the page of the program that runs"
|
|
36
|
+
# A quiet second start opens nothing, so its line promises nothing.
|
|
37
|
+
QUIET_RUNNING_LINE = "already running"
|
|
38
|
+
PAGE_NOT_KNOWN_LINE = ("already running: the page of the program that runs is not known yet; start it again in a "
|
|
39
|
+
"moment to open it")
|
|
40
|
+
CLAIM_FAILED_LINE = "cannot start: the run file cannot be replaced: {path}"
|
|
41
|
+
FOLDER_FAILED_LINE = "cannot start: the config folder cannot be written: {path}"
|
|
42
|
+
_MB_ICONINFORMATION = 0x40
|
|
43
|
+
_MB_SETFOREGROUND = 0x10000
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _data_dir(value):
|
|
47
|
+
if not value.strip():
|
|
48
|
+
raise argparse.ArgumentTypeError("the data directory is empty")
|
|
49
|
+
return Path(value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _client_lockfile(value):
|
|
53
|
+
if not value.strip():
|
|
54
|
+
raise argparse.ArgumentTypeError("the lockfile path is empty")
|
|
55
|
+
return Path(value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _worker_address(value):
|
|
59
|
+
try:
|
|
60
|
+
return worker.loopback_base(value)
|
|
61
|
+
except ValueError as refused:
|
|
62
|
+
raise argparse.ArgumentTypeError(str(refused)) from None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _arguments(argv):
|
|
66
|
+
# The options are accepted before and after the command; SUPPRESS keeps one side from erasing the other.
|
|
67
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
68
|
+
common.add_argument("--data-dir", type=_data_dir, default=argparse.SUPPRESS,
|
|
69
|
+
help="a directory used instead of %%APPDATA%%, for test runs")
|
|
70
|
+
common.add_argument("--worker", type=_worker_address, default=argparse.SUPPRESS,
|
|
71
|
+
help="http://127.0.0.1:<port> or http://localhost:<port> instead of the Worker, for test runs")
|
|
72
|
+
common.add_argument("--client-lockfile", type=_client_lockfile, default=argparse.SUPPRESS,
|
|
73
|
+
help="a lockfile read instead of the client's, its client reached over plain http on "
|
|
74
|
+
"127.0.0.1, for test runs")
|
|
75
|
+
parser = argparse.ArgumentParser(prog=f"python -m {__package__}", parents=[common],
|
|
76
|
+
description="Pairs this PC by a QR on a loopback page, watches the game "
|
|
77
|
+
"client and sends alerts.")
|
|
78
|
+
parser.add_argument("--dry", action="store_true", help="watch and alert, but never accept")
|
|
79
|
+
parser.add_argument("--quiet", action="store_true",
|
|
80
|
+
help="a start by the system: no browser, and no message box for a start that ends well")
|
|
81
|
+
commands = parser.add_subparsers(dest="command")
|
|
82
|
+
ping = commands.add_parser("ping", parents=[common], help="send one alert with the stored pair")
|
|
83
|
+
ping.add_argument("kind", choices=worker.KINDS)
|
|
84
|
+
return parser.parse_args(argv)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ping_line(result):
|
|
88
|
+
if isinstance(result, worker.Sent):
|
|
89
|
+
return "sent"
|
|
90
|
+
if isinstance(result, worker.Refused):
|
|
91
|
+
return REFUSED_LINE
|
|
92
|
+
if isinstance(result, worker.NotDelivered):
|
|
93
|
+
return f"not delivered (HTTP {result.status})"
|
|
94
|
+
return f"failed: {result.reason}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _ping(store, base, kind, timeout):
|
|
98
|
+
pair = store.read() # never a first load: a ping mints nothing
|
|
99
|
+
if pair is None or pair.link_id is None:
|
|
100
|
+
print(NOT_LINKED_LINE)
|
|
101
|
+
return 2
|
|
102
|
+
result = worker.ping(pair.link_id, pair.secret, kind, base=base, timeout=timeout)
|
|
103
|
+
print(_ping_line(result))
|
|
104
|
+
return 0 if isinstance(result, worker.Sent) else 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _watcher(args, store, state, base, timeout, stop, delay, beep):
|
|
108
|
+
"""The watcher and its alert. The test override reads its one lockfile, with no process read, and
|
|
109
|
+
reaches its client over plain http on 127.0.0.1."""
|
|
110
|
+
lockfile = getattr(args, "client_lockfile", None)
|
|
111
|
+
if lockfile is None:
|
|
112
|
+
credentials, addresses = client.ClientCredentials(), client.real_addresses
|
|
113
|
+
else:
|
|
114
|
+
credentials, addresses = client.ClientCredentials((str(lockfile),), run=None), client.loopback_addresses
|
|
115
|
+
alerter = alert.Alerter(store, state, lambda link_id, secret, kind: worker.ping(
|
|
116
|
+
link_id, secret, kind, base=base, timeout=timeout), beep=beep)
|
|
117
|
+
return watcher.Watcher(credentials, alerter, accept=not args.dry, addresses=addresses, delay=delay,
|
|
118
|
+
stop=stop), alerter
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _console_attached():
|
|
122
|
+
"""False only on Windows with no terminal on stdout and no console window: a pythonw start, where a
|
|
123
|
+
printed line reaches nobody."""
|
|
124
|
+
if os.name != "nt":
|
|
125
|
+
return True
|
|
126
|
+
if sys.stdout is not None and sys.stdout.isatty():
|
|
127
|
+
return True
|
|
128
|
+
import ctypes
|
|
129
|
+
from ctypes import wintypes
|
|
130
|
+
|
|
131
|
+
kernel32 = ctypes.WinDLL("kernel32")
|
|
132
|
+
kernel32.GetConsoleWindow.restype = wintypes.HWND
|
|
133
|
+
return bool(kernel32.GetConsoleWindow())
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _message_box(line):
|
|
137
|
+
"""The line in a Windows message box, the one place a start with no console can show it."""
|
|
138
|
+
import ctypes
|
|
139
|
+
from ctypes import wintypes
|
|
140
|
+
|
|
141
|
+
user32 = ctypes.WinDLL("user32")
|
|
142
|
+
user32.MessageBoxW.argtypes = (wintypes.HWND, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.UINT)
|
|
143
|
+
user32.MessageBoxW(None, line, __package__, _MB_ICONINFORMATION | _MB_SETFOREGROUND)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _ends(line, code, show):
|
|
147
|
+
"""The line a start ends on: printed as ever, and shown where no console is attached."""
|
|
148
|
+
print(line, flush=True)
|
|
149
|
+
show(line)
|
|
150
|
+
return code
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, sleep, autostart):
|
|
154
|
+
# A quiet start is the system's, at logon: it never opens the browser, and a start that ends well shows no box.
|
|
155
|
+
calm = (lambda line: None) if args.quiet else show
|
|
156
|
+
run = runfile.RunFile(store.path.parent, clock=clock, sleep=sleep)
|
|
157
|
+
try:
|
|
158
|
+
holder = run.claim()
|
|
159
|
+
if holder is not None and holder["closing"] and run.wait_released(holder, CLOSING_WAIT_SECONDS):
|
|
160
|
+
holder = run.claim() # the closing copy let its file go: this start claims it as a first one does
|
|
161
|
+
except runfile.FolderNotWritable as refused:
|
|
162
|
+
return _ends(FOLDER_FAILED_LINE.format(path=refused.filename), 1, show)
|
|
163
|
+
except OSError:
|
|
164
|
+
return _ends(CLAIM_FAILED_LINE.format(path=run.path), 1, show)
|
|
165
|
+
if holder is not None:
|
|
166
|
+
if holder["closing"]: # still closing at the bound: its page is going, so it is never opened
|
|
167
|
+
return _ends(QUIET_RUNNING_LINE if args.quiet else PAGE_NOT_KNOWN_LINE, 0, calm)
|
|
168
|
+
port = holder["port"] if holder["port"] is not None else run.holder_port(holder)
|
|
169
|
+
if port is None: # a winner with no page yet: nothing is opened; a quiet start promises no page either
|
|
170
|
+
return _ends(QUIET_RUNNING_LINE if args.quiet else PAGE_NOT_KNOWN_LINE, 0, calm)
|
|
171
|
+
if args.quiet: # the system's start at logon: nothing opened, nothing shown, a line of its own
|
|
172
|
+
print(QUIET_RUNNING_LINE, flush=True)
|
|
173
|
+
return 0
|
|
174
|
+
print(ALREADY_RUNNING_LINE, flush=True)
|
|
175
|
+
opener(f"http://{page.ADDRESS}:{port}/")
|
|
176
|
+
show(ALREADY_RUNNING_LINE)
|
|
177
|
+
return 0
|
|
178
|
+
try:
|
|
179
|
+
state = pairing.PairingState(store, lambda secret: worker.check(secret, base=base, timeout=timeout))
|
|
180
|
+
watch, alerter = _watcher(args, store, state, base, timeout, stop, delay, beep)
|
|
181
|
+
|
|
182
|
+
def quit_page():
|
|
183
|
+
# The page's quit marks the record closing at once, since a tick can be held in a slow check before
|
|
184
|
+
# the finally runs, then sets the same stop event as Ctrl+C: the watcher, the page and the run file
|
|
185
|
+
# end below.
|
|
186
|
+
try:
|
|
187
|
+
run.mark_closing()
|
|
188
|
+
except OSError:
|
|
189
|
+
pass # unmarked, the finally marks it again; the stop goes on
|
|
190
|
+
stop.set()
|
|
191
|
+
|
|
192
|
+
pairing_page = page.PairingPage(state, watch=watch.snapshot, on_quit=quit_page, on_pause=watch.pause,
|
|
193
|
+
on_resume=watch.resume, autostart=autostart, events=watch.events)
|
|
194
|
+
url = pairing_page.start()
|
|
195
|
+
watching = threading.Thread(target=watch.run, daemon=True)
|
|
196
|
+
try:
|
|
197
|
+
run.publish(pairing_page.port)
|
|
198
|
+
print(f"page: {url}", flush=True)
|
|
199
|
+
if not args.quiet and not opener(url): # a start by a person opens the page, linked or not
|
|
200
|
+
print("open the address above in a browser", flush=True)
|
|
201
|
+
watching.start()
|
|
202
|
+
while True:
|
|
203
|
+
state.tick()
|
|
204
|
+
if stop.wait(TICK_SECONDS):
|
|
205
|
+
break
|
|
206
|
+
except KeyboardInterrupt:
|
|
207
|
+
pass
|
|
208
|
+
finally:
|
|
209
|
+
stop.set()
|
|
210
|
+
# First, so a start made while this one stops waits for it instead of opening a closing page: Ctrl+C
|
|
211
|
+
# and Ctrl+Break are marked only here, and the page's quit, marked already, is marked again.
|
|
212
|
+
try:
|
|
213
|
+
run.mark_closing()
|
|
214
|
+
except OSError:
|
|
215
|
+
pass # unmarked, a start meanwhile opens this page as before; the stop goes on
|
|
216
|
+
if watching.is_alive():
|
|
217
|
+
watching.join(STOP_SECONDS)
|
|
218
|
+
alerter.flush(STOP_SECONDS)
|
|
219
|
+
pairing_page.close()
|
|
220
|
+
finally:
|
|
221
|
+
run.release()
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _break_as_interrupt():
|
|
226
|
+
"""Ctrl+Break stops the program as Ctrl+C does; only on Windows and only on the main thread."""
|
|
227
|
+
if not hasattr(signal, "SIGBREAK") or threading.current_thread() is not threading.main_thread():
|
|
228
|
+
return None
|
|
229
|
+
return signal.signal(signal.SIGBREAK, signal.default_int_handler)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def main(argv=None, *, opener=webbrowser.open, stop=None, timeout=worker.TIMEOUT_SECONDS, delay=None,
|
|
233
|
+
beep=alert.beep, box=_message_box, autostart=None, console=_console_attached, clock=time.monotonic,
|
|
234
|
+
sleep=time.sleep):
|
|
235
|
+
"""`beep`, `box` and `autostart` are the seams of the sound, of the message box and of the start with Windows
|
|
236
|
+
(the per-user Run value when None): a test run passes silent ones and a fake. `console` says whether a printed
|
|
237
|
+
line reaches anybody; `clock` and `sleep` time the run file's re-reads."""
|
|
238
|
+
args = _arguments(sys.argv[1:] if argv is None else argv)
|
|
239
|
+
store = config.ConfigStore(getattr(args, "data_dir", None) or config.default_base_dir())
|
|
240
|
+
base = getattr(args, "worker", worker.BASE_URL)
|
|
241
|
+
|
|
242
|
+
def show(line): # a start's last line, also in a message box where no console is attached
|
|
243
|
+
if not console():
|
|
244
|
+
box(line)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
if args.command == "ping":
|
|
248
|
+
return _ping(store, base, args.kind, timeout)
|
|
249
|
+
previous = _break_as_interrupt()
|
|
250
|
+
try:
|
|
251
|
+
return _serve(args, store, base, timeout, opener, stop or threading.Event(),
|
|
252
|
+
delay or watcher.accept_delay, beep, show, clock, sleep,
|
|
253
|
+
autostart if autostart is not None else Autostart())
|
|
254
|
+
finally:
|
|
255
|
+
if previous is not None:
|
|
256
|
+
signal.signal(signal.SIGBREAK, previous)
|
|
257
|
+
except config.ConfigError as failure: # at start or mid-run: its one sentence, never a traceback
|
|
258
|
+
print(failure)
|
|
259
|
+
if args.command != "ping": # a start ends on it
|
|
260
|
+
show(str(failure))
|
|
261
|
+
return 1
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
sys.exit(main())
|