Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy connection and healthcheck #183

Merged
merged 16 commits into from
Jan 13, 2022
Merged
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ install:
build:
@chmod u+x ./scripts/build-proxy && ./scripts/build-proxy

## crossbuild: build for all architctures for current OS
## crossbuild: build for all architectures for current OS
crossbuild:
@chmod u+x ./scripts/cross-build-proxy && ./scripts/cross-build-proxy

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"fmt": "yarn run eslint --fix && yarn run prettier --write",
"lint": "yarn run eslint && yarn run prettier --check",
"prettier": "prettier './src/**/*.{ts,tsx}'",
"proxy:run": "make run",
"start": "PORT=3003 nodemon -w './src/assets/styles/antd.customize.less' --exec 'craco start'",
"start:testnet": "REACT_APP_CHAIN=testnet PORT=3003 nodemon -w './src/assets/styles/antd.customize.less' --exec 'craco start'",
"tauri:build": "cargo tauri build",
Expand Down
52 changes: 47 additions & 5 deletions rpc_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ func (s serviceState) String() string {

func init() {
flag.Parse()
// Log method name
// Adds between 20 and 40% overhead
// Comment when not debugging
// https://github.com/sirupsen/logrus#logging-method-name
log.SetReportCaller(true)

if *logToFile {
file, err := os.OpenFile("rpc_proxy.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
Expand Down Expand Up @@ -207,19 +212,29 @@ func (p *rpcProxy) director(
func (p *rpcProxy) newHTTPHandler() *mux.Router {
router := mux.NewRouter()

// Forward all requests to taget TDEX daemon.
// Forward all requests to target TDEX daemon.
router.HandleFunc("/healthcheck", p.handleHealthCheckRequest).Methods(http.MethodGet, http.MethodOptions)
router.HandleFunc("/connect", p.handleConnectRequest).Methods(http.MethodPost, http.MethodOptions)
router.HandleFunc("/disconnect", p.handleDisconnectRequest).Methods(http.MethodPost, http.MethodOptions)
router.PathPrefix("/").HandlerFunc(p.forwardGRPCRequest)

return router
}

func (p *rpcProxy) forwardGRPCRequest(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Access-Control-Allow-Origin", "localhost")
// TODO: 'cargo tauri dev' requires "Access-Control-Allow-Origin: http://localhost:3003/"
// and 'cargo tauri build' requires "Access-Control-Allow-Origin: tauri://localhost"
// Set header from flag
resp.Header().Set("Access-Control-Allow-Origin", "*")
resp.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
resp.Header().Set("Access-Control-Allow-Headers", "*")

if req.Method != "OPTIONS" && !p.isConnected() {
resp.Header().Set("Content-Type", req.Header.Get("Content-Type"))
resp.WriteHeader(http.StatusServiceUnavailable)
return
}

if p.grpcWebProxy.IsGrpcWebRequest(req) ||
p.grpcWebProxy.IsGrpcWebSocketRequest(req) ||
p.grpcWebProxy.IsAcceptableGrpcCorsRequest(req) {
Expand All @@ -236,7 +251,7 @@ func (p *rpcProxy) forwardGRPCRequest(resp http.ResponseWriter, req *http.Reques
}

func (p *rpcProxy) handleHealthCheckRequest(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Access-Control-Allow-Origin", "localhost")
resp.Header().Set("Access-Control-Allow-Origin", "*")
resp.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
resp.Header().Set("Access-Control-Allow-Headers", "*")

Expand All @@ -245,7 +260,7 @@ func (p *rpcProxy) handleHealthCheckRequest(resp http.ResponseWriter, req *http.
}

status := statusServing
if p.tdexdConn == nil {
if !p.isConnected() {
status = statusNotConnected
}
json.NewEncoder(resp).Encode(map[string]interface{}{
Expand All @@ -255,7 +270,7 @@ func (p *rpcProxy) handleHealthCheckRequest(resp http.ResponseWriter, req *http.
}

func (p *rpcProxy) handleConnectRequest(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Access-Control-Allow-Origin", "localhost")
resp.Header().Set("Access-Control-Allow-Origin", "*")
resp.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
resp.Header().Set("Access-Control-Allow-Headers", "*")

Expand Down Expand Up @@ -300,6 +315,33 @@ func (p *rpcProxy) handleConnectRequest(resp http.ResponseWriter, req *http.Requ
})
}

func (p *rpcProxy) handleDisconnectRequest(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Access-Control-Allow-Origin", "*")
resp.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
resp.Header().Set("Access-Control-Allow-Headers", "*")

if req.Method != "OPTIONS" {
log.Infof("handling http request: %s", req.URL.Path)
}

p.lock.Lock()
defer p.lock.Unlock()

if p.tdexdConn != nil {
p.tdexdConn.Close()

p.tdexdConn = nil
}

json.NewEncoder(resp).Encode(map[string]interface{}{
"status": "disconnected",
})
}

func (p *rpcProxy) isConnected() bool {
return p.tdexdConn != nil
}

func createGRPCConn(
daemonEndpoint string, macBytes, certBytes []byte,
) (*grpc.ClientConn, error) {
Expand Down
62 changes: 52 additions & 10 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,63 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]

use tauri::{Menu, MenuItem, Submenu};
use tauri::{Event, Manager, Menu, MenuItem, Submenu};

// the payload type must implement `Serialize`.
// for global events, it also must implement `Clone`.
#[derive(Clone, serde::Serialize)]
struct Payload {
message: String,
}

fn main() {
let menu = Menu::new()
.add_submenu(Submenu::new(
let menu = Menu::new().add_submenu(Submenu::new(
"TdexDashboard",
Menu::new()
.add_native_item(MenuItem::About("TdexDashboard".to_string()))
.add_native_item(MenuItem::Quit),
));
));

#[allow(unused_mut)]
let mut app = tauri::Builder::default()
.menu(menu)
.build(tauri::generate_context!())
.expect("error while running tauri application");

// TODO: remove Event::CloseRequested when Tauri beta.9 is released
// https://tauri.studio/en/docs/api/rust/tauri/enum.event/
app.run(|app_handle, e| match e {
Event::CloseRequested { label, api, .. } => {
let app_handle = app_handle.clone();
let window = app_handle.get_window(&label).unwrap();
api.prevent_close();
window
.emit(
"quit-event",
Payload {
message: "quitting".into(),
},
)
.unwrap();
}

tauri::Builder::default()
.menu(menu)
.run(tauri::generate_context!())
.expect("error while running tauri application");
Event::ExitRequested {
window_label, api, ..
} => {
let app_handle = app_handle.clone();
let window = app_handle.get_window(&window_label).unwrap();
api.prevent_exit();
window
.emit(
"quit-event",
Payload {
message: "quitting".into(),
},
)
.unwrap();
}
_ => {}
})
}
165 changes: 130 additions & 35 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,155 @@
import { once } from '@tauri-apps/api/event';
import { exit } from '@tauri-apps/api/process';
import { Command } from '@tauri-apps/api/shell';
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';

import { useTypedDispatch, useTypedSelector } from './app/store';
import { ServiceUnavailableModal } from './common/ServiceUnavailableModal';
import Shell from './common/Shell';
import { connectProxy } from './features/settings/settingsSlice';
import {
connectProxy,
disconnectProxy,
healthCheckProxy,
setProxyHealth,
} from './features/settings/settingsSlice';
import { useIsReadyQuery } from './features/walletUnlocker/walletUnlocker.api';
import { useInterval } from './hooks/useInterval';
import { Routes } from './routes';
import { sleep } from './utils';

export const App = (): JSX.Element => {
const dispatch = useTypedDispatch();
const useProxy = useTypedSelector(({ settings }) => settings.useProxy);
const { useProxy, proxyHealth } = useTypedSelector(({ settings }) => settings);
const { macaroonCredentials, tdexdConnectUrl } = useTypedSelector(({ settings }) => settings);
const [proxyIsRunning, setProxyIsRunning] = useState<boolean>(false);
const [isServiceUnavailableModalVisible, setIsServiceUnavailableModalVisible] = useState<boolean>(false);
const { queries } = useTypedSelector(({ walletUnlockerService }) => walletUnlockerService);
const isReadyError = queries['isReady(undefined)']?.error;

// Enable copy/paste on Tauri app
useEffect(() => {
document.addEventListener('keypress', function (event) {
if (event.metaKey && event.key === 'c') {
document.execCommand('copy');
event.preventDefault();
}
if (event.metaKey && event.key === 'v') {
document.execCommand('paste');
event.preventDefault();
}
});
// eslint-disable-next-line
}, []);
const [isExiting, setIsExiting] = useState<boolean>(false);
const {
data: isReady,
error: isReadyError,
isLoading: isReadyLoading,
isUninitialized: isReadyUninitialized,
} = useIsReadyQuery();

useEffect(() => {
(async () => {
if (useProxy) {
await Command.sidecar('grpcproxy', '--log-to-file').spawn();
setProxyIsRunning(true);
}
// Enable copy/paste on Tauri app
document.addEventListener('keypress', function (event) {
if (event.metaKey && event.key === 'c') {
document.execCommand('copy');
event.preventDefault();
}
if (event.metaKey && event.key === 'v') {
document.execCommand('paste');
event.preventDefault();
}
});

// Register close app event for cleanup
await once('quit-event', async () => {
try {
setIsExiting(true);
await dispatch(disconnectProxy()).unwrap();
dispatch(setProxyHealth('NOT_SERVING'));
await exit();
} catch (err) {
console.error('err', err);
}
});
})();
// eslint-disable-next-line
}, []);

useEffect(() => {
(async () => {
if (useProxy && tdexdConnectUrl && proxyIsRunning) {
dispatch(connectProxy());
const startProxy = async () => {
const command = Command.sidecar('grpcproxy');
command.on('close', (data) => {
console.log(`grpcproxy command finished with code ${data.code} and signal ${data.signal}`);
});
command.on('error', (error) => console.error(`grpcproxy command error: "${error}"`));
command.stdout.on('data', (line) => console.log(`grpcproxy command stdout: "${line}"`));
command.stderr.on('data', (line) => console.log(`grpcproxy command stderr: "${line}"`));
const child = await command.spawn();
console.log('Proxy pid:', child.pid);
};

const startAndConnectToProxy = useCallback(async () => {
console.log('startAndConnectToProxy');
if (useProxy && proxyHealth !== 'SERVING' && !isExiting) {
// Start proxy
await startProxy();
// Health check
const { desc } = await dispatch(healthCheckProxy()).unwrap();
if (desc === 'SERVING_NOT_CONNECTED') {
// If onboarded, try to connect to proxy
if (macaroonCredentials && tdexdConnectUrl) {
// Connect
await dispatch(connectProxy()).unwrap();
// Recheck health status
const { desc } = await dispatch(healthCheckProxy()).unwrap();
if (desc === 'SERVING') {
console.log('gRPC Proxy:', desc);
} else {
console.log('gRPC Proxy:', desc);
}
}
// If the HTTP call fails
} else if (desc === 'NOT_SERVING') {
console.error('gRPC Proxy:', desc);
}
})();
}, [tdexdConnectUrl, proxyIsRunning, useProxy, dispatch]);
}
}, [dispatch, macaroonCredentials, proxyHealth, tdexdConnectUrl, useProxy, isExiting]);

// Update health proxy status every x seconds
// Try to restart proxy if 'Load failed' error
useInterval(() => {
if (useProxy && !isExiting) {
(async () => {
try {
const health = await dispatch(healthCheckProxy()).unwrap();
console.log('health status:', health);
console.log('isReady', isReady);
console.log('isReadyLoading', isReadyLoading);
console.log('isReadyError', isReadyError);
console.log('isReadyUninitialized', isReadyUninitialized);
} catch (err) {
// Restart proxy
if (err === 'Load failed') {
try {
// Reset proxyHealth manually because healthCheckProxy thunk is throwing before setting it
dispatch(setProxyHealth(undefined));
console.log('Restart proxy');
await startProxy();
} catch (err) {
console.log('Restart failure', err);
}
}
}
})();
}
}, 5_000);

// If onboarded and isReady returns an error
// Start and connect to gRPC proxy
useEffect(() => {
if (macaroonCredentials && tdexdConnectUrl && isReadyError) {
setIsServiceUnavailableModalVisible(true);
if (useProxy) {
(async () => {
const startAndConnectToProxyRetry = async (retryCount = 0, lastError?: string): Promise<void> => {
if (retryCount > 5) throw new Error(lastError);
try {
await startAndConnectToProxy();
} catch (err) {
await sleep(2000);
// @ts-ignore
await startAndConnectToProxyRetry(retryCount + 1, err);
}
};
try {
console.log('startAndConnectToProxyRetry');
await startAndConnectToProxyRetry();
} catch (err) {
console.error('startAndConnectToProxyRetry error', err);
}
})();
}
}, [macaroonCredentials, tdexdConnectUrl, isReadyError]);
}, [proxyHealth, startAndConnectToProxy, useProxy]);

return (
<>
Expand Down
1 change: 1 addition & 0 deletions src/assets/styles/index.less
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import '~antd-css-utilities/utility.min.css';
@import 'utilities.less';
@import 'button.less';
@import 'fonts.less';
@import 'typography.less';
Expand Down
Loading