Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccaf521da4 | |||
| dfecd5dafb | |||
| 6d01e5ccc2 | |||
| 8da02e2392 | |||
| dd5f418ee0 |
+41
-18
@@ -135,6 +135,16 @@ jobs:
|
|||||||
|
|
||||||
# Releases go through the Gitea API directly: it is GitHub-compatible in
|
# Releases go through the Gitea API directly: it is GitHub-compatible in
|
||||||
# shape, but a self-hosted instance may not have ready-made actions.
|
# shape, but a self-hosted instance may not have ready-made actions.
|
||||||
|
#
|
||||||
|
# The step is written to be repeatable. Creating a release in the Gitea
|
||||||
|
# web UI also creates the tag, so by the time this runs the release
|
||||||
|
# usually exists already and a blind POST would fail with 409. The same
|
||||||
|
# goes for re-running a build over a tag whose assets are in place.
|
||||||
|
#
|
||||||
|
# Keep it POSIX: the runner executes steps with dash, so no `pipefail`.
|
||||||
|
# curl calls therefore stay out of pipelines — under `set -e` a failed
|
||||||
|
# command substitution aborts the step, which is what pipefail bought us.
|
||||||
|
# No -x anywhere here: these commands carry the token.
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
env:
|
env:
|
||||||
@@ -142,29 +152,42 @@ jobs:
|
|||||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
# Keep this POSIX: the runner executes steps with dash, where
|
|
||||||
# `set -o pipefail` does not exist. So the curl call is kept out of a
|
|
||||||
# pipeline — under `set -e` a failed command substitution aborts the
|
|
||||||
# step, which is what pipefail would have bought us. Without that, a
|
|
||||||
# failing curl would be masked by a successful jq, id would become
|
|
||||||
# null, and assets would be uploaded to a release that never existed.
|
|
||||||
# No -x here: the command carries the token.
|
|
||||||
set -eu
|
set -eu
|
||||||
response=$(curl -fsS -X POST "$API/releases" \
|
auth="Authorization: token $TOKEN"
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\"}")
|
|
||||||
|
|
||||||
id=$(printf '%s' "$response" | jq -r '.id // empty')
|
# Reuse the release if it is already there, create it otherwise.
|
||||||
|
existing=$(curl -sS -o /tmp/release.json -w '%{http_code}' \
|
||||||
|
-H "$auth" "$API/releases/tags/$TAG")
|
||||||
|
|
||||||
|
if [ "$existing" = "200" ]; then
|
||||||
|
echo "release $TAG already exists, attaching assets to it"
|
||||||
|
else
|
||||||
|
echo "creating release $TAG"
|
||||||
|
curl -fsS -o /tmp/release.json -X POST "$API/releases" \
|
||||||
|
-H "$auth" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\"}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
id=$(jq -r '.id // empty' /tmp/release.json)
|
||||||
if [ -z "$id" ]; then
|
if [ -z "$id" ]; then
|
||||||
echo "no release id in response: $response" >&2
|
echo "could not resolve release id for $TAG:" >&2
|
||||||
|
cat /tmp/release.json >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "release $TAG (id $id)"
|
|
||||||
|
|
||||||
for file in dist/*.exe; do
|
for file in dist/*.exe; do
|
||||||
echo "uploading $(basename "$file")"
|
name=$(basename "$file")
|
||||||
curl -fsS -X POST "$API/releases/$id/assets?name=$(basename "$file")" \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
# Replace an asset of the same name rather than ending up with two.
|
||||||
-F "attachment=@$file" > /dev/null
|
curl -fsS -o /tmp/assets.json -H "$auth" "$API/releases/$id/assets"
|
||||||
|
for old in $(jq -r --arg n "$name" '.[] | select(.name==$n) | .id' /tmp/assets.json); do
|
||||||
|
echo "removing previous $name (asset $old)"
|
||||||
|
curl -fsS -X DELETE -H "$auth" "$API/releases/$id/assets/$old" > /dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "uploading $name"
|
||||||
|
curl -fsS -X POST "$API/releases/$id/assets?name=$name" \
|
||||||
|
-H "$auth" -F "attachment=@$file" > /dev/null
|
||||||
done
|
done
|
||||||
|
|
||||||
|
echo "release $TAG (id $id) is ready"
|
||||||
|
|||||||
Generated
+1
-1
@@ -1224,7 +1224,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syshelper"
|
name = "syshelper"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "syshelper"
|
name = "syshelper"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
+59
-3
@@ -185,7 +185,18 @@ begin
|
|||||||
SW_HIDE, ewWaitUntilTerminated, Code);
|
SW_HIDE, ewWaitUntilTerminated, Code);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure InstallService();
|
// Запускаем через net start, а не sc start, по той же причине, что и остановку:
|
||||||
|
// net дожидается фактического запуска и возвращает ошибку, если он не удался.
|
||||||
|
// sc отдаёт управление сразу, и сломанная установка выглядела бы успешной.
|
||||||
|
function StartService(): Boolean;
|
||||||
|
var
|
||||||
|
Code: Integer;
|
||||||
|
begin
|
||||||
|
Result := Exec(ExpandConstant('{sys}\net.exe'), 'start {#ServiceName}', '',
|
||||||
|
SW_HIDE, ewWaitUntilTerminated, Code) and (Code = 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function InstallService(): Boolean;
|
||||||
var
|
var
|
||||||
Bin: String;
|
Bin: String;
|
||||||
begin
|
begin
|
||||||
@@ -204,7 +215,8 @@ begin
|
|||||||
RunSc('description {#ServiceName} "Родительский контроль: следит за запуском программ."');
|
RunSc('description {#ServiceName} "Родительский контроль: следит за запуском программ."');
|
||||||
// Родительский контроль не должен исчезать из-за одной аварии.
|
// Родительский контроль не должен исчезать из-за одной аварии.
|
||||||
RunSc('failure {#ServiceName} reset= 86400 actions= restart/5000/restart/5000/restart/60000');
|
RunSc('failure {#ServiceName} reset= 86400 actions= restart/5000/restart/5000/restart/60000');
|
||||||
RunSc('start {#ServiceName}');
|
|
||||||
|
Result := StartService();
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Каталог данных закрываем от обычных пользователей: унаследованные от
|
// Каталог данных закрываем от обычных пользователей: унаследованные от
|
||||||
@@ -222,7 +234,34 @@ begin
|
|||||||
'', SW_HIDE, ewWaitUntilTerminated, Code);
|
'', SW_HIDE, ewWaitUntilTerminated, Code);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Без правила брандмауэра панель доступна только с самого компьютера: Windows
|
||||||
|
// режет входящие подключения по умолчанию. Ограничиваемся частными и доменными
|
||||||
|
// сетями — в публичной (кафе, вокзал) панель наружу смотреть не должна.
|
||||||
|
procedure RemoveFirewallRule();
|
||||||
|
var
|
||||||
|
Code: Integer;
|
||||||
|
begin
|
||||||
|
Exec(ExpandConstant('{sys}\netsh.exe'),
|
||||||
|
'advfirewall firewall delete rule name="{#AppName}"',
|
||||||
|
'', SW_HIDE, ewWaitUntilTerminated, Code);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure OpenFirewallPort();
|
||||||
|
var
|
||||||
|
Code: Integer;
|
||||||
|
begin
|
||||||
|
// сначала снимаем старое правило, иначе при переустановке они накопятся
|
||||||
|
RemoveFirewallRule();
|
||||||
|
Exec(ExpandConstant('{sys}\netsh.exe'),
|
||||||
|
'advfirewall firewall add rule name="{#AppName}" dir=in action=allow' +
|
||||||
|
' protocol=TCP localport=8787 profile=private,domain' +
|
||||||
|
' program="' + ExpandConstant('{app}\{#AppExeName}') + '"',
|
||||||
|
'', SW_HIDE, ewWaitUntilTerminated, Code);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure CurStepChanged(CurStep: TSetupStep);
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
|
var
|
||||||
|
Started: Boolean;
|
||||||
begin
|
begin
|
||||||
if CurStep <> ssPostInstall then
|
if CurStep <> ssPostInstall then
|
||||||
Exit;
|
Exit;
|
||||||
@@ -231,7 +270,23 @@ begin
|
|||||||
// только затем поднимаем службу.
|
// только затем поднимаем службу.
|
||||||
LockDownDataDir();
|
LockDownDataDir();
|
||||||
SetPassword();
|
SetPassword();
|
||||||
InstallService();
|
OpenFirewallPort();
|
||||||
|
Started := InstallService();
|
||||||
|
|
||||||
|
// Обе проверки — про молчаливый отказ: без них установка отрапортует об
|
||||||
|
// успехе, а защиты не будет. При тихой установке (автообновление) человека
|
||||||
|
// за экраном нет, и разбираться придётся по журналу Inno, /LOG= его пишет.
|
||||||
|
if WizardSilent() then
|
||||||
|
Exit;
|
||||||
|
|
||||||
|
if not AuthConfigured() then
|
||||||
|
MsgBox('Не удалось сохранить пароль панели.'#13#10 +
|
||||||
|
'Панель откроется без пароля, и задать его сможет любой, ' +
|
||||||
|
'кто зайдёт первым.', mbError, MB_OK);
|
||||||
|
|
||||||
|
if not Started then
|
||||||
|
MsgBox('Служба не запустилась — контроль сейчас не работает.'#13#10 +
|
||||||
|
'Откройте «Службы» и посмотрите System Helper.', mbError, MB_OK);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||||
@@ -241,4 +296,5 @@ begin
|
|||||||
|
|
||||||
StopService();
|
StopService();
|
||||||
RunSc('delete {#ServiceName}');
|
RunSc('delete {#ServiceName}');
|
||||||
|
RemoveFirewallRule();
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+13
-8
@@ -33,7 +33,12 @@ mod auth;
|
|||||||
mod service;
|
mod service;
|
||||||
mod update;
|
mod update;
|
||||||
|
|
||||||
const ADDR: &str = "127.0.0.1:8787";
|
/// Слушаем все интерфейсы, чтобы панель открывалась и с телефона или другого
|
||||||
|
/// компьютера в домашней сети. Правило брандмауэра ставит установщик, и только
|
||||||
|
/// для частных сетей — в публичной панель наружу смотреть не должна.
|
||||||
|
const BIND_ADDR: &str = "0.0.0.0:8787";
|
||||||
|
/// Адрес для ярлыков и сообщений: 0.0.0.0 в браузер не введёшь.
|
||||||
|
const PANEL_URL: &str = "http://127.0.0.1:8787";
|
||||||
/// Служебные команды не должны мигать консольным окном.
|
/// Служебные команды не должны мигать консольным окном.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub(crate) const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
pub(crate) const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||||
@@ -125,19 +130,17 @@ fn stamp() -> String {
|
|||||||
/// Открывает панель в браузере. Своего окна у программы нет, поэтому запуск
|
/// Открывает панель в браузере. Своего окна у программы нет, поэтому запуск
|
||||||
/// ярлыком без этого выглядел бы как «ничего не произошло».
|
/// ярлыком без этого выглядел бы как «ничего не произошло».
|
||||||
fn open_panel() {
|
fn open_panel() {
|
||||||
let url = format!("http://{ADDR}");
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
// пустой заголовок обязателен: иначе start примет URL за имя окна
|
// пустой заголовок обязателен: иначе start примет URL за имя окна
|
||||||
let _ = std::process::Command::new("cmd")
|
let _ = std::process::Command::new("cmd")
|
||||||
.args(["/C", "start", "", &url])
|
.args(["/C", "start", "", PANEL_URL])
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
.spawn();
|
.spawn();
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let _ = std::process::Command::new("open").arg(&url).spawn();
|
let _ = std::process::Command::new("open").arg(PANEL_URL).spawn();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Окна нет, поэтому паника иначе прошла бы совершенно незаметно: процесс
|
/// Окна нет, поэтому паника иначе прошла бы совершенно незаметно: процесс
|
||||||
@@ -612,12 +615,12 @@ async fn bind_panel(
|
|||||||
) -> Option<tokio::net::TcpListener> {
|
) -> Option<tokio::net::TcpListener> {
|
||||||
let mut complained = false;
|
let mut complained = false;
|
||||||
loop {
|
loop {
|
||||||
match tokio::net::TcpListener::bind(ADDR).await {
|
match tokio::net::TcpListener::bind(BIND_ADDR).await {
|
||||||
Ok(listener) => return Some(listener),
|
Ok(listener) => return Some(listener),
|
||||||
Err(e) if !complained => {
|
Err(e) if !complained => {
|
||||||
complained = true; // в журнал раз, а не каждые полминуты
|
complained = true; // в журнал раз, а не каждые полминуты
|
||||||
state
|
state
|
||||||
.log(format!("{ADDR} занят ({e}) — жду, правила при этом работают"))
|
.log(format!("{BIND_ADDR} занят ({e}) — жду, правила при этом работают"))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
Err(_) => {}
|
||||||
@@ -681,7 +684,9 @@ async fn serve(shutdown: tokio::sync::watch::Receiver<bool>) {
|
|||||||
let Some(listener) = bind_panel(&state, shutdown.clone()).await else {
|
let Some(listener) = bind_panel(&state, shutdown.clone()).await else {
|
||||||
return; // службу остановили, пока ждали порт
|
return; // службу остановили, пока ждали порт
|
||||||
};
|
};
|
||||||
state.log(format!("панель на http://{ADDR}")).await;
|
state
|
||||||
|
.log(format!("панель на {PANEL_URL} и на порту 8787 в локальной сети"))
|
||||||
|
.await;
|
||||||
|
|
||||||
let mut stop = shutdown.clone();
|
let mut stop = shutdown.clone();
|
||||||
let result = axum::serve(listener, app)
|
let result = axum::serve(listener, app)
|
||||||
|
|||||||
Reference in New Issue
Block a user