pendify 0.1.4__tar.gz → 0.1.6__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.4/src/pendify.egg-info → pendify-0.1.6}/PKG-INFO +9 -5
- {pendify-0.1.4 → pendify-0.1.6}/README.md +8 -4
- {pendify-0.1.4 → pendify-0.1.6}/pyproject.toml +1 -1
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/__main__.py +147 -7
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/config.py +18 -6
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/page.py +179 -25
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/qr.py +153 -10
- pendify-0.1.6/src/pendify/runfile.py +501 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/tray.py +13 -7
- pendify-0.1.6/src/pendify/update.py +252 -0
- {pendify-0.1.4 → pendify-0.1.6/src/pendify.egg-info}/PKG-INFO +9 -5
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify.egg-info/SOURCES.txt +1 -0
- pendify-0.1.4/src/pendify/runfile.py +0 -246
- {pendify-0.1.4 → pendify-0.1.6}/LICENSE +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/MANIFEST.in +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/setup.cfg +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/__init__.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/alert.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/autostart.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/client.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/codes.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/icon.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/pairing.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/pendify.ico +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/plate_almena.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/watcher.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify/worker.py +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify.egg-info/dependency_links.txt +0 -0
- {pendify-0.1.4 → pendify-0.1.6}/src/pendify.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pendify
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Pairs this PC with a phone account by a QR on a loopback page and sends it two kinds of alerts.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/ElkinDev/PendiFy
|
|
@@ -28,7 +28,7 @@ Pulsa Windows + R, pega esta línea completa y pulsa Enter. También sirve en Po
|
|
|
28
28
|
|
|
29
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
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.
|
|
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. Si PendiFy ya está abierto, la línea instala la versión nueva y la copia nueva reemplaza a la anterior por sí sola.
|
|
32
32
|
|
|
33
33
|
La ventana queda abierta al terminar para que leas el resultado; ciérrala cuando acabe.
|
|
34
34
|
|
|
@@ -49,11 +49,13 @@ Si Windows responde que no encuentra `python`, usa `py` en su lugar: `py -m pip
|
|
|
49
49
|
|
|
50
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
51
|
|
|
52
|
+
El programa revisa PyPI cada seis horas y, cuando hay una versión más nueva, la instala en segundo plano. La página ofrece entonces «Reiniciar ahora» para empezar a usarla. Para apagarlo, inícialo con `--no-update` o escribe `"update": "off"` en `%APPDATA%\pendify\config.json`. Las actualizaciones automáticas se instalan para tu usuario (--user).
|
|
53
|
+
|
|
52
54
|
### Enlazar
|
|
53
55
|
|
|
54
56
|
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
57
|
|
|
56
|
-
El código QR y la clave se muestran durante un minuto al pulsar «Mostrar el código
|
|
58
|
+
El código QR y la clave están ocultos; se muestran durante un minuto al pulsar «Mostrar el código», y la página dice para qué sirve el código.
|
|
57
59
|
|
|
58
60
|
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
61
|
|
|
@@ -91,7 +93,7 @@ Press Windows + R, paste this whole line and press Enter. It also works in Power
|
|
|
91
93
|
|
|
92
94
|
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"
|
|
93
95
|
|
|
94
|
-
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.
|
|
96
|
+
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. If PendiFy is already open, the line installs the new version and the new copy replaces the older one by itself.
|
|
95
97
|
|
|
96
98
|
The window stays open at the end so you can read the result; close it when it is done.
|
|
97
99
|
|
|
@@ -112,11 +114,13 @@ If Windows answers that `python` was not found, use `py` in its place: `py -m pi
|
|
|
112
114
|
|
|
113
115
|
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.
|
|
114
116
|
|
|
117
|
+
The program checks PyPI every six hours and, when a newer version exists, installs it in the background. The page then offers «Restart now» to start using it. To turn this off, start it with `--no-update` or write `"update": "off"` in `%APPDATA%\pendify\config.json`. Automatic updates install for your user (--user).
|
|
118
|
+
|
|
115
119
|
### Link
|
|
116
120
|
|
|
117
121
|
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.
|
|
118
122
|
|
|
119
|
-
The QR code and the key show for a minute when «Show the code» is pressed, and
|
|
123
|
+
The QR code and the key are hidden; they show for a minute when «Show the code» is pressed, and the page says what the code is for.
|
|
120
124
|
|
|
121
125
|
The button at the top right switches between the light and the dark theme, and the program keeps the choice for the next time.
|
|
122
126
|
|
|
@@ -12,7 +12,7 @@ Pulsa Windows + R, pega esta línea completa y pulsa Enter. También sirve en Po
|
|
|
12
12
|
|
|
13
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
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.
|
|
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. Si PendiFy ya está abierto, la línea instala la versión nueva y la copia nueva reemplaza a la anterior por sí sola.
|
|
16
16
|
|
|
17
17
|
La ventana queda abierta al terminar para que leas el resultado; ciérrala cuando acabe.
|
|
18
18
|
|
|
@@ -33,11 +33,13 @@ Si Windows responde que no encuentra `python`, usa `py` en su lugar: `py -m pip
|
|
|
33
33
|
|
|
34
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
35
|
|
|
36
|
+
El programa revisa PyPI cada seis horas y, cuando hay una versión más nueva, la instala en segundo plano. La página ofrece entonces «Reiniciar ahora» para empezar a usarla. Para apagarlo, inícialo con `--no-update` o escribe `"update": "off"` en `%APPDATA%\pendify\config.json`. Las actualizaciones automáticas se instalan para tu usuario (--user).
|
|
37
|
+
|
|
36
38
|
### Enlazar
|
|
37
39
|
|
|
38
40
|
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
41
|
|
|
40
|
-
El código QR y la clave se muestran durante un minuto al pulsar «Mostrar el código
|
|
42
|
+
El código QR y la clave están ocultos; se muestran durante un minuto al pulsar «Mostrar el código», y la página dice para qué sirve el código.
|
|
41
43
|
|
|
42
44
|
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
45
|
|
|
@@ -75,7 +77,7 @@ Press Windows + R, paste this whole line and press Enter. It also works in Power
|
|
|
75
77
|
|
|
76
78
|
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"
|
|
77
79
|
|
|
78
|
-
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.
|
|
80
|
+
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. If PendiFy is already open, the line installs the new version and the new copy replaces the older one by itself.
|
|
79
81
|
|
|
80
82
|
The window stays open at the end so you can read the result; close it when it is done.
|
|
81
83
|
|
|
@@ -96,11 +98,13 @@ If Windows answers that `python` was not found, use `py` in its place: `py -m pi
|
|
|
96
98
|
|
|
97
99
|
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.
|
|
98
100
|
|
|
101
|
+
The program checks PyPI every six hours and, when a newer version exists, installs it in the background. The page then offers «Restart now» to start using it. To turn this off, start it with `--no-update` or write `"update": "off"` in `%APPDATA%\pendify\config.json`. Automatic updates install for your user (--user).
|
|
102
|
+
|
|
99
103
|
### Link
|
|
100
104
|
|
|
101
105
|
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.
|
|
102
106
|
|
|
103
|
-
The QR code and the key show for a minute when «Show the code» is pressed, and
|
|
107
|
+
The QR code and the key are hidden; they show for a minute when «Show the code» is pressed, and the page says what the code is for.
|
|
104
108
|
|
|
105
109
|
The button at the top right switches between the light and the dark theme, and the program keeps the choice for the next time.
|
|
106
110
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pendify"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "Pairs this PC with a phone account by a QR on a loopback page and sends it two kinds of alerts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -4,26 +4,36 @@
|
|
|
4
4
|
python -m <package> --dry the same, but the watcher never accepts: it alerts at the queue pop
|
|
5
5
|
python -m <package> --quiet the same, started by the system at logon: no browser, and no message box
|
|
6
6
|
unless the start fails
|
|
7
|
+
python -m <package> --no-update the same, with no check of PyPI for a newer version (update.py)
|
|
7
8
|
python -m <package> ping <kind> one alert with the stored pair, one fixed line per answer
|
|
8
9
|
|
|
9
10
|
The page and the watcher stop together on Ctrl+C, on Ctrl+Break, on the page's quit button and on the quit of
|
|
10
11
|
the icon by the clock, which remove the run file and exit 0. One instance per config folder: a second start
|
|
11
12
|
opens the running page and exits 0, and one made while that program is still closing after its quit waits for
|
|
12
|
-
it to end and starts.
|
|
13
|
+
it to end and starts. A start of a newer version over a running older copy replaces it: the older copy is asked to
|
|
14
|
+
stop through its page's /replace with the run file's secret, or ended by its pid when it has no secret or its page
|
|
15
|
+
does not answer, and this start then claims the run file as a first one does. A restart the page asks for
|
|
16
|
+
(«Reiniciar ahora», once a newer version is installed, or the
|
|
17
|
+
icon's «Reiniciar para actualizar») starts a new copy once the run file is released: without --quiet, so its page
|
|
18
|
+
opens, and with the --dry, --data-dir, --worker and --client-lockfile of this one.
|
|
13
19
|
Where no console is attached (a pythonw start), the line a start ends on is also shown in a
|
|
14
20
|
message box. `--data-dir`, `--worker` and `--client-lockfile` are the
|
|
15
21
|
test overrides, loopback only, so a test run reads neither the profile, the Worker nor the client's files.
|
|
16
22
|
"""
|
|
17
23
|
import argparse
|
|
24
|
+
import http.client
|
|
18
25
|
import os
|
|
19
26
|
import signal
|
|
27
|
+
import subprocess
|
|
20
28
|
import sys
|
|
21
29
|
import threading
|
|
22
30
|
import time
|
|
31
|
+
import urllib.parse
|
|
32
|
+
import urllib.request
|
|
23
33
|
import webbrowser
|
|
24
34
|
from pathlib import Path
|
|
25
35
|
|
|
26
|
-
from . import alert, client, config, page, pairing, runfile, watcher, worker
|
|
36
|
+
from . import alert, client, config, page, pairing, runfile, update, watcher, worker
|
|
27
37
|
from .autostart import Autostart
|
|
28
38
|
from .tray import Tray
|
|
29
39
|
|
|
@@ -41,8 +51,22 @@ PAGE_NOT_KNOWN_LINE = ("already running: the page of the program that runs is no
|
|
|
41
51
|
"moment to open it")
|
|
42
52
|
CLAIM_FAILED_LINE = "cannot start: the run file cannot be replaced: {path}"
|
|
43
53
|
FOLDER_FAILED_LINE = "cannot start: the config folder cannot be written: {path}"
|
|
54
|
+
RESTART_FAILED_LINE = "cannot restart: the new copy did not start; start the program again"
|
|
55
|
+
# The line a start that replaced an older copy shows, in the page's language: the older copy's version, «desconocida»
|
|
56
|
+
# or "unknown" for a record with none, then this one's.
|
|
57
|
+
REPLACED_LINES = {"es": "Se reemplazó la copia anterior ({old}) por esta ({new}).",
|
|
58
|
+
"en": "The older copy ({old}) was replaced by this one ({new})."}
|
|
59
|
+
UNKNOWN_VERSION = {"es": "desconocida", "en": "unknown"}
|
|
60
|
+
# The line a start prints when the older copy it would replace cannot be ended, in the page's language, with the
|
|
61
|
+
# system's reason.
|
|
62
|
+
CLOSE_REFUSED_LINES = {"es": "No se pudo cerrar la copia anterior: {reason}",
|
|
63
|
+
"en": "The older copy could not be closed: {reason}"}
|
|
64
|
+
REPLACE_TIMEOUT_SECONDS = 5.0 # the replace post to the older copy's page
|
|
44
65
|
_MB_ICONINFORMATION = 0x40
|
|
45
66
|
_MB_SETFOREGROUND = 0x10000
|
|
67
|
+
# The restart's new copy: no console, its own process group, out of this one's.
|
|
68
|
+
_DETACHED_PROCESS = 0x00000008
|
|
69
|
+
_CREATE_NEW_PROCESS_GROUP = 0x00000200
|
|
46
70
|
# The icon by the clock's picture: pendify.ico beside the modules, the file install.ps1 points the shortcuts at.
|
|
47
71
|
ICON_PATH = Path(__file__).with_name("pendify.ico")
|
|
48
72
|
|
|
@@ -82,6 +106,8 @@ def _arguments(argv):
|
|
|
82
106
|
parser.add_argument("--dry", action="store_true", help="watch and alert, but never accept")
|
|
83
107
|
parser.add_argument("--quiet", action="store_true",
|
|
84
108
|
help="a start by the system: no browser, and no message box for a start that ends well")
|
|
109
|
+
parser.add_argument("--no-update", action="store_true",
|
|
110
|
+
help="no check of PyPI for a newer version in this run")
|
|
85
111
|
commands = parser.add_subparsers(dest="command")
|
|
86
112
|
ping = commands.add_parser("ping", parents=[common], help="send one alert with the stored pair")
|
|
87
113
|
ping.add_argument("kind", choices=worker.KINDS)
|
|
@@ -154,14 +180,102 @@ def _ends(line, code, show):
|
|
|
154
180
|
return code
|
|
155
181
|
|
|
156
182
|
|
|
157
|
-
def
|
|
183
|
+
def _restart(args, spawn, show, opens_page):
|
|
184
|
+
"""The restart asked for, once the run file is free: a new copy of this program, detached and with no console,
|
|
185
|
+
which claims the run file as a first start does. One that opens its page, as every restart asked today does,
|
|
186
|
+
goes without --quiet; one that does not keeps this run's --quiet. This run's --dry, --data-dir, --worker and
|
|
187
|
+
--client-lockfile go with it either way."""
|
|
188
|
+
command = [sys.executable, "-m", __package__] + (["--quiet"] if args.quiet and not opens_page else [])
|
|
189
|
+
command += ["--dry"] if args.dry else []
|
|
190
|
+
for option in ("data_dir", "worker", "client_lockfile"):
|
|
191
|
+
if hasattr(args, option): # the parser's SUPPRESS: present only when this run was given it
|
|
192
|
+
command += ["--" + option.replace("_", "-"), str(getattr(args, option))]
|
|
193
|
+
try:
|
|
194
|
+
spawn(command, creationflags=_DETACHED_PROCESS | _CREATE_NEW_PROCESS_GROUP, close_fds=True)
|
|
195
|
+
except OSError:
|
|
196
|
+
return _ends(RESTART_FAILED_LINE, 1, show)
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _replaces(holder):
|
|
201
|
+
"""True when this copy is newer than `holder`, a live holder that is not closing: X.Y.Z compared as tuples of
|
|
202
|
+
ints, and a holder with no version, as an older copy writes its record, older than any. A copy whose own version
|
|
203
|
+
is not X.Y.Z replaces nothing."""
|
|
204
|
+
if update._parts(update.RUNNING_VERSION) is None:
|
|
205
|
+
return False
|
|
206
|
+
return holder["version"] is None or update.newer(update.RUNNING_VERSION, holder["version"])
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _ask_replace(holder):
|
|
210
|
+
"""True when the holder's page answered 202 to POST /replace with the run file's secret; False for any other
|
|
211
|
+
answer, none, or no port."""
|
|
212
|
+
if holder["port"] is None:
|
|
213
|
+
return False
|
|
214
|
+
request = urllib.request.Request(f"http://{page.ADDRESS}:{holder['port']}/replace", method="POST",
|
|
215
|
+
data=urllib.parse.urlencode({"secret": holder["secret"]}).encode())
|
|
216
|
+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) # a loopback address never goes through one
|
|
217
|
+
try:
|
|
218
|
+
with opener.open(request, timeout=REPLACE_TIMEOUT_SECONDS) as answer:
|
|
219
|
+
return answer.status == 202
|
|
220
|
+
except (OSError, http.client.HTTPException): # a refusal (urllib.error.HTTPError), no answer in time, a broken one
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _kept_language(store):
|
|
225
|
+
"""The page's language before the page is made, with no browser to ask: the kept choice, else Spanish, as the
|
|
226
|
+
icon's words. A config file that cannot be read gives Spanish: the choice only words one line."""
|
|
227
|
+
try:
|
|
228
|
+
return store.read_lang() or page.language(None)
|
|
229
|
+
except (OSError, ValueError):
|
|
230
|
+
return page.language(None)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _replace(run, holder, terminate, store):
|
|
234
|
+
"""The older `holder` dealt with, then the run file claimed again: (the claim's answer, `holder` when this start
|
|
235
|
+
replaced it, else None). It is asked through its page's /replace; with no secret in its record or no 202 from its
|
|
236
|
+
page, `terminate` is asked to end it by its pid, which it does only for the process its record names
|
|
237
|
+
(runfile.terminate):
|
|
238
|
+
- a 202, or ended: once the run file no longer names it as a live process within CLOSING_WAIT_SECONDS, its file
|
|
239
|
+
gone or left stale by its death, the file is claimed; a holder still alive then is handed back as it was read;
|
|
240
|
+
- not-ours: the pid names another process now, so the file is stale: evicted while it still holds that record,
|
|
241
|
+
then claimed as a first start claims it;
|
|
242
|
+
- refused: the reason printed on one line and the file waited for as long, since the pid may be dying under
|
|
243
|
+
another start's terminate, then claimed again: a holder that still stands is read afresh, never the snapshot."""
|
|
244
|
+
if holder["secret"] is None or not _ask_replace(holder):
|
|
245
|
+
answer = terminate(holder["pid"], holder["started"], run.path)
|
|
246
|
+
if answer == runfile.NOT_OURS:
|
|
247
|
+
run.evict(holder)
|
|
248
|
+
return run.claim(), holder
|
|
249
|
+
if answer == runfile.REFUSED:
|
|
250
|
+
reason = answer.error.strerror or answer.error
|
|
251
|
+
print(CLOSE_REFUSED_LINES[_kept_language(store)].format(reason=reason), flush=True)
|
|
252
|
+
run.wait_released(holder, CLOSING_WAIT_SECONDS)
|
|
253
|
+
return run.claim(), None
|
|
254
|
+
if run.wait_released(holder, CLOSING_WAIT_SECONDS):
|
|
255
|
+
return run.claim(), holder
|
|
256
|
+
return holder, None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _ready_version(updater):
|
|
260
|
+
"""The version an install made ready, for the icon's item; None in any other state."""
|
|
261
|
+
seen = updater.snapshot()
|
|
262
|
+
return seen["version"] if seen["state"] == update.READY else None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, sleep, autostart, tray, updater,
|
|
266
|
+
spawn, terminate):
|
|
158
267
|
# A quiet start is the system's, at logon: it never opens the browser, and a start that ends well shows no box.
|
|
159
268
|
calm = (lambda line: None) if args.quiet else show
|
|
160
269
|
run = runfile.RunFile(store.path.parent, clock=clock, sleep=sleep)
|
|
270
|
+
replaced = None # the record of the older copy this start replaced
|
|
161
271
|
try:
|
|
162
272
|
holder = run.claim()
|
|
163
273
|
if holder is not None and holder["closing"] and run.wait_released(holder, CLOSING_WAIT_SECONDS):
|
|
164
274
|
holder = run.claim() # the closing copy let its file go: this start claims it as a first one does
|
|
275
|
+
# A running older copy is replaced (_replace); a holder that still stands after it keeps the road below,
|
|
276
|
+
# its page opened.
|
|
277
|
+
if holder is not None and not holder["closing"] and _replaces(holder):
|
|
278
|
+
holder, replaced = _replace(run, holder, terminate, store)
|
|
165
279
|
except runfile.FolderNotWritable as refused:
|
|
166
280
|
return _ends(FOLDER_FAILED_LINE.format(path=refused.filename), 1, show)
|
|
167
281
|
except OSError:
|
|
@@ -179,7 +293,9 @@ def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, s
|
|
|
179
293
|
opener(f"http://{page.ADDRESS}:{port}/")
|
|
180
294
|
show(ALREADY_RUNNING_LINE)
|
|
181
295
|
return 0
|
|
296
|
+
updating = None
|
|
182
297
|
try:
|
|
298
|
+
mode = update.OFF if args.no_update else store.read_update()
|
|
183
299
|
state = pairing.PairingState(store, lambda secret: worker.check(secret, base=base, timeout=timeout))
|
|
184
300
|
watch, alerter = _watcher(args, store, state, base, timeout, stop, delay, beep)
|
|
185
301
|
|
|
@@ -195,16 +311,31 @@ def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, s
|
|
|
195
311
|
|
|
196
312
|
pairing_page = page.PairingPage(state, watch=watch.snapshot, on_quit=quit_page, on_pause=watch.pause,
|
|
197
313
|
on_resume=watch.resume, autostart=autostart, events=watch.events)
|
|
314
|
+
pairing_page.run = run # its /replace answers the secret the publish below writes
|
|
198
315
|
url = pairing_page.start()
|
|
316
|
+
# The update's rounds on their own thread (update.py), built once the page serves; its restart is the
|
|
317
|
+
# page's quit, after the flag read below once the run file is free; pip's output goes to the config folder.
|
|
318
|
+
updating = updater(update.RUNNING_VERSION, mode, restart=quit_page, folder=store.path.parent)
|
|
319
|
+
pairing_page.updater = updating
|
|
199
320
|
# The icon by the clock, under --quiet as well: the way to stop a copy the Run key started. Its words are
|
|
200
321
|
# the page's, in the language the page resolves with no browser to ask: the kept choice, else Spanish.
|
|
201
322
|
icon = tray(url, on_open=lambda: opener(url), on_pause=watch.pause, on_resume=watch.resume,
|
|
202
323
|
on_quit=quit_page, paused=lambda: watch.paused,
|
|
203
|
-
words=lambda: page.WORDS[pairing_page.language_of(None)], icon_path=ICON_PATH
|
|
324
|
+
words=lambda: page.WORDS[pairing_page.language_of(None)], icon_path=ICON_PATH,
|
|
325
|
+
# «Reiniciar para actualizar a X» once an install is ready, the page's own restart
|
|
326
|
+
update_ready=lambda: _ready_version(updating), on_restart=updating.restart)
|
|
204
327
|
watching = threading.Thread(target=watch.run, daemon=True)
|
|
205
328
|
try:
|
|
206
329
|
icon.start()
|
|
330
|
+
if mode != update.OFF:
|
|
331
|
+
updating.start()
|
|
207
332
|
run.publish(pairing_page.port)
|
|
333
|
+
if replaced is not None: # in the page's language, as the icon's words; a box never holds the start
|
|
334
|
+
lang = pairing_page.language_of(None)
|
|
335
|
+
line = REPLACED_LINES[lang].format(old=replaced["version"] or UNKNOWN_VERSION[lang],
|
|
336
|
+
new=update.RUNNING_VERSION)
|
|
337
|
+
print(line, flush=True)
|
|
338
|
+
threading.Thread(target=calm, args=(line,), daemon=True).start()
|
|
208
339
|
print(f"page: {url}", flush=True)
|
|
209
340
|
if not args.quiet and not opener(url): # a start by a person opens the page, linked or not
|
|
210
341
|
print("open the address above in a browser", flush=True)
|
|
@@ -217,6 +348,7 @@ def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, s
|
|
|
217
348
|
pass
|
|
218
349
|
finally:
|
|
219
350
|
stop.set()
|
|
351
|
+
updating.close() # the restart gate shuts at the quit
|
|
220
352
|
# First, so a start made while this one stops waits for it instead of opening a closing page: Ctrl+C
|
|
221
353
|
# and Ctrl+Break are marked only here, and the page's quit, marked already, is marked again.
|
|
222
354
|
try:
|
|
@@ -228,8 +360,13 @@ def _serve(args, store, base, timeout, opener, stop, delay, beep, show, clock, s
|
|
|
228
360
|
alerter.flush(STOP_SECONDS)
|
|
229
361
|
pairing_page.close()
|
|
230
362
|
icon.close()
|
|
363
|
+
# An install under way is waited for before the run file goes, so a quit does not end this copy while
|
|
364
|
+
# pip swaps the files; past the bound pip goes on alone, its output in a file.
|
|
365
|
+
updating.wait(update.INSTALL_JOIN_SECONDS)
|
|
231
366
|
finally:
|
|
232
367
|
run.release()
|
|
368
|
+
if updating is not None and updating.restart_requested.is_set():
|
|
369
|
+
return _restart(args, spawn, show, updating.restart_opens_page)
|
|
233
370
|
return 0
|
|
234
371
|
|
|
235
372
|
|
|
@@ -242,11 +379,14 @@ def _break_as_interrupt():
|
|
|
242
379
|
|
|
243
380
|
def main(argv=None, *, opener=webbrowser.open, stop=None, timeout=worker.TIMEOUT_SECONDS, delay=None,
|
|
244
381
|
beep=alert.beep, box=_message_box, autostart=None, console=_console_attached, clock=time.monotonic,
|
|
245
|
-
sleep=time.sleep, tray=Tray):
|
|
382
|
+
sleep=time.sleep, tray=Tray, updater=update.Updater, spawn=subprocess.Popen, terminate=runfile.terminate):
|
|
246
383
|
"""`beep`, `box` and `autostart` are the seams of the sound, of the message box and of the start with Windows
|
|
247
384
|
(the per-user Run value when None): a test run passes silent ones and a fake. `console` says whether a printed
|
|
248
385
|
line reaches anybody; `clock` and `sleep` time the run file's re-reads. `tray` makes the icon by the clock, the
|
|
249
|
-
seam a test run fills with a fake, so no test shows a real icon.
|
|
386
|
+
seam a test run fills with a fake, so no test shows a real icon. `updater` makes the update and `spawn` starts
|
|
387
|
+
the restart's new copy, the seams a test run fills with fakes, so no test reaches PyPI, runs pip or starts a
|
|
388
|
+
copy. `terminate` ends an older copy by its pid when it cannot be asked to stop, the seam every test run fakes, so
|
|
389
|
+
no test ends a process."""
|
|
250
390
|
args = _arguments(sys.argv[1:] if argv is None else argv)
|
|
251
391
|
store = config.ConfigStore(getattr(args, "data_dir", None) or config.default_base_dir())
|
|
252
392
|
base = getattr(args, "worker", worker.BASE_URL)
|
|
@@ -262,7 +402,7 @@ def main(argv=None, *, opener=webbrowser.open, stop=None, timeout=worker.TIMEOUT
|
|
|
262
402
|
try:
|
|
263
403
|
return _serve(args, store, base, timeout, opener, stop or threading.Event(),
|
|
264
404
|
delay or watcher.accept_delay, beep, show, clock, sleep,
|
|
265
|
-
autostart if autostart is not None else Autostart(), tray)
|
|
405
|
+
autostart if autostart is not None else Autostart(), tray, updater, spawn, terminate)
|
|
266
406
|
finally:
|
|
267
407
|
if previous is not None:
|
|
268
408
|
signal.signal(signal.SIGBREAK, previous)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""The config file: the secret and the link id, under the user's profile (design P1, P6), and the page's theme
|
|
2
2
|
choice when there is one (light or dark; the system's choice is no key), and its language choice when there is one
|
|
3
|
-
(es or en; with none the page follows the browser)
|
|
3
|
+
(es or en; with none the page follows the browser), and the update's mode when one is written there by hand
|
|
4
|
+
(auto, notify or off; with none, auto).
|
|
4
5
|
|
|
5
6
|
The file is `<base>/<folder>/config.json`, where the base is %APPDATA% in a real run and is injected
|
|
6
7
|
everywhere else, so no test touches the real profile. A write goes to a temp file in the same folder
|
|
@@ -30,6 +31,10 @@ THEMES = ("light", "dark")
|
|
|
30
31
|
SYSTEM_THEME = "system"
|
|
31
32
|
# The language switch's choices; with no key the page follows the browser's language.
|
|
32
33
|
LANGS = ("es", "en")
|
|
34
|
+
# The update's modes (update.py), written by hand as the key "update": auto checks PyPI and installs in the
|
|
35
|
+
# background, notify checks only, off never checks. With no key, or a foreign value, auto.
|
|
36
|
+
UPDATE_MODES = ("auto", "notify", "off")
|
|
37
|
+
DEFAULT_UPDATE = "auto"
|
|
33
38
|
# _write's default: the stored theme and language choices are kept.
|
|
34
39
|
_KEEP = object()
|
|
35
40
|
|
|
@@ -92,6 +97,11 @@ class ConfigStore:
|
|
|
92
97
|
and cannot be read raises ConfigError, as read_theme() does; a missing, corrupt or foreign value is None."""
|
|
93
98
|
return _choice(self._stored(), "lang", LANGS)
|
|
94
99
|
|
|
100
|
+
def read_update(self):
|
|
101
|
+
"""The update's mode, auto, notify or off; auto when the file holds none or a foreign one. A file that is
|
|
102
|
+
there and cannot be read raises ConfigError, as read_theme() does."""
|
|
103
|
+
return _choice(self._stored(), "update", UPDATE_MODES) or DEFAULT_UPDATE
|
|
104
|
+
|
|
95
105
|
def set_theme(self, choice):
|
|
96
106
|
"""The theme button's choice written beside the pair: light or dark kept, system kept as no key. Any
|
|
97
107
|
other value is refused and nothing is written. Answers the stored choice, None for the system's."""
|
|
@@ -176,17 +186,19 @@ class ConfigStore:
|
|
|
176
186
|
|
|
177
187
|
def _write(self, pairing, theme=_KEEP, lang=_KEEP):
|
|
178
188
|
"""The pair, and the theme and language choices: each stored one kept unless set_theme or set_lang passes
|
|
179
|
-
its own."""
|
|
189
|
+
its own. The update's mode, written by hand, is kept as it is stored."""
|
|
180
190
|
folder = self.path.parent
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
stored = self._stored()
|
|
192
|
+
theme = _choice(stored, "theme", THEMES) if theme is _KEEP else theme
|
|
193
|
+
lang = _choice(stored, "lang", LANGS) if lang is _KEEP else lang
|
|
194
|
+
update = _choice(stored, "update", UPDATE_MODES)
|
|
185
195
|
values = {"secret": pairing.secret, "linkId": pairing.link_id}
|
|
186
196
|
if theme is not None:
|
|
187
197
|
values["theme"] = theme
|
|
188
198
|
if lang is not None:
|
|
189
199
|
values["lang"] = lang
|
|
200
|
+
if update is not None:
|
|
201
|
+
values["update"] = update
|
|
190
202
|
data = json.dumps(values).encode("utf-8")
|
|
191
203
|
try:
|
|
192
204
|
folder.mkdir(parents=True, exist_ok=True)
|