Merge branch 'v2.0' into task/refactor-spn
This commit is contained in:
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Release
|
||||
name: Release v2.X
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -36,6 +36,8 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
installer-linux:
|
||||
#JOB DISABLED FOR NOW
|
||||
if: false
|
||||
name: Installer linux
|
||||
runs-on: ubuntu-latest
|
||||
needs: release-prep
|
||||
@@ -63,6 +65,8 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
installer-windows:
|
||||
#JOB DISABLED FOR NOW
|
||||
if: false
|
||||
name: Installer windows
|
||||
runs-on: windows-latest
|
||||
needs: release-prep
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -57,3 +57,10 @@ windows_core_dll/.vs/windows_core_dll
|
||||
#windows_core_dll
|
||||
windows_core_dll/x64/
|
||||
windows_core_dll/portmaster-core/x64/
|
||||
|
||||
#Tauri-generated files
|
||||
desktop/tauri/src-tauri/gen/
|
||||
|
||||
#Binaries used for installer gereneration for Windows
|
||||
desktop/tauri/src-tauri/binary/
|
||||
desktop/tauri/src-tauri/intel/
|
||||
|
||||
@@ -308,7 +308,7 @@ angular-base:
|
||||
COPY desktop/angular/ .
|
||||
# Remove symlink and copy assets directly.
|
||||
RUN rm ./assets
|
||||
COPY assets/data ./assets
|
||||
# COPY assets/data ./assets # Do not include the assets folder into portmaster.zip, we use the assets.zip instead
|
||||
|
||||
IF [ "${configuration}" = "production" ]
|
||||
RUN --no-cache npm run build-libs
|
||||
@@ -603,6 +603,10 @@ installer-linux:
|
||||
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
||||
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
||||
|
||||
all-artifacts:
|
||||
BUILD +release-prep
|
||||
BUILD +installer-linux
|
||||
|
||||
kext-build:
|
||||
FROM ${rust_builder_image}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ func (s *WindowsSystemService) Execute(args []string, changeRequests <-chan svc.
|
||||
syscall.SIGTERM,
|
||||
)
|
||||
|
||||
isShuttingDown := false
|
||||
// Wait for shutdown signal.
|
||||
waitSignal:
|
||||
for {
|
||||
@@ -119,12 +120,16 @@ waitSignal:
|
||||
}
|
||||
|
||||
case <-s.instance.ShuttingDown():
|
||||
isShuttingDown = true
|
||||
break waitSignal
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger shutdown.
|
||||
s.instance.Shutdown()
|
||||
// Trigger shutdown,
|
||||
// but only if we are not already shutting down.
|
||||
if !isShuttingDown {
|
||||
s.instance.Shutdown()
|
||||
}
|
||||
|
||||
// Notify the service host that service is in shutting down state.
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
|
||||
80
desktop/angular/package-lock.json
generated
80
desktop/angular/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "portmaster",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "portmaster",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.14",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^16.0.1",
|
||||
"@angular/cdk": "^16.0.1",
|
||||
@@ -28,11 +28,10 @@
|
||||
"@tauri-apps/plugin-cli": ">=2.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
|
||||
"@tauri-apps/plugin-dialog": ">=2.0.0",
|
||||
"@tauri-apps/plugin-http": ">=2.2.0",
|
||||
"@tauri-apps/plugin-notification": ">=2.0.0",
|
||||
"@tauri-apps/plugin-os": ">=2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"@tauri-apps/plugin-websocket": ">=2.2.0",
|
||||
"@tauri-apps/plugin-websocket": "^2.3.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"d3": "^7.8.4",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -1190,9 +1189,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/fast-color/node_modules/@babel/runtime": {
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
|
||||
"integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
@@ -1662,39 +1661,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
|
||||
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
|
||||
"integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.26.9",
|
||||
"@babel/types": "^7.26.9"
|
||||
"@babel/template": "^7.27.0",
|
||||
"@babel/types": "^7.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers/node_modules/@babel/template": {
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
||||
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
|
||||
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@babel/types": "^7.26.9"
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/types": "^7.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
|
||||
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
|
||||
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.9"
|
||||
"@babel/types": "^7.27.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -3125,9 +3124,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
|
||||
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
||||
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
@@ -4846,15 +4845,6 @@
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-http": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.4.2.tgz",
|
||||
"integrity": "sha512-deoafidYelei/fmd4AQoHa2aCA9N2DvnnQrF/91QNjE0xCCTuVpPhIQdVRgdHDhFehEal9uI14OTvERBpcfHrg==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-notification": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.2.1.tgz",
|
||||
@@ -4874,9 +4864,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-shell": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.2.0.tgz",
|
||||
"integrity": "sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.2.1.tgz",
|
||||
"integrity": "sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
@@ -7075,9 +7065,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -11924,9 +11914,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
|
||||
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -17424,9 +17414,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portmaster",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.14",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "npm install && npm run build-libs:dev && ng serve --proxy-config ./proxy.json",
|
||||
@@ -45,8 +45,7 @@
|
||||
"@tauri-apps/plugin-notification": ">=2.0.0",
|
||||
"@tauri-apps/plugin-os": ">=2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"@tauri-apps/plugin-http": ">=2.2.0",
|
||||
"@tauri-apps/plugin-websocket": ">=2.2.0",
|
||||
"@tauri-apps/plugin-websocket": "^2.3.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"d3": "^7.8.4",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -105,4 +104,4 @@
|
||||
"webpack-ext-reloader": "^1.1.9",
|
||||
"zip-a-folder": "^1.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
} from './portapi.service';
|
||||
import { Process } from './portapi.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@Injectable()
|
||||
export class AppProfileService {
|
||||
private watchedProfiles = new Map<string, Observable<AppProfile>>();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface AuthKeyResponse {
|
||||
export class MetaAPI {
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
@Inject(PORTMASTER_HTTP_API_ENDPOINT) @Optional() private httpEndpoint: string = 'http://localhost:817/api',
|
||||
@Inject(PORTMASTER_HTTP_API_ENDPOINT) @Optional() private httpEndpoint: string = 'http://127.0.0.1:817/api',
|
||||
) { }
|
||||
|
||||
listEndpoints(): Observable<MetaEndpoint[]> {
|
||||
|
||||
@@ -7,12 +7,37 @@ import { Netquery } from "./netquery.service";
|
||||
import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT, PORTMASTER_WS_API_ENDPOINT } from "./portapi.service";
|
||||
import { SPNService } from "./spn.service";
|
||||
import { WebsocketService } from "./websocket.service";
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { TauriHttpInterceptor } from "./platform-specific/tauri/tauri-http-interceptor";
|
||||
import { IsTauriEnvironment } from "./platform-specific/utils";
|
||||
|
||||
export interface ModuleConfig {
|
||||
httpAPI?: string;
|
||||
websocketAPI?: string;
|
||||
}
|
||||
|
||||
// Factory function to provide the appropriate HTTP client configuration
|
||||
//
|
||||
// This function determines the appropriate HTTP client configuration based on the runtime environment.
|
||||
// If the application is running in a Tauri environment, it uses the TauriHttpInterceptor to ensure
|
||||
// that all HTTP requests are made from the application binary instead of the WebView instance.
|
||||
// This allows for more direct and controlled communication with the Portmaster API.
|
||||
// In other environments (e.g., browser, Electron), the standard HttpClient is used without any interceptors.
|
||||
export function HttpClientProviderFactory() {
|
||||
if (IsTauriEnvironment())
|
||||
{
|
||||
console.log("[portmaster-api] Running under Tauri - using TauriHttpClient");
|
||||
return provideHttpClient(
|
||||
withInterceptors([TauriHttpInterceptor])
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("[portmaster-api] Running in browser - using default HttpClient");
|
||||
return provideHttpClient();
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({})
|
||||
export class PortmasterAPIModule {
|
||||
|
||||
@@ -23,15 +48,16 @@ export class PortmasterAPIModule {
|
||||
*/
|
||||
static forRoot(cfg: ModuleConfig = {}): ModuleWithProviders<PortmasterAPIModule> {
|
||||
if (cfg.httpAPI === undefined) {
|
||||
cfg.httpAPI = `http://${window.location.host}/api`;
|
||||
cfg.httpAPI = `http://127.0.0.1:817/api`;
|
||||
}
|
||||
if (cfg.websocketAPI === undefined) {
|
||||
cfg.websocketAPI = `ws://${window.location.host}/api/database/v1`;
|
||||
cfg.websocketAPI = `ws://127.0.0.1:817/api/database/v1`;
|
||||
}
|
||||
|
||||
return {
|
||||
ngModule: PortmasterAPIModule,
|
||||
providers: [
|
||||
HttpClientProviderFactory(),
|
||||
PortapiService,
|
||||
WebsocketService,
|
||||
MetaAPI,
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { HttpEvent, HttpHandlerFn, HttpRequest, HttpResponse, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
||||
import { from, Observable, switchMap, map, catchError, throwError } from 'rxjs';
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
/**
|
||||
* TauriHttpInterceptor intercepts HTTP requests and routes them through Tauri's `@tauri-apps/plugin-http` API.
|
||||
*
|
||||
* This allows HTTP requests to be executed from the Tauri application binary instead of the WebView,
|
||||
* enabling more secure and direct communication with external APIs.
|
||||
*
|
||||
* The interceptor handles various response types (e.g., JSON, text, blob, arraybuffer) and ensures
|
||||
* that headers and response data are properly mapped to Angular's HttpResponse format.
|
||||
*
|
||||
* References:
|
||||
* - https://angular.dev/guide/http/interceptors
|
||||
*/
|
||||
export function TauriHttpInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
|
||||
const fetchOptions: RequestInit = {
|
||||
method: req.method,
|
||||
headers: req.headers.keys().reduce((acc: Record<string, string>, key) => {
|
||||
acc[key] = req.headers.get(key) || '';
|
||||
return acc;
|
||||
}, {}),
|
||||
body: getRequestBody(req),
|
||||
};
|
||||
//console.log('[TauriHttpInterceptor] Fetching:', req.url, "Headers:", fetchOptions.headers);
|
||||
return from(send_tauri_http_request(req.url, fetchOptions)).pipe(
|
||||
switchMap(response => {
|
||||
// Copy all response headers
|
||||
const headerMap: Record<string, string> = {};
|
||||
response.headers.forEach((value: string, key: string) => {
|
||||
headerMap[key] = value;
|
||||
});
|
||||
const headers = new HttpHeaders(headerMap);
|
||||
|
||||
// Check if response status is ok (2xx)
|
||||
if (!response.ok) {
|
||||
// Get the error content
|
||||
return from(response.text()).pipe(
|
||||
map(errorText => {
|
||||
throw new HttpErrorResponse({
|
||||
error: errorText,
|
||||
headers: headers,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url: req.url
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Get the response type from the request
|
||||
const responseType = req.responseType || 'json';
|
||||
|
||||
// Helper function to create HttpResponse from body
|
||||
const createResponse = (body: any): HttpEvent<unknown> => {
|
||||
return new HttpResponse({
|
||||
body,
|
||||
status: response.status,
|
||||
headers: headers,
|
||||
url: req.url
|
||||
}) as HttpEvent<unknown>;
|
||||
};
|
||||
|
||||
switch (responseType) {
|
||||
case 'text':
|
||||
return from(response.text()).pipe(map(createResponse));
|
||||
case 'arraybuffer':
|
||||
return from(response.arrayBuffer()).pipe(map(createResponse));
|
||||
case 'blob':
|
||||
return from(response.blob()).pipe(
|
||||
map(blob => {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
// Create a new blob with the proper MIME type
|
||||
if (contentType && (!blob.type || blob.type === 'application/octet-stream')) {
|
||||
const typedBlob = new Blob([blob], { type: contentType });
|
||||
return createResponse(typedBlob);
|
||||
}
|
||||
|
||||
return createResponse(blob);
|
||||
})
|
||||
);
|
||||
case 'json':
|
||||
default:
|
||||
return from(response.text()).pipe(
|
||||
map(body => {
|
||||
let parsedBody: any;
|
||||
try {
|
||||
// Only attempt to parse as JSON if we have content
|
||||
// and either explicitly requested JSON or content-type is JSON
|
||||
if (body && (responseType === 'json' ||
|
||||
(response.headers.get('content-type') || '').includes('application/json'))) {
|
||||
parsedBody = JSON.parse(body);
|
||||
} else {
|
||||
parsedBody = body;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[TauriHttpInterceptor] Failed to parse JSON response:', e);
|
||||
parsedBody = body;
|
||||
}
|
||||
return createResponse(parsedBody);
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('[TauriHttpInterceptor] Request failed:', error);
|
||||
|
||||
// If it's already an HttpErrorResponse, just return it
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
return throwError(() => error);
|
||||
}
|
||||
|
||||
// Otherwise create a new HttpErrorResponse with available information
|
||||
return throwError(() => new HttpErrorResponse({
|
||||
error: error.message || 'Unknown error occurred',
|
||||
status: error.status || 0,
|
||||
statusText: error.statusText || 'Unknown Error',
|
||||
url: req.url,
|
||||
headers: error.headers ? new HttpHeaders(error.headers) : new HttpHeaders()
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getRequestBody(req: HttpRequest<unknown>): any {
|
||||
if (!req.body) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Handle different body types properly
|
||||
if (req.body instanceof FormData ||
|
||||
req.body instanceof Blob ||
|
||||
req.body instanceof ArrayBuffer ||
|
||||
req.body instanceof URLSearchParams) {
|
||||
return req.body;
|
||||
}
|
||||
|
||||
// Default to JSON stringify for object data
|
||||
return JSON.stringify(req.body);
|
||||
}
|
||||
|
||||
export async function send_tauri_http_request(
|
||||
url: string,
|
||||
init: RequestInit = {}
|
||||
): Promise<Response> {
|
||||
// Extract method, headers, and body buffer
|
||||
const method = init.method || 'GET';
|
||||
const headers = [...(init.headers instanceof Headers
|
||||
? (() => {
|
||||
const headerArray: [string, string][] = [];
|
||||
init.headers.forEach((value, key) => headerArray.push([key, value]));
|
||||
return headerArray;
|
||||
})()
|
||||
: Object.entries(init.headers || {}))];
|
||||
const body = init.body
|
||||
? new Uint8Array(await new Response(init.body as any).arrayBuffer())
|
||||
: undefined;
|
||||
|
||||
const res = await invoke<{
|
||||
status: number;
|
||||
status_text: string;
|
||||
headers: [string, string][];
|
||||
body: number[];
|
||||
}>('send_tauri_http_request', { url, opts: { method, headers, body } });
|
||||
|
||||
return new Response(new Uint8Array(res.body), {
|
||||
status: res.status,
|
||||
statusText: res.status_text,
|
||||
headers: res.headers,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import WebSocket, { ConnectionConfig, Message } from '@tauri-apps/plugin-websocket';
|
||||
import { Subject, Observable } from 'rxjs';
|
||||
import { WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
||||
import { NgZone } from '@angular/core';
|
||||
|
||||
const LOG_PREFIX = '[tauri_ws]';
|
||||
|
||||
/**
|
||||
* Creates a WebSocket connection using the Tauri WebSocket API and wraps it in an RxJS WebSocketSubject-compatible interface.
|
||||
*
|
||||
* @template T - The type of messages sent and received through the WebSocket.
|
||||
* @param {WebSocketSubjectConfig<T>} opts - Configuration options for the WebSocket connection.
|
||||
* @param {NgZone} ngZone - Angular's NgZone to ensure change detection runs properly.
|
||||
* @returns {WebSocketSubject<T>} - An RxJS WebSocketSubject-compatible object for interacting with the WebSocket.
|
||||
* @throws {Error} If the `serializer` or `deserializer` functions are not provided.
|
||||
*
|
||||
* @example
|
||||
* const wsSubject = createTauriWsConnection({
|
||||
* url: 'ws://example.com',
|
||||
* serializer: JSON.stringify,
|
||||
* deserializer: JSON.parse,
|
||||
* }, ngZone);
|
||||
*/
|
||||
export function createTauriWsConnection<T>(opts: WebSocketSubjectConfig<T>, ngZone: NgZone): WebSocketSubject<T> {
|
||||
if (!opts.serializer) throw new Error(`${LOG_PREFIX} Messages Serializer not provided!`);
|
||||
if (!opts.deserializer) throw new Error(`${LOG_PREFIX} Messages Deserializer not provided!`);
|
||||
|
||||
const serializer = opts.serializer;
|
||||
const deserializer = opts.deserializer;
|
||||
|
||||
let wsConnection: WebSocket | null = null;
|
||||
const messageSubject = new Subject<T>();
|
||||
const observable$ = messageSubject.asObservable();
|
||||
|
||||
// A queue for messages that need to be sent before the connection is established
|
||||
const pendingMessages: T[] = [];
|
||||
|
||||
const notifySubjectError = (descriptionToLog: string, error: Error | any | null = null) => {
|
||||
if (!descriptionToLog) return;
|
||||
if (!error) error = new Error(descriptionToLog);
|
||||
console.error(`${LOG_PREFIX} ${descriptionToLog}:`, error);
|
||||
|
||||
// Run inside NgZone to ensure Angular detects this change
|
||||
ngZone.run(() => {
|
||||
// This completes the observable and prevents further messages from being processed.
|
||||
messageSubject.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// RxJS WebSocketSubject-compatible implementation
|
||||
//////////////////////////////////////////////////////////////
|
||||
const webSocketSubject = {
|
||||
// Standard Observer interface methods
|
||||
next: (message: T) => {
|
||||
if (!wsConnection) {
|
||||
if (pendingMessages.length >= 1000) {
|
||||
console.error(`${LOG_PREFIX} Too many pending messages, skipping message`);
|
||||
return;
|
||||
}
|
||||
pendingMessages.push(message);
|
||||
console.log(`${LOG_PREFIX} Connection not established yet, message queued`);
|
||||
return;
|
||||
}
|
||||
|
||||
let serializedMessage: any;
|
||||
try {
|
||||
serializedMessage = serializer(message);
|
||||
// 'string' type is enough here, since default serializer for portmaster message returns string
|
||||
if (typeof serializedMessage !== 'string')
|
||||
throw new Error('Serialized message is not a string');
|
||||
} catch (error) {
|
||||
console.error(`${LOG_PREFIX} Error serializing message:`, error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Run outside NgZone for better performance during send operations
|
||||
ngZone.runOutsideAngular(() => {
|
||||
try {
|
||||
wsConnection!.send(serializedMessage).catch((err: Error) => {
|
||||
notifySubjectError('Error sending text message', err);
|
||||
});
|
||||
} catch (error) {
|
||||
notifySubjectError('Error sending message', error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
complete: () => {
|
||||
if (wsConnection) {
|
||||
console.log(`${LOG_PREFIX} Closing connection`);
|
||||
|
||||
// Run inside NgZone to ensure Angular detects this change
|
||||
ngZone.run(() => {
|
||||
if (opts.closingObserver?.next) {
|
||||
opts.closingObserver.next(undefined);
|
||||
}
|
||||
|
||||
wsConnection!.disconnect().catch((err: Error) => console.error(`${LOG_PREFIX} Error closing connection:`, err));
|
||||
wsConnection = null;
|
||||
messageSubject.complete();
|
||||
});
|
||||
} else {
|
||||
messageSubject.complete();
|
||||
}
|
||||
},
|
||||
|
||||
// RxJS Observable methods required for compatibility
|
||||
pipe: function(): Observable<any> {
|
||||
// @ts-ignore - Ignore the parameter type mismatch
|
||||
return observable$.pipe(...arguments);
|
||||
},
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Connect to WebSocket
|
||||
//////////////////////////////////////////////////////////////
|
||||
console.log(`${LOG_PREFIX} Connecting to WebSocket:`, opts.url);
|
||||
|
||||
// Connect outside of Angular zone for better performance
|
||||
ngZone.runOutsideAngular(() => {
|
||||
WebSocket.connect(opts.url)
|
||||
.then((ws) => {
|
||||
wsConnection = ws;
|
||||
console.log(`${LOG_PREFIX} Connection established`);
|
||||
|
||||
// Run inside NgZone to ensure Angular detects this connection event
|
||||
ngZone.run(() => {
|
||||
// Create a mock Event for the openObserver
|
||||
if (opts.openObserver) {
|
||||
const mockEvent = new Event('open') as Event;
|
||||
opts.openObserver.next(mockEvent);
|
||||
}
|
||||
|
||||
// Send any pending messages
|
||||
while (pendingMessages.length > 0) {
|
||||
const message = pendingMessages.shift();
|
||||
if (message) webSocketSubject.next(message);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Add a single listener for ALL message types according to Tauri WebSocket API
|
||||
ws.addListener((message: Message) => {
|
||||
// Process message inside ngZone to trigger change detection
|
||||
ngZone.run(() => {
|
||||
try {
|
||||
// Handle different message types from Tauri
|
||||
switch (message.type) {
|
||||
case 'Text':
|
||||
const textData = message.data as string;
|
||||
try {
|
||||
const deserializedMessage = deserializer({ data: textData } as any);
|
||||
messageSubject.next(deserializedMessage);
|
||||
} catch (err) {
|
||||
notifySubjectError('Error deserializing text message', err);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Binary':
|
||||
const binaryData = message.data as number[];
|
||||
try {
|
||||
const uint8Array = new Uint8Array(binaryData);
|
||||
const buffer = uint8Array.buffer;
|
||||
const deserializedMessage = deserializer({ data: buffer } as any);
|
||||
messageSubject.next(deserializedMessage);
|
||||
} catch (err) {
|
||||
notifySubjectError('Error deserializing binary message', err);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Close':
|
||||
// Handle close message
|
||||
const closeData = message.data as { code: number; reason: string } | null;
|
||||
console.log(`${LOG_PREFIX} Connection closed by server`, closeData);
|
||||
|
||||
if (opts.closeObserver) {
|
||||
const closeEvent = {
|
||||
code: closeData?.code || 1000,
|
||||
reason: closeData?.reason || '',
|
||||
wasClean: true,
|
||||
type: 'close',
|
||||
target: null
|
||||
} as unknown as CloseEvent;
|
||||
|
||||
opts.closeObserver.next(closeEvent);
|
||||
}
|
||||
|
||||
messageSubject.complete();
|
||||
wsConnection = null;
|
||||
break;
|
||||
|
||||
case 'Ping':
|
||||
console.log(`${LOG_PREFIX} Received ping`);
|
||||
break;
|
||||
|
||||
case 'Pong':
|
||||
console.log(`${LOG_PREFIX} Received pong`);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${LOG_PREFIX} Error processing message:`, error);
|
||||
// Don't error the subject on message processing errors to keep connection alive
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`${LOG_PREFIX} Listener added successfully`);
|
||||
|
||||
} catch (error) {
|
||||
notifySubjectError('Error adding message listener', error);
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
notifySubjectError('Connection failed', error);
|
||||
});
|
||||
});
|
||||
|
||||
// Cast to WebSocketSubject<T>
|
||||
return webSocketSubject as unknown as WebSocketSubject<T>;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Simple function to detect if the app is running in a Tauri environment
|
||||
export function IsTauriEnvironment(): boolean {
|
||||
return '__TAURI__' in window;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { FeatureID } from "./features";
|
||||
import { PORTMASTER_HTTP_API_ENDPOINT, PortapiService } from './portapi.service';
|
||||
import { Feature, Pin, SPNStatus, UserProfile } from "./spn.types";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable()
|
||||
export class SPNService {
|
||||
|
||||
/** Emits the SPN status whenever it changes */
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, NgZone } from '@angular/core';
|
||||
import { webSocket, WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
||||
import { createTauriWsConnection } from './platform-specific/tauri/tauri-websocket-subject';
|
||||
import { IsTauriEnvironment } from './platform-specific/utils';
|
||||
|
||||
@Injectable()
|
||||
export class WebsocketService {
|
||||
constructor() { }
|
||||
constructor(private ngZone: NgZone) { }
|
||||
|
||||
/**
|
||||
* createConnection creates a new websocket connection using opts.
|
||||
@@ -11,7 +13,12 @@ export class WebsocketService {
|
||||
* @param opts Options for the websocket connection.
|
||||
*/
|
||||
createConnection<T>(opts: WebSocketSubjectConfig<T>): WebSocketSubject<T> {
|
||||
return webSocket(opts);
|
||||
}
|
||||
}
|
||||
if (IsTauriEnvironment()) {
|
||||
console.log('[portmaster-api] Running under Tauri - Using Tauri WebSocket');
|
||||
return createTauriWsConnection<T>(opts, this.ngZone);
|
||||
}
|
||||
|
||||
console.log('[portmaster-api] Running in browser - Using RxJS WebSocket');
|
||||
return webSocket<T>(opts);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { PortalModule } from '@angular/cdk/portal';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { CdkTableModule } from '@angular/cdk/table';
|
||||
import { CommonModule, registerLocaleData } from '@angular/common';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
import { APP_INITIALIZER, LOCALE_ID, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
@@ -174,7 +173,6 @@ const localeConfig = {
|
||||
PortalModule,
|
||||
CdkTableModule,
|
||||
DragDropModule,
|
||||
HttpClientModule,
|
||||
MarkdownModule.forRoot(),
|
||||
ScrollingModule,
|
||||
SfngAccordionModule,
|
||||
|
||||
@@ -24,7 +24,7 @@ function asyncInvoke<T>(method: string, args: object): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const eventId = uuid();
|
||||
|
||||
once<T & { error: string }>(eventId, (event) => {
|
||||
const listenerPromise = once<T & { error: string }>(eventId, (event) => {
|
||||
if (typeof event.payload === 'object' && 'error' in event.payload) {
|
||||
reject(event.payload);
|
||||
return
|
||||
@@ -33,14 +33,17 @@ function asyncInvoke<T>(method: string, args: object): Promise<T> {
|
||||
resolve(event.payload);
|
||||
})
|
||||
|
||||
invoke<string>(method, {
|
||||
...args,
|
||||
responseId: eventId,
|
||||
}).catch((err: any) => {
|
||||
console.error("tauri:invoke rejected: ", method, args, err);
|
||||
reject(err)
|
||||
});
|
||||
})
|
||||
// Only make the invoke call after the listener is registered
|
||||
listenerPromise.then(() => {
|
||||
invoke<string>(method, {
|
||||
...args,
|
||||
responseId: eventId,
|
||||
}).catch((err: any) => {
|
||||
console.error("tauri:invoke rejected: ", method, args, err);
|
||||
reject(err)
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export type ServiceManagerStatus = 'Running' | 'Stopped' | 'NotFound' | 'unsupported service manager' | 'unsupported operating system';
|
||||
|
||||
@@ -348,11 +348,13 @@ export class DashboardPageComponent implements OnInit, AfterViewInit {
|
||||
)
|
||||
.subscribe(response => {
|
||||
// bandwidth bar chart
|
||||
const barChartData = response.bwBarChart
|
||||
.filter(value => (value.sent + value.received) > 0)
|
||||
.sort((a, b) => (b.sent + b.received) - (a.sent + a.received))
|
||||
.slice(0, 10);
|
||||
this.bandwidthBarData = splitQueryResult(barChartData, ['sent', 'received']) as BandwidthBarData[]
|
||||
if (response?.bwBarChart){
|
||||
const barChartData = response.bwBarChart
|
||||
.filter(value => (value.sent + value.received) > 0)
|
||||
.sort((a, b) => (b.sent + b.received) - (a.sent + a.received))
|
||||
.slice(0, 10);
|
||||
this.bandwidthBarData = splitQueryResult(barChartData, ['sent', 'received']) as BandwidthBarData[]
|
||||
}
|
||||
|
||||
// profileCount
|
||||
this.blockedConnections = 0;
|
||||
|
||||
@@ -80,8 +80,8 @@ if (location.pathname !== "/prompt") {
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
importProvidersFrom(PortmasterAPIModule.forRoot({
|
||||
websocketAPI: "ws://localhost:817/api/database/v1",
|
||||
httpAPI: "http://localhost:817/api"
|
||||
websocketAPI: "ws://127.0.0.1:817/api/database/v1",
|
||||
httpAPI: "http://127.0.0.1:817/api"
|
||||
})),
|
||||
NotificationsService,
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Struct representing an RGB color
|
||||
#[allow(dead_code)] // Suppress warnings for unused fields in this struct only
|
||||
pub(crate) struct Rgb(pub(crate) u32, pub(crate) u32, pub(crate) u32);
|
||||
|
||||
impl FromStr for Rgb {
|
||||
|
||||
115
desktop/tauri/src-tauri/Cargo.lock
generated
115
desktop/tauri/src-tauri/Cargo.lock
generated
@@ -1227,12 +1227,6 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||
|
||||
[[package]]
|
||||
name = "dataurl"
|
||||
version = "0.1.2"
|
||||
@@ -2026,10 +2020,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2039,11 +2031,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2381,7 +2371,6 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3956,7 +3945,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portmaster"
|
||||
version = "2.0.0"
|
||||
version = "2.0.14"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"cached",
|
||||
@@ -3988,7 +3977,6 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-os",
|
||||
@@ -4158,60 +4146,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases 0.2.1",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.25",
|
||||
"socket2 0.5.9",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.2",
|
||||
"rand 0.9.0",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.25",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.12",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5"
|
||||
dependencies = [
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.9",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
@@ -4465,10 +4399,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.25",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@@ -4476,7 +4407,6 @@ dependencies = [
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-service",
|
||||
@@ -4485,7 +4415,6 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
@@ -4601,12 +4530,6 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -4676,7 +4599,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.1",
|
||||
"subtle",
|
||||
@@ -4697,9 +4619,6 @@ name = "rustls-pki-types"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
@@ -5561,28 +5480,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "696ef548befeee6c6c17b80ef73e7c41205b6c2204e87ef78ccc231212389a5c"
|
||||
dependencies = [
|
||||
"data-url",
|
||||
"http",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "2.3.1"
|
||||
@@ -6682,16 +6579,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webkit2gtk"
|
||||
version = "2.0.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "portmaster"
|
||||
version = "2.0.0"
|
||||
version = "2.0.14"
|
||||
description = "Portmaster UI"
|
||||
authors = ["Safing"]
|
||||
license = ""
|
||||
@@ -25,8 +25,7 @@ tauri-plugin-single-instance = "2.2.1"
|
||||
tauri-plugin-notification = "2.2.1"
|
||||
tauri-plugin-log = "2.2.1"
|
||||
tauri-plugin-window-state = "2.2.1"
|
||||
tauri-plugin-http = "2.2.1"
|
||||
tauri-plugin-websocket = "2.2.1"
|
||||
tauri-plugin-websocket = "2"
|
||||
|
||||
clap_lex = "0.7.2"
|
||||
|
||||
@@ -49,7 +48,7 @@ http = "1.0.0"
|
||||
url = "2.5.0"
|
||||
thiserror = "1.0"
|
||||
log = "0.4.21"
|
||||
reqwest = { version = "0.12" }
|
||||
reqwest = { version = "0.12", features = ["cookies", "json"] }
|
||||
|
||||
rfd = { version = "*", default-features = false, features = [ "tokio", "gtk3", "common-controls-v6" ] }
|
||||
open = "5.1.3"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
],
|
||||
"remote": {
|
||||
"urls": [
|
||||
"http://localhost:817"
|
||||
"http://127.0.0.1:817"
|
||||
]
|
||||
},
|
||||
"permissions": [
|
||||
@@ -33,6 +33,7 @@
|
||||
"window-state:allow-save-window-state",
|
||||
"window-state:allow-restore-state",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text"
|
||||
"clipboard-manager:allow-write-text",
|
||||
"websocket:default"
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","remote":{"urls":["http://localhost:817"]},"local":true,"windows":["main","splash"],"permissions":["core:path:default","core:event:allow-listen","core:event:allow-unlisten","core:event:allow-emit","core:event:allow-emit-to","core:window:allow-hide","core:window:allow-show","core:window:allow-is-visible","core:window:allow-set-focus","core:window:allow-close","core:window:allow-get-all-windows","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default","shell:allow-open","notification:default","window-state:allow-save-window-state","window-state:allow-restore-state","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
desktop/tauri/src-tauri/src/commands/mod.rs
Normal file
1
desktop/tauri/src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod tauri_http;
|
||||
75
desktop/tauri/src-tauri/src/commands/tauri_http.rs
Normal file
75
desktop/tauri/src-tauri/src/commands/tauri_http.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use tauri::State;
|
||||
use reqwest::{Client, Method};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Creates and configures a shared HTTP client for application-wide use.
|
||||
///
|
||||
/// Returns a reqwest Client configured with:
|
||||
/// - Connection pooling
|
||||
/// - Persistent cookie store
|
||||
///
|
||||
/// Client can be accessed from UI through the exposed Tauri command `send_tauri_http_request(...)`
|
||||
/// Such requests execute directly from the Tauri app binary, not from the WebView process
|
||||
pub fn create_http_client() -> Client {
|
||||
Client::builder()
|
||||
// Maximum idle connections per host
|
||||
.pool_max_idle_per_host(10)
|
||||
// Enable cookie support
|
||||
.cookie_store(true)
|
||||
.user_agent("Portmaster UI")
|
||||
.build()
|
||||
.expect("failed to build HTTP client")
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct HttpRequestOptions {
|
||||
method: String,
|
||||
headers: Vec<(String, String)>,
|
||||
body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct HttpResponse {
|
||||
status: u16,
|
||||
status_text: String,
|
||||
headers: Vec<(String, String)>,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_tauri_http_request(
|
||||
client: State<'_, Client>,
|
||||
url: String,
|
||||
opts: HttpRequestOptions
|
||||
) -> Result<HttpResponse, String> {
|
||||
//println!("URL: {}", url);
|
||||
|
||||
// Build the request
|
||||
let mut req = client
|
||||
.request(Method::from_bytes(opts.method.as_bytes()).map_err(|e| e.to_string())?, &url);
|
||||
|
||||
// Apply headers
|
||||
for (k, v) in opts.headers {
|
||||
req = req.header(&k, &v);
|
||||
}
|
||||
|
||||
// Attach body if present
|
||||
if let Some(body) = opts.body {
|
||||
req = req.body(body);
|
||||
}
|
||||
|
||||
// Send and await the response
|
||||
let resp = req.send().await.map_err(|e| e.to_string())?;
|
||||
|
||||
// Read status, headers, and body
|
||||
let status = resp.status().as_u16();
|
||||
let status_text = resp.status().canonical_reason().unwrap_or("").to_string();
|
||||
let headers = resp
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
|
||||
.collect();
|
||||
let body = resp.bytes().await.map_err(|e| e.to_string())?.to_vec();
|
||||
|
||||
Ok(HttpResponse { status, status_text, headers, body })
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::{env, path::Path, time::Duration};
|
||||
use std::{env, time::Duration};
|
||||
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent, WindowEvent};
|
||||
|
||||
@@ -18,12 +18,14 @@ mod config;
|
||||
mod portmaster;
|
||||
mod traymenu;
|
||||
mod window;
|
||||
mod commands;
|
||||
|
||||
use log::{debug, error, info};
|
||||
use portmaster::PortmasterExt;
|
||||
use tauri_plugin_log::RotationStrategy;
|
||||
use traymenu::setup_tray_menu;
|
||||
use window::{close_splash_window, create_main_window, hide_splash_window};
|
||||
use tauri_plugin_window_state::StateFlags;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
@@ -139,13 +141,22 @@ fn main() {
|
||||
|
||||
// TODO(vladimir): Permission for logs/app2 folder are not guaranteed. Use the default location for now.
|
||||
#[cfg(target_os = "windows")]
|
||||
let log_target = if let Some(data_dir) = cli_args.data {
|
||||
let log_target = if let Some(_) = cli_args.data {
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None })
|
||||
} else {
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout)
|
||||
};
|
||||
|
||||
// Create a single HTTP client that:
|
||||
// - Pools and reuses connections for better performance
|
||||
// - Is exposed to UI through 'send_tauri_http_request()' command
|
||||
// - Such requests execute directly from the Tauri app binary, not from the WebView process
|
||||
let http_client = commands::tauri_http::create_http_client();
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
// make HTTP client accessible in commands ('send_tauri_http_request()')
|
||||
.manage(http_client)
|
||||
.plugin(tauri_plugin_websocket::init())
|
||||
// Shell plugin for open_external support
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
// Initialize Logging plugin.
|
||||
@@ -164,7 +175,12 @@ fn main() {
|
||||
// OS Version and Architecture support
|
||||
.plugin(tauri_plugin_os::init())
|
||||
// Initialize save windows state plugin.
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(tauri_plugin_window_state::Builder::default()
|
||||
// Don't save visibility state, so it will not interfere with "--background" command line argument
|
||||
.with_state_flags(StateFlags::all() & !StateFlags::VISIBLE)
|
||||
// Don't save splash window state
|
||||
.with_denylist(&["splash",])
|
||||
.build())
|
||||
// Single instance guard
|
||||
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||
// Send info to already dunning instance.
|
||||
@@ -179,7 +195,8 @@ fn main() {
|
||||
portmaster::commands::get_state,
|
||||
portmaster::commands::set_state,
|
||||
portmaster::commands::should_show,
|
||||
portmaster::commands::should_handle_prompts
|
||||
portmaster::commands::should_handle_prompts,
|
||||
commands::tauri_http::send_tauri_http_request,
|
||||
])
|
||||
// Setup the app an any listeners
|
||||
.setup(move |app| {
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::portapi::client::*;
|
||||
use crate::portapi::message::*;
|
||||
use crate::portapi::models::notification::*;
|
||||
use crate::portapi::types::*;
|
||||
use log::debug;
|
||||
use log::error;
|
||||
use serde_json::json;
|
||||
use tauri::async_runtime;
|
||||
|
||||
@@ -181,15 +181,15 @@ pub fn may_navigate_to_ui(win: &mut WebviewWindow, force: bool) {
|
||||
// Only for dev build
|
||||
// Allow connection to http://localhost:4200
|
||||
let capabilities = include_str!("../capabilities/default.json")
|
||||
.replace("http://localhost:817", "http://localhost:4200");
|
||||
.replace("http://127.0.0.1:817", "http://127.0.0.1:4200");
|
||||
let _ = win.add_capability(capabilities);
|
||||
debug!("[tauri] navigating to http://localhost:4200");
|
||||
_ = win.navigate("http://localhost:4200".parse().unwrap());
|
||||
debug!("[tauri] navigating to http://127.0.0.1:4200");
|
||||
_ = win.navigate("http://127.0.0.1:4200".parse().unwrap());
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
_ = win.navigate("http://localhost:817".parse().unwrap());
|
||||
_ = win.navigate("http://127.0.0.1:817".parse().unwrap());
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
|
||||
@@ -81,6 +81,15 @@ var dataDir
|
||||
|
||||
SimpleSC::SetServiceDescription "PortmasterCore" "Portmaster Application Firewall - Core Service"
|
||||
|
||||
;
|
||||
; Auto start the UI
|
||||
;
|
||||
DetailPrint "Creating registry entry for autostart"
|
||||
WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "Portmaster" '"$INSTDIR\portmaster.exe" --with-prompts --with-notifications --background'
|
||||
|
||||
;
|
||||
; MIGRATION FROM PMv1 TO PMv2
|
||||
;
|
||||
StrCpy $oldInstallationDir "$COMMONPROGRAMDATA\Safing\Portmaster"
|
||||
StrCpy $dataDir "$COMMONPROGRAMDATA\Portmaster"
|
||||
|
||||
@@ -168,6 +177,10 @@ var dataDir
|
||||
Delete /REBOOTOK "$INSTDIR\assets.zip"
|
||||
RMDir /r /REBOOTOK "$INSTDIR"
|
||||
|
||||
; remove the registry entry for the autostart
|
||||
DetailPrint "Removing registry entry for autostart"
|
||||
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
; delete data files
|
||||
Delete /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\history.db"
|
||||
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\cache"
|
||||
|
||||
7
go.mod
7
go.mod
@@ -2,9 +2,6 @@ module github.com/safing/portmaster
|
||||
|
||||
go 1.22.0
|
||||
|
||||
// TODO: Remove when https://github.com/tc-hib/winres/pull/4 is released.
|
||||
replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2
|
||||
|
||||
require (
|
||||
github.com/VictoriaMetrics/metrics v1.35.1
|
||||
github.com/Xuanwo/go-locale v1.1.1
|
||||
@@ -35,7 +32,6 @@ require (
|
||||
github.com/jackc/puddle/v2 v2.2.1
|
||||
github.com/lmittmann/tint v1.0.5
|
||||
github.com/maruel/panicparse/v2 v2.3.1
|
||||
github.com/mat/besticon v3.12.0+incompatible
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/miekg/dns v1.1.62
|
||||
@@ -70,6 +66,8 @@ require (
|
||||
zombiezen.com/go/sqlite v1.3.0
|
||||
)
|
||||
|
||||
require github.com/sergeymakinen/go-bmp v1.0.0 // indirect
|
||||
|
||||
require (
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/aead/ecdh v0.2.0 // indirect
|
||||
@@ -101,6 +99,7 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/satori/go.uuid v1.2.0 // indirect
|
||||
github.com/seehuhn/sha256d v1.0.0 // indirect
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -53,8 +53,6 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dhaavi/winres v0.2.2 h1:SUago7FwhgLSMyDdeuV6enBZ+ZQSl0KwcnbWzvlfBls=
|
||||
github.com/dhaavi/winres v0.2.2/go.mod h1:1NTs+/DtKP1BplIL1+XQSoq4X1PUfLczexS7gf3x9T4=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
@@ -173,8 +171,6 @@ github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/maruel/panicparse/v2 v2.3.1 h1:NtJavmbMn0DyzmmSStE8yUsmPZrZmudPH7kplxBinOA=
|
||||
github.com/maruel/panicparse/v2 v2.3.1/go.mod h1:s3UmQB9Fm/n7n/prcD2xBGDkwXD6y2LeZnhbEXvs9Dg=
|
||||
github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1LhCkL9Q47H9Bg=
|
||||
github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU=
|
||||
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
@@ -254,6 +250,10 @@ github.com/seehuhn/fortuna v1.0.1 h1:lu9+CHsmR0bZnx5Ay646XvCSRJ8PJTi5UYJwDBX68H0
|
||||
github.com/seehuhn/fortuna v1.0.1/go.mod h1:LX8ubejCnUoT/hX+1aKUtbKls2H6DRkqzkc7TdR3iis=
|
||||
github.com/seehuhn/sha256d v1.0.0 h1:TXTsAuEWr02QjRm153Fnvvb6fXXDo7Bmy1FizxarGYw=
|
||||
github.com/seehuhn/sha256d v1.0.0/go.mod h1:PEuxg9faClSveVuFXacQmi+NtDI/PX8bpKjtNzf2+s4=
|
||||
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
|
||||
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
|
||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
@@ -286,6 +286,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tannerryan/ring v1.1.2 h1:iXayOjqHQOLzuy9GwSKuG3nhWfzQkldMlQivcgIr7gQ=
|
||||
github.com/tannerryan/ring v1.1.2/go.mod h1:DkELJEjbZhJBtFKR9Xziwj3HKZnb/knRgljNqp65vH4=
|
||||
github.com/tc-hib/winres v0.3.1 h1:CwRjEGrKdbi5CvZ4ID+iyVhgyfatxFoizjPhzez9Io4=
|
||||
github.com/tc-hib/winres v0.3.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
|
||||
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
|
||||
@@ -3,6 +3,7 @@ Name=Portmaster
|
||||
GenericName=Application Firewall
|
||||
Exec={{exec}} --data=/opt/safing/portmaster --with-prompts --with-notifications
|
||||
Icon={{icon}}
|
||||
StartupWMClass=portmaster
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=System
|
||||
|
||||
@@ -23,6 +23,7 @@ Environment=LOGLEVEL=info
|
||||
Environment=PORTMASTER_ARGS=
|
||||
EnvironmentFile=-/etc/default/portmaster
|
||||
ProtectSystem=true
|
||||
ReadWritePaths=/usr/lib/portmaster
|
||||
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
|
||||
RestrictNamespaces=yes
|
||||
ProtectHome=read-only
|
||||
|
||||
79
packaging/windows/dev_helpers/build_angular.ps1
Normal file
79
packaging/windows/dev_helpers/build_angular.ps1
Normal file
@@ -0,0 +1,79 @@
|
||||
# This script builds the Angular project for the Portmaster application and packages it into a zip file.
|
||||
# The script assumes that all necessary dependencies are installed and available.
|
||||
# Output file: dist/portmaster.zip
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory=$false)]
|
||||
[Alias("d")]
|
||||
[switch]$Development,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[Alias("i")]
|
||||
[switch]$Interactive
|
||||
)
|
||||
|
||||
# Store original directory and find project root
|
||||
$originalDir = Get-Location
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$projectRoot = (Get-Item $scriptDir).Parent.Parent.Parent.FullName
|
||||
|
||||
try {
|
||||
# Create output directory
|
||||
$outputDir = Join-Path $scriptDir "dist"
|
||||
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
||||
|
||||
# Navigate to Angular project
|
||||
Set-Location (Join-Path $projectRoot "desktop\angular")
|
||||
|
||||
# npm install - always run in non-interactive mode, ask in interactive mode
|
||||
if (!$Interactive -or (Read-Host "Run 'npm install'? (Y/N, default: Y)") -notmatch '^[Nn]$') {
|
||||
npm install
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
# build libs - always run in non-interactive mode, ask in interactive mode
|
||||
if (!$Interactive -or (Read-Host "Build shared libraries? (Y/N, default: Y)") -notmatch '^[Nn]$') {
|
||||
if ($Development) {
|
||||
Write-Host "Building shared libraries in development mode" -ForegroundColor Yellow
|
||||
npm run build-libs:dev
|
||||
} else {
|
||||
Write-Host "Building shared libraries in production mode" -ForegroundColor Yellow
|
||||
npm run build-libs
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
# Build Angular project
|
||||
if ($Development) {
|
||||
Write-Host "Building Angular project in development mode" -ForegroundColor Yellow
|
||||
ng build --configuration development --base-href /ui/modules/portmaster/ portmaster
|
||||
} else {
|
||||
Write-Host "Building Angular project in production mode" -ForegroundColor Yellow
|
||||
ng build --configuration production --base-href /ui/modules/portmaster/ portmaster
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
# Create zip archive
|
||||
Write-Host "Creating zip archive" -ForegroundColor Yellow
|
||||
Set-Location dist
|
||||
$destinationZip = Join-Path $outputDir "portmaster.zip"
|
||||
if ($PSVersionTable.PSVersion.Major -ge 5) {
|
||||
# Option 1: Use .NET Framework directly (faster than Compress-Archive)
|
||||
Write-Host "Using System.IO.Compression for faster archiving" -ForegroundColor Yellow
|
||||
if (Test-Path $destinationZip) { Remove-Item $destinationZip -Force } # Remove existing zip if it exists
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
|
||||
[System.IO.Compression.ZipFile]::CreateFromDirectory((Get-Location), $destinationZip, $compressionLevel, $false)
|
||||
}
|
||||
else {
|
||||
# Fall back to Compress-Archive
|
||||
Compress-Archive -Path * -DestinationPath $destinationZip -Force
|
||||
}
|
||||
|
||||
Write-Host "Build completed successfully: $(Join-Path $outputDir "portmaster.zip")" -ForegroundColor Green
|
||||
}
|
||||
finally {
|
||||
# Return to original directory - this will execute even if Ctrl+C is pressed
|
||||
Set-Location $originalDir
|
||||
}
|
||||
38
packaging/windows/dev_helpers/build_tauri.ps1
Normal file
38
packaging/windows/dev_helpers/build_tauri.ps1
Normal file
@@ -0,0 +1,38 @@
|
||||
# This script builds the Tauri application for Portmaster on Windows.
|
||||
# It optionally builds the required Angular tauri-builtin project first.
|
||||
# The script assumes that all necessary dependencies (Node.js, Rust, etc.) are installed.
|
||||
# Output file: dist/portmaster.exe
|
||||
|
||||
# Store original directory and find project root
|
||||
$originalDir = Get-Location
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$projectRoot = (Get-Item $scriptDir).Parent.Parent.Parent.FullName
|
||||
|
||||
# Create output directory
|
||||
$outputDir = Join-Path $scriptDir "dist"
|
||||
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
||||
|
||||
# Ask if user wants to build the Angular tauri-builtin project
|
||||
if ((Read-Host "Build Angular tauri-builtin project? (Y/N, default: Y)") -notmatch '^[Nn]$') {
|
||||
# Navigate to Angular project
|
||||
Set-Location (Join-Path $projectRoot "desktop\angular")
|
||||
|
||||
# Build tauri-builtin project
|
||||
ng build --configuration production --base-href / tauri-builtin
|
||||
if ($LASTEXITCODE -ne 0) { Set-Location $originalDir; exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
# Navigate to Tauri project directory
|
||||
Set-Location (Join-Path $projectRoot "desktop\tauri\src-tauri")
|
||||
|
||||
# Build Tauri project for Windows
|
||||
cargo tauri build --no-bundle
|
||||
if ($LASTEXITCODE -ne 0) { Set-Location $originalDir; exit $LASTEXITCODE }
|
||||
|
||||
# Copy the output files to the script's dist directory
|
||||
$tauriOutput = Join-Path (Get-Location) "target\release"
|
||||
Copy-Item -Path "$tauriOutput\portmaster.exe" -Destination $outputDir -Force
|
||||
|
||||
# Return to original directory
|
||||
Set-Location $originalDir
|
||||
Write-Host "Build completed successfully: $outputDir\portmaster.exe" -ForegroundColor Green
|
||||
@@ -92,7 +92,8 @@ func (sc *ServiceConfig) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCurrentBinaryFolder() (string, error) {
|
||||
// returns the absolute path of the currently running executable
|
||||
func getCurrentBinaryPath() (string, error) {
|
||||
// Get the path of the currently running executable
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
@@ -105,6 +106,16 @@ func getCurrentBinaryFolder() (string, error) {
|
||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
func getCurrentBinaryFolder() (string, error) {
|
||||
// Get the absolute path of the currently running executable
|
||||
absPath, err := getCurrentBinaryPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get the directory of the executable
|
||||
installDir := filepath.Dir(absPath)
|
||||
|
||||
@@ -119,8 +130,8 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
||||
Directory: svcCfg.BinDir,
|
||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
||||
PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"),
|
||||
Ignore: []string{"databases", "intel", "config.json"},
|
||||
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
||||
Ignore: []string{"uninstall.exe"}, // "databases", "intel" and "config.json" not needed here since they are not in the bin dir.
|
||||
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
||||
IndexFile: "index.json",
|
||||
Verify: svcCfg.VerifyBinaryUpdates,
|
||||
AutoCheck: true, // May be changed by config during instance startup.
|
||||
@@ -150,7 +161,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
||||
Directory: svcCfg.BinDir,
|
||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
||||
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"),
|
||||
Ignore: []string{"databases", "intel", "config.json"},
|
||||
Ignore: []string{}, // "databases", "intel" and "config.json" not needed here since they are not in the bin dir.
|
||||
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
||||
IndexFile: "index.json",
|
||||
Verify: svcCfg.VerifyBinaryUpdates,
|
||||
@@ -160,6 +171,21 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
||||
NeedsRestart: true,
|
||||
Notify: true,
|
||||
}
|
||||
if binPath, err := getCurrentBinaryPath(); err == nil {
|
||||
binaryUpdateConfig.PostUpgradeCommands = []updates.UpdateCommandConfig{
|
||||
// Restore SELinux context for the new core binary after upgrade
|
||||
// (`restorecon /usr/lib/portmaster/portmaster-core`)
|
||||
{
|
||||
Command: "restorecon",
|
||||
Args: []string{binPath},
|
||||
TriggerArtifactFName: binPath,
|
||||
FailOnError: false, // Ignore error: 'restorecon' may not be available on a non-SELinux systems.
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return nil, nil, fmt.Errorf("failed to get current binary path: %w", err)
|
||||
}
|
||||
|
||||
intelUpdateConfig = &updates.Config{
|
||||
Name: configure.DefaultIntelIndexName,
|
||||
Directory: filepath.Join(svcCfg.DataDir, "intel"),
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -72,7 +73,7 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) {
|
||||
specialProfileID = profile.PortmasterProfileID
|
||||
default:
|
||||
// Check if this is another Portmaster component.
|
||||
if module.portmasterUIPath != "" && p.Path == module.portmasterUIPath {
|
||||
if p.IsPortmasterUi(context.Background()) {
|
||||
specialProfileID = profile.PortmasterAppProfileID
|
||||
}
|
||||
// Check if this is the system resolver.
|
||||
@@ -104,3 +105,37 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) {
|
||||
|
||||
return specialProfileID
|
||||
}
|
||||
|
||||
// IsPortmasterUi checks if the process is the Portmaster UI or its child (up to 3 parent levels).
|
||||
func (p *Process) IsPortmasterUi(ctx context.Context) bool {
|
||||
if module.portmasterUIPath == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Find parent for up to two levels, if we don't match the path.
|
||||
const checkLevels = 3
|
||||
|
||||
var previousPid int
|
||||
proc := p
|
||||
|
||||
for i := 0; i < checkLevels; i++ {
|
||||
if proc.Pid == UnidentifiedProcessID || proc.Pid == SystemProcessID {
|
||||
break
|
||||
}
|
||||
|
||||
realPath, err := filepath.EvalSymlinks(proc.Path)
|
||||
if err == nil && realPath == module.portmasterUIPath {
|
||||
return true
|
||||
}
|
||||
|
||||
if i < checkLevels-1 { // no need to check parent if we are at the last level
|
||||
previousPid = proc.Pid
|
||||
proc, err = GetOrFindProcess(ctx, proc.ParentPid)
|
||||
if err != nil || proc.Pid == previousPid {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,17 +3,22 @@ package binmeta
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/png" // Register png support for image package
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
_ "github.com/mat/besticon/ico" // Register ico support for image package
|
||||
|
||||
// Import the specialized ICO decoder package
|
||||
// This package seems to work better than "github.com/mat/besticon/ico" with ICO files
|
||||
// extracted from Windows binaries, particularly those containing cursor-related data
|
||||
ico "github.com/sergeymakinen/go-ico"
|
||||
)
|
||||
|
||||
// ConvertICOtoPNG converts a an .ico to a .png image.
|
||||
func ConvertICOtoPNG(ico []byte) (png []byte, err error) {
|
||||
// Decode the ICO.
|
||||
icon, _, err := image.Decode(bytes.NewReader(ico))
|
||||
func ConvertICOtoPNG(icoBytes []byte) (png []byte, err error) {
|
||||
// Decode ICO image.
|
||||
// Note: The standard approach with `image.Decode(bytes.NewReader(icoBytes))` sometimes fails
|
||||
// when processing certain ICO files (particularly those with cursor data),
|
||||
// as it reads initial bytes for format detection before passing the stream to the decoder.
|
||||
icon, err := ico.Decode(bytes.NewReader(icoBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode ICO: %w", err)
|
||||
}
|
||||
|
||||
@@ -538,13 +538,14 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context, md Matchin
|
||||
}
|
||||
|
||||
// Apply new icon if found.
|
||||
if newIcon != nil {
|
||||
if newIcon != nil && !profile.iconExists(newIcon) {
|
||||
if len(profile.Icons) == 0 {
|
||||
profile.Icons = []binmeta.Icon{*newIcon}
|
||||
} else {
|
||||
profile.Icons = append(profile.Icons, *newIcon)
|
||||
profile.Icons = binmeta.SortAndCompactIcons(profile.Icons)
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -559,3 +560,13 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context, md Matchin
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks if the given icon already assigned to the profile.
|
||||
func (profile *Profile) iconExists(newIcon *binmeta.Icon) bool {
|
||||
for _, icon := range profile.Icons {
|
||||
if icon.Value == newIcon.Value && icon.Type == newIcon.Type && icon.Source == newIcon.Source {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
|
||||
case !rrCache.Expired():
|
||||
// Return non-expired cached entry immediately.
|
||||
return rrCache, nil
|
||||
case useStaleCache():
|
||||
case rrCache.RCode == dns.RcodeSuccess && useStaleCache():
|
||||
// Return expired cache if we should use stale cache entries,
|
||||
// but start an async query instead.
|
||||
log.Tracer(ctx).Tracef(
|
||||
|
||||
@@ -107,15 +107,21 @@ func (tr *TCPResolver) UseTLS() *TCPResolver {
|
||||
}
|
||||
|
||||
func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolverConn, error) {
|
||||
var existingConn *tcpResolverConn
|
||||
|
||||
// Minimize the time we hold the lock to avoid blocking other threads.
|
||||
tr.Lock()
|
||||
defer tr.Unlock()
|
||||
if tr.resolverConn != nil && tr.resolverConn.abandoned.IsNotSet() {
|
||||
existingConn = tr.resolverConn
|
||||
}
|
||||
tr.Unlock()
|
||||
|
||||
// Check if we have a resolver.
|
||||
if tr.resolverConn != nil && tr.resolverConn.abandoned.IsNotSet() {
|
||||
if existingConn != nil {
|
||||
// If there is one, check if it's alive!
|
||||
select {
|
||||
case tr.resolverConn.heartbeat <- struct{}{}:
|
||||
return tr.resolverConn, nil
|
||||
case existingConn.heartbeat <- struct{}{}:
|
||||
return existingConn, nil
|
||||
case <-time.After(heartbeatTimeout):
|
||||
log.Warningf("resolver: heartbeat for dns client %s failed", tr.resolver.Info.DescriptiveName())
|
||||
case <-ctx.Done():
|
||||
@@ -162,6 +168,10 @@ func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolve
|
||||
tr.resolver.Info.DescriptiveName(),
|
||||
)
|
||||
|
||||
// Thread-safe resolverConn creation.
|
||||
tr.Lock()
|
||||
defer tr.Unlock()
|
||||
|
||||
// Create resolver connection.
|
||||
tr.resolverConnInstanceID++
|
||||
resolverConn := &tcpResolverConn{
|
||||
|
||||
@@ -50,6 +50,22 @@ var (
|
||||
ErrActionRequired = errors.New("action required")
|
||||
)
|
||||
|
||||
// UpdateCommandConfig defines the configuration for a shell command
|
||||
// that is executed when an update is applied
|
||||
type UpdateCommandConfig struct {
|
||||
// Shell command to execute
|
||||
Command string
|
||||
// Arguments to pass to the command
|
||||
Args []string
|
||||
// Execute triggers: if not empty, the command will be executed only if specified file was updated
|
||||
// if empty, the command will be executed always
|
||||
TriggerArtifactFName string
|
||||
// FailOnError defines whether the upgrade should fail if the command fails
|
||||
// true - upgrade will fail if the command fails
|
||||
// false - upgrade will continue even if the command fails
|
||||
FailOnError bool
|
||||
}
|
||||
|
||||
// Config holds the configuration for the updates module.
|
||||
type Config struct {
|
||||
// Name of the updater.
|
||||
@@ -87,6 +103,9 @@ type Config struct {
|
||||
// Notify defines whether the user shall be informed about events via notifications.
|
||||
// If enabled, disables automatic restart after upgrade.
|
||||
Notify bool
|
||||
|
||||
// list of shell commands needed to run after the upgrade (if any)
|
||||
PostUpgradeCommands []UpdateCommandConfig
|
||||
}
|
||||
|
||||
// Check looks for obvious configuration errors.
|
||||
@@ -404,7 +423,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV
|
||||
Type: notifications.ActionTypeWebhook,
|
||||
Payload: notifications.ActionTypeWebhookPayload{
|
||||
Method: "POST",
|
||||
URL: "updates/apply",
|
||||
URL: "core/restart",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -24,12 +25,20 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Unload UI assets to be able to move files on Windows.
|
||||
u.instance.UI().EnableUpgradeLock()
|
||||
defer u.instance.UI().DisableUpgradeLock()
|
||||
// If we are running in a UI instance, we need to unload the UI assets
|
||||
if u.instance != nil {
|
||||
u.instance.UI().EnableUpgradeLock()
|
||||
defer u.instance.UI().DisableUpgradeLock()
|
||||
}
|
||||
|
||||
// Execute the upgrade.
|
||||
upgradeError := u.upgradeMoveFiles(downloader)
|
||||
if upgradeError == nil {
|
||||
// Files upgraded successfully.
|
||||
// Applying post-upgrade tasks, if any.
|
||||
upgradeError = u.applyPostUpgradeCommands()
|
||||
}
|
||||
|
||||
if upgradeError == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -207,3 +216,41 @@ func (u *Updater) deleteUnfinishedFiles(dir string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Updater) applyPostUpgradeCommands() error {
|
||||
// At this point, we assume that the upgrade was successful and all files are in place.
|
||||
// We need to execute the post-upgrade commands, if any.
|
||||
|
||||
if len(u.cfg.PostUpgradeCommands) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// collect full paths to files that were upgraded, required to check the trigger.
|
||||
upgradedFiles := make(map[string]struct{})
|
||||
for _, artifact := range u.index.Artifacts {
|
||||
upgradedFiles[filepath.Join(u.cfg.Directory, artifact.Filename)] = struct{}{}
|
||||
}
|
||||
|
||||
// Execute post-upgrade commands.
|
||||
for _, puCmd := range u.cfg.PostUpgradeCommands {
|
||||
|
||||
// Check trigger to ensure that we need to run this command.
|
||||
if len(puCmd.TriggerArtifactFName) > 0 {
|
||||
if _, ok := upgradedFiles[puCmd.TriggerArtifactFName]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("updates/%s: executing post-upgrade command: '%s %s'", u.cfg.Name, puCmd.Command, strings.Join(puCmd.Args, " "))
|
||||
output, err := exec.Command(puCmd.Command, puCmd.Args...).CombinedOutput()
|
||||
if err != nil {
|
||||
if puCmd.FailOnError {
|
||||
return fmt.Errorf("post-upgrade command '%s %s' failed: %w, output: %s", puCmd.Command, strings.Join(puCmd.Args, " "), err, string(output))
|
||||
}
|
||||
|
||||
log.Warningf("updates/%s: post-upgrade command '%s %s' failed, but ignored. Error: %s", u.cfg.Name, puCmd.Command, strings.Join(puCmd.Args, " "), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user