add logging in renderer thread (#323)

* #322 add logging in renderer thread

* #322 add more logging in main process

* #322 improve logging

* #322 cleanup data requested

---------

Co-authored-by: neil molina <neil@neils-MacBook-Pro.local>
This commit is contained in:
Neil 2023-03-22 14:53:27 +08:00 committed by GitHub
parent caec6b2e7b
commit a50e7b9289
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 249 additions and 153 deletions

View file

@ -40,6 +40,8 @@ jobs:
s3-artifacts-key: ${{ steps.s3-artifact-uploader.outputs.key }}
steps:
- uses: teaxyz/setup@v0
with:
version: 0.25.0
- uses: actions/checkout@v3
- name: get gui version
@ -156,6 +158,8 @@ jobs:
- darwin+aarch64
steps:
- uses: teaxyz/setup@v0
with:
version: 0.25.0
- uses: actions/checkout@v3
- run: rm -rf ./*.{dmg,zip} || true

View file

@ -35,6 +35,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: teaxyz/setup@v0
with:
version: 0.25.0
- uses: actions/checkout@v3
- name: install app dependencies
run: tea -E xc setup
@ -48,6 +50,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: teaxyz/setup@v0
with:
version: 0.25.0
- uses: actions/checkout@v3
- name: cache node_modules build
# TODO: cache issue in our self-hosted macos runner ESPIPE: invalid seek, read

View file

@ -96,12 +96,11 @@ pnpm --filter tea exec pnpm dist
| Project | Version |
|-----------------------------------|-----------|
| nodejs.org | =18.13.0 |
| pnpm.io | >=7.27 |
| nodejs.org | =18.15.0 |
| pnpm.io | =7.18.2 |
| xcfile.dev | >=0.0.110 |
| python.org | >=3.10 |
[aws/cli]: https://aws.amazon.com/cli/
[`xc`]: https://xcfile.dev
[semver]: https://semver.org

View file

@ -4,11 +4,12 @@ import fs from "fs";
import { getTeaPath } from "./tea-dir";
import * as v1Client from "./v1-client";
import { app } from "electron";
import * as log from "electron-log";
const sessionFilePath = path.join(getTeaPath(), "tea.xyz/gui/tmp.dat");
const sessionFolder = path.join(getTeaPath(), "tea.xyz/gui");
interface Session {
export interface Session {
device_id?: string;
key?: string;
user?: any;
@ -18,14 +19,17 @@ interface Session {
export async function readSessionData(): Promise<Session> {
const locale = app.getLocale();
try {
log.info("reading session data");
const sessionBuffer = await fs.readFileSync(sessionFilePath);
const session = JSON.parse(sessionBuffer.toString()) as Session;
session.locale = locale;
return session;
} catch (error) {
console.error(error);
log.error(error);
log.info("requesting device_id");
const req = await v1Client.get<{ deviceId: string }>("/auth/registerDevice");
const data = { device_id: req.deviceId, locale };
log.info("got device_id", data);
await writeSessionData(data);
return data;
}
@ -33,11 +37,13 @@ export async function readSessionData(): Promise<Session> {
export async function writeSessionData(data: Session) {
try {
log.info("creating:", sessionFolder);
await mkdirp(sessionFolder);
log.info("writing session data:", data); // rm this
await fs.writeFileSync(sessionFilePath, JSON.stringify(data), {
encoding: "utf-8"
});
} catch (error) {
console.error(error);
log.error(error);
}
}

View file

@ -50,6 +50,7 @@ export async function openTerminal(cmd: string) {
try {
// TODO SECURITY: escape the cmd if possible or create whitelist of acceptable commands
scriptPath = await createCommandScriptFile(cmd);
if (!scriptPath) throw new Error("unable to create Applse Script");
let stdout = ``;
let stderr = ``;
@ -72,33 +73,38 @@ export async function openTerminal(cmd: string) {
});
});
} catch (error) {
console.error("root:", error);
log.error("openTerminal:", error);
} finally {
if (scriptPath) await fs.unlinkSync(scriptPath);
}
}
const createCommandScriptFile = async (cmd: string): Promise<string> => {
const guiFolder = getGuiPath();
const tmpFilePath = path.join(guiFolder, `${+new Date()}.scpt`);
const command = `"${cmd.replace(/"/g, '\\"')}"`;
const script = `
tell application "iTerm"
activate
if application "iTerm" is running then
try
tell the first window to create tab with default profile
on error
create window with default profile
end try
end if
try {
const guiFolder = getGuiPath();
const tmpFilePath = path.join(guiFolder, `${+new Date()}.scpt`);
const command = `"${cmd.replace(/"/g, '\\"')}"`;
const script = `
tell application "iTerm"
activate
if application "iTerm" is running then
try
tell the first window to create tab with default profile
on error
create window with default profile
end try
end if
delay 0.1
tell the first window to tell current session to write text ${command}
end tell
`.trim();
delay 0.1
tell the first window to tell current session to write text ${command}
end tell
`.trim();
await fs.writeFileSync(tmpFilePath, script, "utf-8");
return tmpFilePath;
await fs.writeFileSync(tmpFilePath, script, "utf-8");
return tmpFilePath;
} catch (error) {
log.error(error);
return "";
}
};

View file

@ -17,7 +17,9 @@ export const setProtocolPath = (path: string) => {
export default function initializeHandlers() {
ipcMain.handle("get-installed-packages", async () => {
try {
log.info("getting installed packages");
const pkgs = await getInstalledPackages();
log.info(`got installed packages: ${pkgs.length}`);
return pkgs;
} catch (error) {
log.error(error);
@ -27,7 +29,9 @@ export default function initializeHandlers() {
ipcMain.handle("get-session", async () => {
try {
log.info("getting session");
const session = await readSessionData();
log.info(session ? "found session data" : "no session data found");
return session;
} catch (error) {
log.error(error);
@ -37,6 +41,7 @@ export default function initializeHandlers() {
ipcMain.handle("update-session", async (_, data) => {
try {
log.info("updating session data with", data); // rm this
await writeSessionData(data as Session);
} catch (error) {
log.error(error);
@ -45,6 +50,7 @@ export default function initializeHandlers() {
ipcMain.handle("install-package", async (_, data) => {
try {
log.info("installing package:", data.full_name);
const result = await installPackage(data.full_name);
return result;
} catch (error) {
@ -58,9 +64,10 @@ export default function initializeHandlers() {
try {
// TODO: detect if mac or linux
// current openTerminal is only design for Mac
log.info("open terminal w/ cmd:", cmd);
await openTerminal(cmd);
} catch (error) {
console.error("elast:", error);
log.error(error);
}
});

View file

@ -8,13 +8,12 @@ export default function initialize(mainWindow: BrowserWindow) {
Pushy.listen();
// Register device for push notifications
Pushy.register({ appId: "64110fb47446e48a2a0e906d" })
.then(async (token) => {
.then(async (push_token) => {
const { device_id } = await readSessionData();
console.log("DEVICE_ID:", device_id, token);
if (device_id)
await post(`/auth/device/${device_id}/register-push-token`, { push_token: token });
if (device_id) await post(`/auth/device/${device_id}/register-push-token`, { push_token });
})
.catch((err) => {
log.error(err);
// Display error dialog
// Pushy.alert(mainWindow, 'Pushy registration error: ' + err.message);
});

View file

@ -3,6 +3,7 @@ import fs from "fs";
import path from "path";
import { app } from "electron";
import semver from "semver";
import * as log from "electron-log";
type Dir = {
name: string;
@ -22,7 +23,7 @@ export const getGuiPath = () => {
export async function getInstalledPackages() {
const pkgsPath = getTeaPath();
log.info("recusively reading:", pkgsPath);
const folders = await deepReadDir({
dir: pkgsPath,
continueDeeper: (name: string) => !semver.valid(name),
@ -40,6 +41,7 @@ export async function getInstalledPackages() {
};
});
log.info("found installed packages:", pkgs.length);
return pkgs;
}
@ -47,6 +49,7 @@ const semverTest =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
export const getPkgBottles = (packageDir: Dir): string[] => {
log.info("getting installed bottle for ", packageDir);
const bottles: string[] = [];
const pkg = packageDir.path.split(".tea/")[1];
@ -63,7 +66,10 @@ export const getPkgBottles = (packageDir: Dir): string[] => {
bottles.push(...childBottles);
}
return bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
const foundBottles = bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
log.info(`Found ${foundBottles.length} bottles from `, packageDir);
return foundBottles;
};
const deepReadDir = async ({
@ -91,7 +97,7 @@ const deepReadDir = async ({
}
}
} catch (e) {
console.log(e);
log.error(e);
}
return arrayOfFiles;
};

View file

@ -1,31 +1,80 @@
import axios from "axios";
import path from "path";
import * as log from "electron-log";
import bcrypt from "bcryptjs";
import { readSessionData, type Session } from "./auth";
const base = "https://api.tea.xyz";
const publicHeader = { Authorization: "public" };
export async function get<T>(urlPath: string) {
const url = new URL(path.join("v1", urlPath), base).toString();
// TODO: add headers
const req = await axios.request<T>({
method: "GET",
url,
headers: {}
});
try {
log.info(`GET /v1/${urlPath}`);
return req.data;
const session = await readSessionData();
const headers =
session?.device_id && session?.user
? await getHeaders(`GET/${urlPath}`, session)
: publicHeader;
const url = new URL(path.join("v1", urlPath), base).toString();
// TODO: add headers
const req = await axios.request<T>({
method: "GET",
url,
headers
});
log.info("REQUEST:", urlPath, req.status);
return req.data;
} catch (error) {
log.error(error);
return null;
}
}
export async function post<T>(urlPath: string, data: { [key: string]: any }) {
const url = new URL(path.join("v1", urlPath), base).toString();
const req = await axios.request<T>({
method: "POST",
url,
headers: {},
data
});
try {
log.info(`POST /v1/${urlPath}`);
console.log("REQ:", req.data);
const session = await readSessionData();
const headers =
session?.device_id && session?.user
? await getHeaders(`GET/${urlPath}`, session)
: publicHeader;
return req.data;
const url = new URL(path.join("v1", urlPath), base).toString();
const req = await axios.request<T>({
method: "POST",
url,
headers,
data
});
log.info("REQUEST:", urlPath, req.status);
return req.data;
} catch (error) {
log.error(error);
return null;
}
}
async function getHeaders(path: string, session: Session) {
const unixMs = new Date().getTime();
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
const deviceId = session.device_id?.split("-")[0];
const preHash = [unixHexSecs, session.key, deviceId, path].join("");
const Authorization = bcrypt.hashSync(preHash, 10);
return {
Authorization,
["tea-ts"]: unixMs.toString(),
["tea-uid"]: session.user?.developer_id,
["tea-gui_id"]: session.device_id
};
}
export default get;

View file

@ -13,19 +13,40 @@
import semverCompare from "semver/functions/compare";
import type { Package, Review, AirtablePost, Bottle } from "@tea/ui/types";
import type { GUIPackage, Course, Category, DeviceAuth, Session } from "./types";
import {
type GUIPackage,
type Course,
type Category,
type DeviceAuth,
type Session,
AuthStatus
} from "./types";
import * as mock from "./native-mock";
import { PackageStates } from "./types";
import { installPackageCommand } from "./native/cli";
import { get as apiGet } from "$libs/v1-client";
const log = window.require("electron-log");
const { ipcRenderer, shell } = window.require("electron");
let retryLimit = 0;
async function getDistPackages(): Promise<Package[]> {
let packages: Package[] = [];
try {
const resultingPackages = await apiGet<Package[]>("packages");
if (resultingPackages) packages = resultingPackages;
} catch (error) {
retryLimit++;
log.error("getDistPackagesList:", error);
if (retryLimit < 3) packages = await getDistPackages();
}
return packages;
}
export async function getPackages(): Promise<GUIPackage[]> {
const [packages, installedPackages] = await Promise.all([
apiGet<Package[]>("packages"),
getDistPackages(),
ipcRenderer.invoke("get-installed-packages") as { version: string; full_name: string }[]
]);
@ -34,7 +55,7 @@ export async function getPackages(): Promise<GUIPackage[]> {
// NOTE: its not ideal to get bottles or set package states here maybe do it async in the package store init
// --- it has noticeable slowness
log.info(`native: installed ${installedPackages.length} out of ${(packages || []).length}`);
return (packages || []).map((pkg) => {
const installedVersions = installedPackages
.filter((p) => p.full_name === pkg.full_name)
@ -67,22 +88,10 @@ export async function installPackage(pkg: GUIPackage, version?: string) {
const specificVersion = version || latestVersion;
await installPackageCommand(pkg.full_name + (specificVersion ? `@${specificVersion}` : ""));
} catch (error) {
console.error(error);
log.error("installPackage:", error);
}
}
export async function getFeaturedCourses(): Promise<Course[]> {
const posts = await apiGet<AirtablePost[]>("posts", { tag: "featured_course" });
return posts.map((post) => {
return {
title: post.title,
sub_title: post.sub_title,
banner_image_url: post.thumb_image_url,
link: post.link
} as Course;
});
}
export async function getTopPackages(): Promise<GUIPackage[]> {
const packages = await mock.getTopPackages();
return packages;
@ -90,47 +99,95 @@ export async function getTopPackages(): Promise<GUIPackage[]> {
export async function getAllPosts(tag?: string): Promise<AirtablePost[]> {
// add filter here someday: tag = news | course
const queryParams = {
...(tag ? { tag } : {}),
nocache: "true"
};
const posts = await apiGet<AirtablePost[]>("posts", queryParams);
return posts;
}
export async function getCategorizedPackages(): Promise<Category[]> {
const categories = await apiGet<Category[]>("/packages/categorized");
return categories;
try {
const queryParams = {
...(tag ? { tag } : {}),
nocache: "true"
};
const posts = await apiGet<AirtablePost[]>("posts", queryParams);
return posts || [];
} catch (error) {
log.error(error);
return [];
}
}
export async function getDeviceAuth(deviceId: string): Promise<DeviceAuth> {
const data = await apiGet<DeviceAuth>(`/auth/device/${deviceId}`);
return data;
let auth: DeviceAuth = {
status: AuthStatus.UNKNOWN,
key: ""
};
try {
const data = await apiGet<DeviceAuth>(`/auth/device/${deviceId}`);
if (data) auth = data;
} catch (error) {
log.error(error);
auth = await getDeviceAuth(deviceId);
}
return auth;
}
export async function getPackageBottles(packageName: string): Promise<Bottle[]> {
console.log("getting bottles for ", packageName);
const pkg: Package = await apiGet<Package>(`packages/${packageName.replaceAll("/", ":")}`);
return pkg.bottles || [];
try {
const pkg = await apiGet<Package>(`packages/${packageName.replaceAll("/", ":")}`);
log.info(`got ${pkg?.bottles?.length || 0} bottles for ${packageName}`);
return (pkg && pkg.bottles) || [];
} catch (error) {
log.error(error);
return [];
}
}
const retryGetPackage: { [key: string]: number } = {};
export async function getPackage(packageName: string): Promise<Partial<Package>> {
const pkg: Partial<Package> = await apiGet<Partial<Package>>(
`packages/${packageName.replaceAll("/", ":")}`
);
let pkg: Partial<Package> = {};
try {
const data = await apiGet<Partial<Package>>(`packages/${packageName.replaceAll("/", ":")}`);
if (data) {
pkg = data;
} else {
throw new Error(`package:${packageName} not found`);
}
} catch (error) {
log.error(error);
retryGetPackage[packageName] = (retryGetPackage[packageName] || 0) + 1;
if (retryGetPackage[packageName] < 3) {
pkg = await getPackage(packageName);
} else {
log.info(`failed to get package:${packageName} after 3 tries`);
}
}
return pkg;
}
export const getSession = async (): Promise<Session | null> => {
const session = await ipcRenderer.invoke("get-session");
return session;
try {
log.info("getting local session data");
const session = await ipcRenderer.invoke("get-session");
log.info("local session data ", session ? "found" : "not found");
return session;
} catch (error) {
log.error(error);
return null;
}
};
export const updateSession = async (session: Partial<Session>) => {
await ipcRenderer.invoke("update-session", session);
try {
await ipcRenderer.invoke("update-session", session);
} catch (error) {
log.error(error);
}
};
export const openTerminal = (cmd: string) => ipcRenderer.invoke("open-terminal", { cmd });
export const openTerminal = (cmd: string) => {
try {
ipcRenderer.invoke("open-terminal", { cmd });
} catch (error) {
log.error(error);
}
};
export const shellOpenExternal = (link: string) => shell.openExternal(link);

View file

@ -229,31 +229,6 @@ function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function getFeaturedCourses(): Promise<Course[]> {
const mockCourses: Course[] = [
{
title: "Developing With Tea",
sub_title: "by Mxcl",
link: "#",
banner_image_url: "https://tea.xyz/Images/packages/mesonbuild_com.jpg"
},
{
title: "Brewing Tea",
sub_title: "by Mxcl",
link: "#",
banner_image_url: "https://tea.xyz/Images/packages/tea_xyz_gx_cc.jpg"
},
{
title: "Harvesting Tea",
sub_title: "by Mxcl",
link: "#",
banner_image_url: "https://tea.xyz/Images/packages/ipfs_tech.jpg"
}
];
return mockCourses;
}
export async function getTopPackages(): Promise<GUIPackage[]> {
await delay(500);
return packages.slice(0, 9).map((pkg) => {
@ -311,30 +286,6 @@ export async function getAllPosts(type: string): Promise<AirtablePost[]> {
return posts;
}
export async function getCategorizedPackages(): Promise<Category[]> {
const mockPackages = packages.slice(0, 9).map((pkg) => ({
...pkg,
state: PackageStates.AVAILABLE
}));
return [
{
label: "framework essentials",
cta_label: "View all essentials >",
packages: mockPackages
},
{
label: "star-struck heavyweights",
cta_label: "View all star-strucks >",
packages: mockPackages
},
{
label: "simply delightful",
cta_label: "View all delightful packages >",
packages: mockPackages
}
];
}
export async function getDeviceAuth(deviceId: string): Promise<any> {
const data = await v1Client.get<any>(`/auth/device/${deviceId}`);
return data;

View file

@ -66,7 +66,7 @@ function initPosts() {
if (!initialized) {
initialized = true;
getAllPosts().then(set);
// getAllPosts().then(set);
}
subscribe((v) => {

View file

@ -53,7 +53,7 @@ export default function initAuthStore() {
key: data.key,
user: data.user
});
user.set(data.user);
user.set(data.user!);
timer && clearInterval(timer);
timer = null;
}

View file

@ -11,6 +11,8 @@ import { notificationStore } from "../stores";
import { openTerminal } from "@native";
import { NotificationType } from "@tea/ui/types";
const log = window.require("electron-log");
const installTea = async () => {
console.log("installing tea...");
try {
@ -28,20 +30,26 @@ export default function initPackagesStore() {
if (!initialized) {
initialized = true;
getPackages().then((pkgs) => {
getPackages().then(async (pkgs) => {
packages.set(pkgs);
packagesIndex = new Fuse(pkgs, {
keys: ["name", "full_name", "desc", "categories"]
});
const teaCliName = "tea.xyz";
pkgs.forEach((pkg) => {
if (pkg.full_name === teaCliName) {
syncTeaCliPackage(pkg);
} else if (pkg.state === PackageStates.INSTALLED) {
syncPackageBottlesAndState(pkg.full_name);
try {
const teaCliName = "tea.xyz";
for (const pkg of pkgs) {
log.info(`syncing ${pkg.full_name}`);
if (pkg.full_name === teaCliName) {
await syncTeaCliPackage(pkg);
} else if (pkg.state === PackageStates.INSTALLED) {
await syncPackageBottlesAndState(pkg.full_name);
}
log.info(`synced ${pkg.full_name}`);
}
});
} catch (error) {
log.error(error);
}
});
}

View file

@ -42,7 +42,7 @@ export enum AuthStatus {
export type DeviceAuth = {
status: AuthStatus;
user: Developer;
user?: Developer;
key: string;
};
export interface Session {