mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 06:12:51 +00:00
logs
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
"react-native": "0.81.4",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
"react-native-logs": "^5.5.0",
|
||||
"react-native-mmkv": "^3.3.1",
|
||||
"react-native-reanimated": "~4.1.2",
|
||||
"react-native-safe-area-context": "~5.6.1",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useAppForm, useFieldContext } from '@/components/form-components';
|
||||
import { KeyList } from '@/components/key-manager/KeyList';
|
||||
import { rootLogger } from '@/lib/logger';
|
||||
import { useSshConnMutation } from '@/lib/query-fns';
|
||||
import {
|
||||
connectionDetailsSchema,
|
||||
@@ -23,6 +24,8 @@ import {
|
||||
import { useTheme } from '@/lib/theme';
|
||||
import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing';
|
||||
|
||||
const logger = rootLogger.extend('TabsIndex');
|
||||
|
||||
export default function TabsIndex() {
|
||||
return <Host />;
|
||||
}
|
||||
@@ -65,7 +68,7 @@ function Host() {
|
||||
const formErrors = useStore(connectionForm.store, (state) => state.errorMap);
|
||||
useEffect(() => {
|
||||
if (!formErrors || Object.keys(formErrors).length === 0) return;
|
||||
console.log('formErrors', JSON.stringify(formErrors, null, 2));
|
||||
logger.info('formErrors', JSON.stringify(formErrors, null, 2));
|
||||
}, [formErrors]);
|
||||
|
||||
const isSubmitting = useStore(
|
||||
@@ -207,7 +210,7 @@ function Host() {
|
||||
submittingTitle={buttonLabel}
|
||||
testID="connect"
|
||||
onPress={() => {
|
||||
console.log('Connect button pressed', { isSubmitting });
|
||||
logger.info('Connect button pressed', { isSubmitting });
|
||||
if (isSubmitting) return;
|
||||
void connectionForm.handleSubmit();
|
||||
}}
|
||||
|
||||
@@ -14,11 +14,14 @@ import {
|
||||
import React, {
|
||||
createContext,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { KeyboardAvoidingView, Pressable, Text, View } from 'react-native';
|
||||
import { rootLogger } from '@/lib/logger';
|
||||
import { useSshStore } from '@/lib/ssh-store';
|
||||
import { useTheme } from '@/lib/theme';
|
||||
import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing';
|
||||
@@ -26,6 +29,8 @@ import { useContextSafe } from '@/lib/utils';
|
||||
|
||||
type IconName = keyof typeof Ionicons.glyphMap;
|
||||
|
||||
const logger = rootLogger.extend('TabsShellDetail');
|
||||
|
||||
export default function TabsShellDetail() {
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
@@ -71,7 +76,6 @@ const encoder = new TextEncoder();
|
||||
|
||||
function ShellDetail() {
|
||||
const xtermRef = useRef<XtermWebViewHandle>(null);
|
||||
const terminalReadyRef = useRef(false);
|
||||
const listenerIdRef = useRef<bigint | null>(null);
|
||||
|
||||
const searchParams = useLocalSearchParams<{
|
||||
@@ -95,7 +99,7 @@ function ShellDetail() {
|
||||
|
||||
useEffect(() => {
|
||||
if (shell && connection) return;
|
||||
console.log('shell or connection not found, replacing route with /shell');
|
||||
logger.info('shell or connection not found, replacing route with /shell');
|
||||
router.back();
|
||||
}, [connection, router, shell]);
|
||||
|
||||
@@ -111,6 +115,37 @@ function ShellDetail() {
|
||||
|
||||
const marginBottom = useBottomTabSpacing();
|
||||
|
||||
const [modifierKeysActive, setModifierKeysActive] = useState<
|
||||
KeyboardToolbarModifierButtonProps[]
|
||||
>([]);
|
||||
|
||||
const sendBytes = useCallback(
|
||||
(bytes: Uint8Array<ArrayBuffer>) => {
|
||||
if (!shell) return;
|
||||
|
||||
modifierKeysActive
|
||||
.sort((a, b) => a.orderPreference - b.orderPreference)
|
||||
.forEach((m) => {
|
||||
if (!m.canApplyModifierToBytes(bytes)) return;
|
||||
bytes = m.applyModifierToBytes(bytes);
|
||||
});
|
||||
|
||||
shell.sendData(bytes.buffer).catch((e: unknown) => {
|
||||
logger.warn('sendData failed', e);
|
||||
router.back();
|
||||
});
|
||||
},
|
||||
[shell, router, modifierKeysActive],
|
||||
);
|
||||
const toolbarContext: KeyboardToolbarContextType = useMemo(
|
||||
() => ({
|
||||
modifierKeysActive,
|
||||
setModifierKeysActive,
|
||||
sendBytes,
|
||||
}),
|
||||
[sendBytes, modifierKeysActive],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
@@ -133,11 +168,12 @@ function ShellDetail() {
|
||||
accessibilityLabel="Disconnect"
|
||||
hitSlop={10}
|
||||
onPress={async () => {
|
||||
logger.info('Disconnect button pressed');
|
||||
if (!connection) return;
|
||||
try {
|
||||
await connection.disconnect();
|
||||
} catch (e) {
|
||||
console.warn('Failed to disconnect', e);
|
||||
logger.warn('Failed to disconnect', e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -151,93 +187,80 @@ function ShellDetail() {
|
||||
keyboardVerticalOffset={120}
|
||||
style={{ flex: 1, gap: 4 }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: theme.colors.border,
|
||||
}}
|
||||
>
|
||||
<XtermJsWebView
|
||||
ref={xtermRef}
|
||||
style={{ flex: 1 }}
|
||||
webViewOptions={{
|
||||
// Prevent iOS from adding automatic top inset inside WebView
|
||||
contentInsetAdjustmentBehavior: 'never',
|
||||
<KeyboardToolBarContext value={toolbarContext}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
borderWidth: 2,
|
||||
borderColor: theme.colors.border,
|
||||
}}
|
||||
logger={{
|
||||
log: console.log,
|
||||
// debug: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
}}
|
||||
// xterm options
|
||||
xtermOptions={{
|
||||
theme: {
|
||||
background: theme.colors.background,
|
||||
foreground: theme.colors.textPrimary,
|
||||
},
|
||||
}}
|
||||
onInitialized={() => {
|
||||
if (terminalReadyRef.current) return;
|
||||
terminalReadyRef.current = true;
|
||||
>
|
||||
<XtermJsWebView
|
||||
ref={xtermRef}
|
||||
style={{ flex: 1 }}
|
||||
webViewOptions={{
|
||||
// Prevent iOS from adding automatic top inset inside WebView
|
||||
contentInsetAdjustmentBehavior: 'never',
|
||||
}}
|
||||
logger={{
|
||||
log: logger.info,
|
||||
// debug: logger.debug,
|
||||
warn: logger.warn,
|
||||
error: logger.error,
|
||||
}}
|
||||
xtermOptions={{
|
||||
theme: {
|
||||
background: theme.colors.background,
|
||||
foreground: theme.colors.textPrimary,
|
||||
},
|
||||
}}
|
||||
onInitialized={() => {
|
||||
if (!shell) throw new Error('Shell not found');
|
||||
|
||||
if (!shell) throw new Error('Shell not found');
|
||||
|
||||
// Replay from head, then attach live listener
|
||||
void (async () => {
|
||||
const res = shell.readBuffer({ mode: 'head' });
|
||||
console.log('readBuffer(head)', {
|
||||
chunks: res.chunks.length,
|
||||
nextSeq: res.nextSeq,
|
||||
dropped: res.dropped,
|
||||
});
|
||||
if (res.chunks.length) {
|
||||
const chunks = res.chunks.map((c) => c.bytes);
|
||||
const xr = xtermRef.current;
|
||||
if (xr) {
|
||||
xr.writeMany(chunks.map((c) => new Uint8Array(c)));
|
||||
xr.flush();
|
||||
}
|
||||
}
|
||||
const id = shell.addListener(
|
||||
(ev: ListenerEvent) => {
|
||||
if ('kind' in ev) {
|
||||
console.log('listener.dropped', ev);
|
||||
return;
|
||||
// Replay from head, then attach live listener
|
||||
void (async () => {
|
||||
const res = shell.readBuffer({ mode: 'head' });
|
||||
logger.info('readBuffer(head)', {
|
||||
chunks: res.chunks.length,
|
||||
nextSeq: res.nextSeq,
|
||||
dropped: res.dropped,
|
||||
});
|
||||
if (res.chunks.length) {
|
||||
const chunks = res.chunks.map((c) => c.bytes);
|
||||
const xr = xtermRef.current;
|
||||
if (xr) {
|
||||
xr.writeMany(chunks.map((c) => new Uint8Array(c)));
|
||||
xr.flush();
|
||||
}
|
||||
const chunk = ev;
|
||||
const xr3 = xtermRef.current;
|
||||
if (xr3) xr3.write(new Uint8Array(chunk.bytes));
|
||||
},
|
||||
{ cursor: { mode: 'seq', seq: res.nextSeq } },
|
||||
);
|
||||
console.log('shell listener attached', id.toString());
|
||||
listenerIdRef.current = id;
|
||||
})();
|
||||
// Focus to pop the keyboard (iOS needs the prop we set)
|
||||
const xr2 = xtermRef.current;
|
||||
if (xr2) xr2.focus();
|
||||
}}
|
||||
onData={(terminalMessage) => {
|
||||
if (!shell) return;
|
||||
const bytes = encoder.encode(terminalMessage);
|
||||
shell.sendData(bytes.buffer).catch((e: unknown) => {
|
||||
console.warn('sendData failed', e);
|
||||
router.back();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<KeyboardToolbar
|
||||
sendBytes={(bytes) => {
|
||||
if (!shell) return;
|
||||
shell.sendData(bytes.buffer).catch((e: unknown) => {
|
||||
console.warn('sendData failed', e);
|
||||
router.back();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
const id = shell.addListener(
|
||||
(ev: ListenerEvent) => {
|
||||
if ('kind' in ev) {
|
||||
logger.warn('listener.dropped', ev);
|
||||
return;
|
||||
}
|
||||
const chunk = ev;
|
||||
const xr3 = xtermRef.current;
|
||||
if (xr3) xr3.write(new Uint8Array(chunk.bytes));
|
||||
},
|
||||
{ cursor: { mode: 'seq', seq: res.nextSeq } },
|
||||
);
|
||||
logger.info('shell listener attached', id.toString());
|
||||
listenerIdRef.current = id;
|
||||
})();
|
||||
// Focus to pop the keyboard (iOS needs the prop we set)
|
||||
const xr2 = xtermRef.current;
|
||||
if (xr2) xr2.focus();
|
||||
}}
|
||||
onData={(terminalMessage) => {
|
||||
if (!shell) return;
|
||||
const bytes = encoder.encode(terminalMessage);
|
||||
sendBytes(bytes);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<KeyboardToolbar />
|
||||
</KeyboardToolBarContext>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
{/* <KeyboardToolbar
|
||||
@@ -249,39 +272,43 @@ function ShellDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
type KeyboardToolbarProps = {
|
||||
type KeyboardToolbarContextType = {
|
||||
modifierKeysActive: KeyboardToolbarModifierButtonProps[];
|
||||
setModifierKeysActive: React.Dispatch<
|
||||
React.SetStateAction<KeyboardToolbarModifierButtonProps[]>
|
||||
>;
|
||||
sendBytes: (bytes: Uint8Array<ArrayBuffer>) => void;
|
||||
};
|
||||
const KeyboardToolBarContext = createContext<KeyboardToolbarProps | null>(null);
|
||||
const KeyboardToolBarContext = createContext<KeyboardToolbarContextType | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
function KeyboardToolbar(props: KeyboardToolbarProps) {
|
||||
function KeyboardToolbar() {
|
||||
return (
|
||||
<KeyboardToolBarContext value={props}>
|
||||
<View
|
||||
style={{
|
||||
height: 100,
|
||||
}}
|
||||
>
|
||||
<KeyboardToolbarRow>
|
||||
<KeyboardToolbarButtonPreset preset="esc" />
|
||||
<KeyboardToolbarButtonPreset preset="/" />
|
||||
<KeyboardToolbarButtonPreset preset="|" />
|
||||
<KeyboardToolbarButtonPreset preset="home" />
|
||||
<KeyboardToolbarButtonPreset preset="up" />
|
||||
<KeyboardToolbarButtonPreset preset="end" />
|
||||
<KeyboardToolbarButtonPreset preset="pgup" />
|
||||
</KeyboardToolbarRow>
|
||||
<KeyboardToolbarRow>
|
||||
<KeyboardToolbarButtonPreset preset="tab" />
|
||||
<KeyboardToolbarButtonPreset preset="ctrl" />
|
||||
<KeyboardToolbarButtonPreset preset="alt" />
|
||||
<KeyboardToolbarButtonPreset preset="left" />
|
||||
<KeyboardToolbarButtonPreset preset="down" />
|
||||
<KeyboardToolbarButtonPreset preset="right" />
|
||||
<KeyboardToolbarButtonPreset preset="pgdn" />
|
||||
</KeyboardToolbarRow>
|
||||
</View>
|
||||
</KeyboardToolBarContext>
|
||||
<View
|
||||
style={{
|
||||
height: 100,
|
||||
}}
|
||||
>
|
||||
<KeyboardToolbarRow>
|
||||
<KeyboardToolbarButtonPreset preset="esc" />
|
||||
<KeyboardToolbarButtonPreset preset="/" />
|
||||
<KeyboardToolbarButtonPreset preset="|" />
|
||||
<KeyboardToolbarButtonPreset preset="home" />
|
||||
<KeyboardToolbarButtonPreset preset="up" />
|
||||
<KeyboardToolbarButtonPreset preset="end" />
|
||||
<KeyboardToolbarButtonPreset preset="pgup" />
|
||||
</KeyboardToolbarRow>
|
||||
<KeyboardToolbarRow>
|
||||
<KeyboardToolbarButtonPreset preset="tab" />
|
||||
<KeyboardToolbarButtonPreset preset="ctrl" />
|
||||
<KeyboardToolbarButtonPreset preset="alt" />
|
||||
<KeyboardToolbarButtonPreset preset="left" />
|
||||
<KeyboardToolbarButtonPreset preset="down" />
|
||||
<KeyboardToolbarButtonPreset preset="right" />
|
||||
<KeyboardToolbarButtonPreset preset="pgdn" />
|
||||
</KeyboardToolbarRow>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -321,6 +348,61 @@ function KeyboardToolbarButtonPreset({
|
||||
);
|
||||
}
|
||||
|
||||
type ModifierContract = {
|
||||
canApplyModifierToBytes: (bytes: Uint8Array<ArrayBuffer>) => boolean;
|
||||
applyModifierToBytes: (
|
||||
bytes: Uint8Array<ArrayBuffer>,
|
||||
) => Uint8Array<ArrayBuffer>;
|
||||
orderPreference: number;
|
||||
};
|
||||
|
||||
const noOpModifier: ModifierContract = {
|
||||
canApplyModifierToBytes: (_) => false,
|
||||
applyModifierToBytes: (bytes) => bytes,
|
||||
orderPreference: 0,
|
||||
};
|
||||
|
||||
const escapeByte = 27;
|
||||
|
||||
const ctrlModifier: ModifierContract = {
|
||||
orderPreference: 10,
|
||||
canApplyModifierToBytes: (bytes) => {
|
||||
const firstByte = bytes[0];
|
||||
if (firstByte === undefined) return false;
|
||||
return mapByteToCtrl(firstByte) != null;
|
||||
},
|
||||
applyModifierToBytes: (bytes) => {
|
||||
const firstByte = bytes[0];
|
||||
if (firstByte === undefined) return bytes;
|
||||
const ctrlByte = mapByteToCtrl(firstByte);
|
||||
if (ctrlByte == null) return bytes;
|
||||
return new Uint8Array([ctrlByte]);
|
||||
},
|
||||
};
|
||||
|
||||
const altModifier: ModifierContract = {
|
||||
orderPreference: 20,
|
||||
canApplyModifierToBytes: (bytes) => {
|
||||
return bytes.length > 0 && bytes[0] !== escapeByte;
|
||||
},
|
||||
applyModifierToBytes: (bytes) => {
|
||||
const result = new Uint8Array(bytes.length + 1);
|
||||
result[0] = escapeByte;
|
||||
result.set(bytes, 1);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
function mapByteToCtrl(byte: number): number | null {
|
||||
if (byte === 32) return 0; // Ctrl+Space
|
||||
const uppercase = byte & 0b1101_1111; // Fold to uppercase / control range
|
||||
if (uppercase >= 64 && uppercase <= 95) {
|
||||
return uppercase & 0x1f;
|
||||
}
|
||||
if (byte === 63) return 127; // Ctrl+?
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyboardToolbarButtonPresetToProps: Record<
|
||||
KeyboardToolbarButtonPresetType,
|
||||
KeyboardToolbarButtonProps
|
||||
@@ -332,10 +414,7 @@ const keyboardToolbarButtonPresetToProps: Record<
|
||||
end: { label: 'END', sendBytes: new Uint8Array([27, 91, 70]) },
|
||||
pgup: { label: 'PGUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
|
||||
pgdn: { label: 'PGDN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
|
||||
fn: { label: 'FN', isModifier: true },
|
||||
tab: { label: 'TAB', sendBytes: new Uint8Array([9]) },
|
||||
ctrl: { label: 'CTRL', isModifier: true },
|
||||
alt: { label: 'ALT', isModifier: true },
|
||||
left: { iconName: 'arrow-back', sendBytes: new Uint8Array([27, 91, 68]) },
|
||||
up: { iconName: 'arrow-up', sendBytes: new Uint8Array([27, 91, 65]) },
|
||||
down: { iconName: 'arrow-down', sendBytes: new Uint8Array([27, 91, 66]) },
|
||||
@@ -347,29 +426,40 @@ const keyboardToolbarButtonPresetToProps: Record<
|
||||
delete: { label: 'DELETE', sendBytes: new Uint8Array([27, 91, 51, 126]) },
|
||||
pageup: { label: 'PAGEUP', sendBytes: new Uint8Array([27, 91, 53, 126]) },
|
||||
pagedown: { label: 'PAGEDOWN', sendBytes: new Uint8Array([27, 91, 54, 126]) },
|
||||
fn: {
|
||||
label: 'FN',
|
||||
type: 'modifier',
|
||||
...noOpModifier,
|
||||
},
|
||||
ctrl: { label: 'CTRL', type: 'modifier', ...ctrlModifier },
|
||||
alt: { label: 'ALT', type: 'modifier', ...altModifier },
|
||||
};
|
||||
|
||||
type KeyboardToolbarButtonProps = (
|
||||
type KeyboardToolbarButtonViewProps =
|
||||
| {
|
||||
isModifier: true;
|
||||
label: string;
|
||||
}
|
||||
| {
|
||||
sendBytes: Uint8Array;
|
||||
}
|
||||
) &
|
||||
(
|
||||
| {
|
||||
label: string;
|
||||
}
|
||||
| {
|
||||
iconName: IconName;
|
||||
}
|
||||
);
|
||||
iconName: IconName;
|
||||
};
|
||||
|
||||
type KeyboardToolbarModifierButtonProps = {
|
||||
type: 'modifier';
|
||||
} & ModifierContract &
|
||||
KeyboardToolbarButtonViewProps;
|
||||
type KeyboardToolbarInstantButtonProps = {
|
||||
type?: 'sendBytes';
|
||||
sendBytes: Uint8Array<ArrayBuffer>;
|
||||
} & KeyboardToolbarButtonViewProps;
|
||||
|
||||
type KeyboardToolbarButtonProps =
|
||||
| KeyboardToolbarModifierButtonProps
|
||||
| KeyboardToolbarInstantButtonProps;
|
||||
|
||||
function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
|
||||
const theme = useTheme();
|
||||
const [modifierActive, setModifierActive] = useState(false);
|
||||
const { sendBytes } = useContextSafe(KeyboardToolBarContext);
|
||||
const { sendBytes, modifierKeysActive, setModifierKeysActive } =
|
||||
useContextSafe(KeyboardToolBarContext);
|
||||
|
||||
const isTextLabel = 'label' in props;
|
||||
const children = isTextLabel ? (
|
||||
@@ -382,16 +472,26 @@ function KeyboardToolbarButton(props: KeyboardToolbarButtonProps) {
|
||||
/>
|
||||
);
|
||||
|
||||
const modifierActive =
|
||||
props.type === 'modifier' && modifierKeysActive.includes(props);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
console.log('button pressed');
|
||||
if ('isModifier' in props && props.isModifier) {
|
||||
setModifierActive((active) => !active);
|
||||
} else if ('sendBytes' in props) {
|
||||
// todo: send key press
|
||||
sendBytes(new Uint8Array(props.sendBytes));
|
||||
if (props.type === 'modifier') {
|
||||
setModifierKeysActive((modifierKeysActive) =>
|
||||
modifierKeysActive.includes(props)
|
||||
? modifierKeysActive.filter((m) => m !== props)
|
||||
: [...modifierKeysActive, props],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('sendBytes' in props) {
|
||||
sendBytes(new Uint8Array(props.sendBytes));
|
||||
return;
|
||||
}
|
||||
throw new Error('Invalid button type');
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
|
||||
@@ -17,11 +17,14 @@ import {
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { rootLogger } from '@/lib/logger';
|
||||
import { preferences } from '@/lib/preferences';
|
||||
import {} from '@/lib/query-fns';
|
||||
import { useSshStore } from '@/lib/ssh-store';
|
||||
import { useTheme } from '@/lib/theme';
|
||||
|
||||
const logger = rootLogger.extend('TabsShellList');
|
||||
|
||||
export default function TabsShellList() {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
@@ -35,7 +38,7 @@ function ShellContent() {
|
||||
const connections = useSshStore(
|
||||
useShallow((s) => Object.values(s.connections)),
|
||||
);
|
||||
console.log('DEBUG list view connections', connections.length);
|
||||
logger.debug('list view connections', connections.length);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
|
||||
@@ -4,17 +4,18 @@ import { isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import { Stack } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { KeyboardProvider } from 'react-native-keyboard-controller';
|
||||
import { rootLogger } from '@/lib/logger';
|
||||
import { ThemeProvider } from '../lib/theme';
|
||||
import { queryClient } from '../lib/utils';
|
||||
|
||||
console.log('Fressh App Init', {
|
||||
rootLogger.info('Fressh App Init', {
|
||||
isLiquidGlassAvailable: isLiquidGlassAvailable(),
|
||||
});
|
||||
|
||||
void DevClient.registerDevMenuItems([
|
||||
{
|
||||
callback: () => {
|
||||
console.log('Hello from dev menu');
|
||||
rootLogger.info('Hello from dev menu');
|
||||
},
|
||||
name: 'Hello from dev menu',
|
||||
},
|
||||
|
||||
25
apps/mobile/src/lib/logger.ts
Normal file
25
apps/mobile/src/lib/logger.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { logger, consoleTransport } from "react-native-logs";
|
||||
|
||||
export const rootLogger = logger.createLogger({
|
||||
levels: {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
},
|
||||
severity: "debug",
|
||||
transport: consoleTransport,
|
||||
transportOptions: {
|
||||
colors: {
|
||||
info: "blueBright",
|
||||
warn: "yellowBright",
|
||||
error: "redBright",
|
||||
},
|
||||
},
|
||||
async: true,
|
||||
dateFormat: "time",
|
||||
printLevel: true,
|
||||
printDate: true,
|
||||
fixedExtLvlLength: false,
|
||||
enabled: true,
|
||||
});
|
||||
@@ -1,10 +1,13 @@
|
||||
import { type SshConnectionProgress } from '@fressh/react-native-uniffi-russh';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { rootLogger } from './logger';
|
||||
import { secretsManager, type InputConnectionDetails } from './secrets-manager';
|
||||
import { useSshStore } from './ssh-store';
|
||||
import { AbortSignalTimeout } from './utils';
|
||||
|
||||
const logger = rootLogger.extend('QueryFns');
|
||||
|
||||
export const useSshConnMutation = (opts?: {
|
||||
onConnectionProgress?: (progressEvent: SshConnectionProgress) => void;
|
||||
}) => {
|
||||
@@ -14,20 +17,20 @@ export const useSshConnMutation = (opts?: {
|
||||
return useMutation({
|
||||
mutationFn: async (connectionDetails: InputConnectionDetails) => {
|
||||
try {
|
||||
console.log('Connecting to SSH server...');
|
||||
logger.info('Connecting to SSH server...');
|
||||
// Resolve security into the RN bridge shape
|
||||
const security =
|
||||
connectionDetails.security.type === 'password'
|
||||
? {
|
||||
type: 'password' as const,
|
||||
password: connectionDetails.security.password,
|
||||
}
|
||||
type: 'password' as const,
|
||||
password: connectionDetails.security.password,
|
||||
}
|
||||
: {
|
||||
type: 'key' as const,
|
||||
privateKey: await secretsManager.keys.utils
|
||||
.getPrivateKey(connectionDetails.security.keyId)
|
||||
.then((e) => e.value),
|
||||
};
|
||||
type: 'key' as const,
|
||||
privateKey: await secretsManager.keys.utils
|
||||
.getPrivateKey(connectionDetails.security.keyId)
|
||||
.then((e) => e.value),
|
||||
};
|
||||
|
||||
const sshConnection = await connect({
|
||||
host: connectionDetails.host,
|
||||
@@ -35,11 +38,11 @@ export const useSshConnMutation = (opts?: {
|
||||
username: connectionDetails.username,
|
||||
security,
|
||||
onConnectionProgress: (progressEvent) => {
|
||||
console.log('SSH connect progress event', progressEvent);
|
||||
logger.info('SSH connect progress event', progressEvent);
|
||||
opts?.onConnectionProgress?.(progressEvent);
|
||||
},
|
||||
onServerKey: async (serverKeyInfo) => {
|
||||
console.log('SSH server key', serverKeyInfo);
|
||||
logger.info('SSH server key', serverKeyInfo);
|
||||
return true;
|
||||
},
|
||||
abortSignal: AbortSignalTimeout(5_000),
|
||||
@@ -55,7 +58,7 @@ export const useSshConnMutation = (opts?: {
|
||||
abortSignal: AbortSignalTimeout(5_000),
|
||||
});
|
||||
|
||||
console.log(
|
||||
logger.info(
|
||||
'Connected to SSH server',
|
||||
sshConnection.connectionId,
|
||||
shellHandle.channelId,
|
||||
@@ -68,7 +71,7 @@ export const useSshConnMutation = (opts?: {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error connecting to SSH server', error);
|
||||
logger.error('Error connecting to SSH server', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,14 +3,10 @@ import { queryOptions } from '@tanstack/react-query';
|
||||
import * as Crypto from 'expo-crypto';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import * as z from 'zod';
|
||||
import { rootLogger } from './logger';
|
||||
import { queryClient, type StrictOmit } from './utils';
|
||||
|
||||
const shouldLog = false as boolean;
|
||||
const log = (...args: Parameters<typeof console.log>) => {
|
||||
if (shouldLog) {
|
||||
console.log(...args);
|
||||
}
|
||||
};
|
||||
const logger = rootLogger.extend('SecretsManager');
|
||||
|
||||
function splitIntoChunks(data: string, chunkSize: number): string[] {
|
||||
const chunks: string[] = [];
|
||||
@@ -74,17 +70,17 @@ function makeBetterSecureStore<
|
||||
const rawRootManifestString =
|
||||
await SecureStore.getItemAsync(rootManifestKey);
|
||||
|
||||
log('DEBUG rawRootManifestString', rawRootManifestString);
|
||||
logger.debug('rawRootManifestString', rawRootManifestString);
|
||||
|
||||
log(
|
||||
logger.info(
|
||||
`Root manifest for ${rootManifestKey} is ${rawRootManifestString?.length ?? 0} bytes`,
|
||||
);
|
||||
const unsafedRootManifest: unknown = rawRootManifestString
|
||||
? JSON.parse(rawRootManifestString)
|
||||
: {
|
||||
manifestVersion: rootManifestVersion,
|
||||
manifestChunksIds: [],
|
||||
};
|
||||
manifestVersion: rootManifestVersion,
|
||||
manifestChunksIds: [],
|
||||
};
|
||||
const rootManifest = rootManifestSchema.parse(unsafedRootManifest);
|
||||
const manifestChunks = await Promise.all(
|
||||
rootManifest.manifestChunksIds.map(async (manifestChunkId) => {
|
||||
@@ -94,7 +90,7 @@ function makeBetterSecureStore<
|
||||
);
|
||||
if (!rawManifestChunkString)
|
||||
throw new Error('Manifest chunk not found');
|
||||
log(
|
||||
logger.info(
|
||||
`Manifest chunk for ${manifestChunkKeyString} is ${rawManifestChunkString.length} bytes`,
|
||||
);
|
||||
const unsafedManifestChunk: unknown = JSON.parse(
|
||||
@@ -120,7 +116,7 @@ function makeBetterSecureStore<
|
||||
Array.from({ length: manifestEntry.chunkCount }, async (_, chunkIdx) => {
|
||||
const entryKeyString = entryKey(manifestEntry.id, chunkIdx);
|
||||
const rawEntryChunk = await SecureStore.getItemAsync(entryKeyString);
|
||||
log(
|
||||
logger.info(
|
||||
`Entry chunk for ${entryKeyString} is ${rawEntryChunk?.length} bytes`,
|
||||
);
|
||||
if (!rawEntryChunk) throw new Error('Entry chunk not found');
|
||||
@@ -206,7 +202,7 @@ function makeBetterSecureStore<
|
||||
(mChunk) => mChunk.manifestChunk.entries.length === 0,
|
||||
);
|
||||
if (emptyManifestChunks.length > 0) {
|
||||
log('DEBUG: removing empty manifest chunks', emptyManifestChunks.length);
|
||||
logger.debug('removing empty manifest chunks', emptyManifestChunks.length);
|
||||
manifest.rootManifest.manifestChunksIds =
|
||||
manifest.rootManifest.manifestChunksIds.filter(
|
||||
(mChunkId) =>
|
||||
@@ -234,7 +230,7 @@ function makeBetterSecureStore<
|
||||
value: string;
|
||||
}) {
|
||||
await deleteEntry(params.id).catch(() => {
|
||||
log(`Entry ${params.id} not found, creating new one`);
|
||||
logger.info(`Entry ${params.id} not found, creating new one`);
|
||||
});
|
||||
|
||||
const valueChunks = splitIntoChunks(params.value, sizeLimit);
|
||||
@@ -251,7 +247,7 @@ function makeBetterSecureStore<
|
||||
const existingManifestChunkWithRoom = manifest.manifestChunks.find(
|
||||
(mChunk) => sizeLimit > mChunk.manifestChunkSize + newManifestEntrySize,
|
||||
);
|
||||
log('DEBUG existingManifestChunkWithRoom', existingManifestChunkWithRoom);
|
||||
logger.debug('existingManifestChunkWithRoom', existingManifestChunkWithRoom);
|
||||
const manifestChunkWithRoom =
|
||||
existingManifestChunkWithRoom ??
|
||||
(await (async () => {
|
||||
@@ -263,7 +259,7 @@ function makeBetterSecureStore<
|
||||
manifestChunkId: Crypto.randomUUID(),
|
||||
manifestChunkSize: 0,
|
||||
} satisfies NonNullable<(typeof manifest.manifestChunks)[number]>;
|
||||
log(`Adding new manifest chunk ${newManifestChunk.manifestChunkId}`);
|
||||
logger.info(`Adding new manifest chunk ${newManifestChunk.manifestChunkId}`);
|
||||
manifest.rootManifest.manifestChunksIds.push(
|
||||
newManifestChunk.manifestChunkId,
|
||||
);
|
||||
@@ -271,7 +267,7 @@ function makeBetterSecureStore<
|
||||
rootManifestKey,
|
||||
JSON.stringify(manifest.rootManifest),
|
||||
);
|
||||
log('DEBUG: newRootManifest', manifest.rootManifest);
|
||||
logger.debug('newRootManifest', manifest.rootManifest);
|
||||
return newManifestChunk;
|
||||
})());
|
||||
|
||||
@@ -284,15 +280,15 @@ function makeBetterSecureStore<
|
||||
manifestChunkKeyString,
|
||||
JSON.stringify(manifestChunkWithRoom.manifestChunk),
|
||||
).then(() => {
|
||||
log(
|
||||
logger.info(
|
||||
`Set manifest chunk for ${manifestChunkKeyString} to ${JSON.stringify(manifestChunkWithRoom.manifestChunk).length} bytes`,
|
||||
);
|
||||
}),
|
||||
...valueChunks.map(async (vChunk, chunkIdx) => {
|
||||
const entryKeyString = entryKey(newManifestEntry.id, chunkIdx);
|
||||
console.log('DEBUG: setting entry chunk', entryKeyString);
|
||||
logger.debug('setting entry chunk', entryKeyString);
|
||||
await SecureStore.setItemAsync(entryKeyString, vChunk);
|
||||
log(
|
||||
logger.info(
|
||||
`Set entry chunk for ${entryKeyString} ${chunkIdx} to ${vChunk.length} bytes`,
|
||||
);
|
||||
}),
|
||||
@@ -332,15 +328,15 @@ async function upsertPrivateKey(params: {
|
||||
}) {
|
||||
const validateKeyResult = RnRussh.validatePrivateKey(params.value);
|
||||
if (!validateKeyResult.valid) {
|
||||
console.log('Invalid private key', validateKeyResult.error);
|
||||
logger.info('Invalid private key', validateKeyResult.error);
|
||||
if (validateKeyResult.error.tag === SshError_Tags.RusshKeys) {
|
||||
console.log('Invalid private key inner', validateKeyResult.error.inner);
|
||||
console.log('Invalid private key content', params.value);
|
||||
logger.info('Invalid private key inner', validateKeyResult.error.inner);
|
||||
logger.info('Invalid private key content', params.value);
|
||||
}
|
||||
throw new Error('Invalid private key', { cause: validateKeyResult.error });
|
||||
}
|
||||
const keyId = params.keyId ?? `key_${Crypto.randomUUID()}`;
|
||||
log(`${params.keyId ? 'Upserting' : 'Creating'} private key ${keyId}`);
|
||||
logger.info(`${params.keyId ? 'Upserting' : 'Creating'} private key ${keyId}`);
|
||||
// Preserve createdAtMs if the entry already exists
|
||||
const existing = await betterKeyStorage
|
||||
.getEntry(keyId)
|
||||
@@ -356,7 +352,7 @@ async function upsertPrivateKey(params: {
|
||||
},
|
||||
value: params.value,
|
||||
});
|
||||
log('DEBUG: invalidating key query');
|
||||
logger.debug('invalidating key query');
|
||||
await queryClient.invalidateQueries({ queryKey: [keyQueryKey] });
|
||||
}
|
||||
|
||||
@@ -371,7 +367,7 @@ const listKeysQueryOptions = queryOptions({
|
||||
queryKey: [keyQueryKey],
|
||||
queryFn: async () => {
|
||||
const results = await betterKeyStorage.listEntriesWithValues();
|
||||
log(`Listed ${results.length} private keys`);
|
||||
logger.info(`Listed ${results.length} private keys`);
|
||||
return results;
|
||||
},
|
||||
});
|
||||
@@ -431,7 +427,7 @@ async function upsertConnection(params: {
|
||||
},
|
||||
value: JSON.stringify(params.details),
|
||||
});
|
||||
log('DEBUG: invalidating connection query');
|
||||
logger.debug('invalidating connection query');
|
||||
await queryClient.invalidateQueries({ queryKey: [connectionQueryKey] });
|
||||
return params.details;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
type SshShell,
|
||||
} from '@fressh/react-native-uniffi-russh';
|
||||
import { create } from 'zustand';
|
||||
import { rootLogger } from './logger';
|
||||
|
||||
const logger = rootLogger.extend('SshStore');
|
||||
|
||||
type SshRegistryStore = {
|
||||
connections: Record<string, SshConnection>;
|
||||
@@ -19,7 +22,7 @@ export const useSshStore = create<SshRegistryStore>((set) => ({
|
||||
...args,
|
||||
onDisconnected: (connectionId) => {
|
||||
args.onDisconnected?.(connectionId);
|
||||
console.log('DEBUG connection disconnected', connectionId);
|
||||
logger.debug('connection disconnected', connectionId);
|
||||
set((s) => {
|
||||
const { [connectionId]: _omit, ...rest } = s.connections;
|
||||
return { connections: rest };
|
||||
@@ -33,7 +36,7 @@ export const useSshStore = create<SshRegistryStore>((set) => ({
|
||||
onClosed: (channelId) => {
|
||||
args.onClosed?.(channelId);
|
||||
const storeKey = `${connection.connectionId}-${channelId}` as const;
|
||||
console.log('DEBUG shell closed', storeKey);
|
||||
logger.debug('shell closed', storeKey);
|
||||
set((s) => {
|
||||
const { [storeKey]: _omit, ...rest } = s.shells;
|
||||
if (Object.keys(rest).length === 0) {
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -151,6 +151,9 @@ importers:
|
||||
react-native-keyboard-controller:
|
||||
specifier: 1.18.5
|
||||
version: 1.18.5(react-native-reanimated@4.1.2(@babel/core@7.28.3)(react-native-worklets@0.5.1(@babel/core@7.28.3)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
||||
react-native-logs:
|
||||
specifier: ^5.5.0
|
||||
version: 5.5.0
|
||||
react-native-mmkv:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
||||
@@ -7697,6 +7700,9 @@ packages:
|
||||
react-native: '*'
|
||||
react-native-reanimated: '>=3.0.0'
|
||||
|
||||
react-native-logs@5.5.0:
|
||||
resolution: {integrity: sha512-H3Jc1pNTzNhYb9yHuk1drHdyGHwRvt4IERSz3EUul8vVTey6999fzGRFLK6ugrxYnmw7P+5fo/mRzDXeByhA8g==}
|
||||
|
||||
react-native-mmkv@3.3.1:
|
||||
resolution: {integrity: sha512-LYamDWQirPTUJZ9Re+BkCD+zLRGNr+EVJDeIeblvoJXGatWy9PXnChtajDSLqwjX3EXVeUyjgrembs7wlBw9ug==}
|
||||
peerDependencies:
|
||||
@@ -18495,6 +18501,8 @@ snapshots:
|
||||
react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
||||
react-native-reanimated: 4.1.2(@babel/core@7.28.3)(react-native-worklets@0.5.1(@babel/core@7.28.3)(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
|
||||
|
||||
react-native-logs@5.5.0: {}
|
||||
|
||||
react-native-mmkv@3.3.1(react-native@0.81.4(@babel/core@7.28.3)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
Reference in New Issue
Block a user