This commit is contained in:
EthanShoeDev
2025-10-04 16:28:36 -04:00
parent adbd87d102
commit d3f492facb
10 changed files with 331 additions and 188 deletions

View File

@@ -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",

View File

@@ -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();
}}

View File

@@ -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={[
{

View File

@@ -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 }}>

View File

@@ -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',
},

View 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,
});

View File

@@ -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;
}
},

View File

@@ -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;
}

View File

@@ -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) {