Files
portmaster/desktop/angular/src/app/shared/exit-screen/exit.service.ts

147 lines
4.6 KiB
TypeScript

import { IntegrationService } from './../../integration/integration';
import { Injectable, inject } from '@angular/core';
import { PortapiService } from '@safing/portmaster-api';
import { SfngDialogService } from '@safing/ui';
import { BehaviorSubject, merge, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, skip, switchMap, tap, timeout } from 'rxjs/operators';
import { UIStateService } from 'src/app/services';
import { ActionIndicatorService } from '../action-indicator';
import { ExitScreenComponent } from './exit-screen';
import { INTEGRATION_SERVICE } from 'src/app/integration';
const MessageConnecting = 'Connecting to Portmaster';
const MessageShutdown = 'Shutting Down Portmaster';
const MessageRestart = 'Restarting Portmaster';
const MessageHidden = '';
export type OverlayMessage = typeof MessageConnecting
| typeof MessageShutdown
| typeof MessageRestart
| typeof MessageHidden;
@Injectable({ providedIn: 'root' })
export class ExitService {
private integration = inject(INTEGRATION_SERVICE);
private hasOverlay = false;
private _showOverlay = new BehaviorSubject<OverlayMessage>(MessageConnecting);
/**
* Emits whenever the "Connecting to ..." or "Restarting ..." overlays
* should be shown. It actually emits the message that should be shown.
* An empty string indicates the overlay should be closed.
*/
get showOverlay$() { return this._showOverlay.asObservable() }
constructor(
private stateService: UIStateService,
private portapi: PortapiService,
private dialog: SfngDialogService,
private uai: ActionIndicatorService,
) {
this.portapi.connected$
.pipe(
distinctUntilChanged(),
)
.subscribe(connected => {
if (connected) {
this._showOverlay.next(MessageHidden);
} else if (this._showOverlay.getValue() !== MessageShutdown) {
this._showOverlay.next(MessageConnecting)
}
})
let restartInProgress = false;
merge<OverlayMessage[]>(
this.portapi.sub('runtime:modules/core/event/shutdown')
.pipe(map(() => MessageShutdown)),
this.portapi.sub('runtime:modules/core/event/restart')
.pipe(
tap(() => restartInProgress = true),
map(() => MessageRestart)
),
)
.pipe(
tap(msg => this._showOverlay.next(msg)),
switchMap(() => this.portapi.connected$),
distinctUntilChanged(),
skip(1),
debounceTime(1000), // make sure we display the "shutdown" overlay for at least a second
)
.subscribe(connected => {
if (this._showOverlay.getValue() === MessageShutdown) {
setTimeout(() => {
this.integration.exitApp();
}, 1000)
}
if (connected && restartInProgress) {
restartInProgress = false;
this.portapi.reloadUI()
.pipe(
tap(() => {
setTimeout(() => window.location.reload(), 1000)
})
)
.subscribe(this.uai.httpObserver(
'Reloading UI ...',
'Failed to Reload UI',
))
}
})
window.addEventListener('beforeunload', () => {
// best effort. may not work all the time depending on
// the current websocket buffer state
this.portapi.bridgeAPI('ui/reload', 'POST').subscribe();
})
this.integration.onExitRequest(() => {
this.stateService.uiState()
// make sure to not wait for the portmaster to start
.pipe(timeout(1000), catchError(() => of(null)))
.subscribe(state => {
if (state?.hideExitScreen) {
this.integration.exitApp();
return
}
if (this.hasOverlay) {
return;
}
this.hasOverlay = true;
this.dialog.create(ExitScreenComponent, { autoclose: true })
.onAction('exit', () => this.integration.exitApp())
.onClose.subscribe(() => this.hasOverlay = false);
})
})
}
shutdownPortmaster() {
this.dialog.confirm({
canCancel: true,
header: 'Shutting Down Portmaster',
message: 'Shutting down the Portmaster will stop all Portmaster components and will leave your system unprotected!',
caption: 'Caution',
buttons: [
{
id: 'shutdown',
class: 'danger',
text: 'Shut Down Portmaster'
}
]
})
.onAction('shutdown', () => {
this.portapi.shutdownPortmaster()
.subscribe(this.uai.httpObserver(
'Shutting Down ...',
'Failed to Shut Down',
))
})
}
}