Skip to content

Commit

Permalink
feat(desktop): playback of script
Browse files Browse the repository at this point in the history
chore(desktop): upgrade heroicons
  • Loading branch information
blakebyrnes committed Feb 21, 2023
1 parent 7ee3d02 commit 521d21f
Show file tree
Hide file tree
Showing 44 changed files with 385 additions and 268 deletions.
2 changes: 1 addition & 1 deletion datastore/docpage/package.json
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@headlessui/vue": "^1.5.0",
"@heroicons/vue": "^1.0.6",
"@heroicons/vue": "^2.0.16",
"@tailwindcss/forms": "^0.5.0",
"@types/lodash.kebabcase": "^4.1.7",
"@types/node": "^16.7.1",
Expand Down
26 changes: 24 additions & 2 deletions desktop/chrome-extension/src/devtools.ts
Expand Up @@ -8,12 +8,34 @@
// console.log("panel is being hidden");
// }

chrome.devtools.panels.create('Hero Script', null, '/hero-script.html', () => {
window.addEventListener('message', event => {
if (event.data.action === 'returnMinerAddress') {
// @ts-expect-error
window.minerAddress = event.data.minerAddress;
}
});
window.parent?.postMessage({ action: 'getMinerAddress' });

chrome.devtools.panels.create('Hero Script', null, '/hero-script.html', extensionPanel => {
let runOnce = false;
extensionPanel.onShown.addListener(panelWindow => {
if (runOnce) return;
runOnce = true;
// @ts-expect-error
panelWindow.setMinerAddress(window.minerAddress);
});
// newPanel.onHidden.addListener(handleHidden);
return null;
});

chrome.devtools.panels.create('State Generator', null, '/state-generator.html', () => {
chrome.devtools.panels.create('State Generator', null, '/state-generator.html', extensionPanel => {
let runOnce = false;
extensionPanel.onShown.addListener(panelWindow => {
if (runOnce) return;
runOnce = true;
// @ts-expect-error
panelWindow.setMinerAddress(window.minerAddress);
});
// newPanel.onHidden.addListener(handleHidden);
return null;
});
80 changes: 34 additions & 46 deletions desktop/core/index.ts
Expand Up @@ -66,10 +66,15 @@ export default class DesktopCore {
transport: ITransportToClient<IDesktopAppApis>,
request: IncomingMessage,
): IConnectionToClient<IDesktopAppApis, IDesktopAppEvents> {
const url = new URL(request.url, 'https://localhost');
const connectionType = url.searchParams.get('type');
let id: string;
const host = request.socket.remoteAddress;
// give local desktop special permissions. does not need to be specified
if (host === '::1' || host === '::' || host === '127.0.0.1' || host === '::ffff:127.0.0.1') {
if (
connectionType === 'app' &&
(host === '::1' || host === '::' || host === '127.0.0.1' || host === '::ffff:127.0.0.1')
) {
id = 'local';
} else id = nanoid(10);

Expand Down Expand Up @@ -166,10 +171,6 @@ export default class DesktopCore {
const script = heroSession.options.scriptInstanceMeta?.entrypoint;
if (!script) return;

if (heroSession.mode === 'timetravel' || heroSession.mode === 'production') {
return;
}

if (heroSession.options.resumeSessionId || heroSession.options.replaySessionId) {
SourceMapSupport.resetCache();
}
Expand All @@ -195,25 +196,33 @@ export default class DesktopCore {
});
// keep alive session
heroSession.options.sessionKeepAlive = true;

const originalController = this.sessionControllersById.get(heroSession.options.resumeSessionId);
originalController?.setResuming(heroSession.options.resumeSessionId);
if (originalController) return;

const { sessionController, appConnectionId } = this.createSessionController(
heroSession.db,
heroSession.options,
);
if (!sessionController) return;
sessionController.bindLiveSession(heroSession);

await this.sendReadyEvent(
appConnectionId,
sessionId,
heroSession.options,
heroSession.db.path,
new Date(heroSession.createdTime),
);
try {
const originalController = this.sessionControllersById.get(
heroSession.options.resumeSessionId,
);
originalController?.setResuming(heroSession.options.resumeSessionId);
if (originalController) return;

const { sessionController, appConnectionId } = this.createSessionController(
heroSession.db,
heroSession.options,
);
if (!sessionController) return;
sessionController.bindLiveSession(heroSession);

const appConnection = this.appConnectionsById.get(appConnectionId);
await appConnection.sendEvent({
eventType: 'Session.opened',
data: {
heroSessionId: heroSession.id,
options: heroSession.options,
dbPath: heroSession.db.path,
startDate: new Date(heroSession.createdTime),
},
});
} catch (error) {
log.error('ERROR launching ChromeAlive for Session', { error, sessionId });
}
}

private static createSessionController(
Expand Down Expand Up @@ -254,16 +263,9 @@ export default class DesktopCore {
execArgv: dbSession.scriptExecArgv,
execPath: dbSession.scriptExecPath,
};
const { sessionController, appConnectionId } = this.createSessionController(db, options);
const { sessionController } = this.createSessionController(db, options);
const apiConnection = sessionController.addConnection(transport, request);

this.sendReadyEvent(
appConnectionId,
db.sessionId,
options,
db.path,
new Date(dbSession.startDate),
);
sessionController.loadFromDb().catch(error => {
log.error('ERROR loading session from database', {
error,
Expand All @@ -273,20 +275,6 @@ export default class DesktopCore {
return apiConnection;
}

private static sendReadyEvent(
appConnectionId: string,
heroSessionId: string,
options: ISessionCreateOptions,
dbPath: string,
startDate: Date,
): void {
const appConnection = this.appConnectionsById.get(appConnectionId);
appConnection.sendEvent({
eventType: 'Session.opened',
data: { heroSessionId, options, dbPath, startDate },
});
}

private static broadcastAppEvent<T extends keyof IDesktopAppEvents & string>(
eventType: T,
data: IDesktopAppEvents[T],
Expand Down
9 changes: 5 additions & 4 deletions desktop/core/lib/ChromeAliveWindowController.ts
@@ -1,13 +1,14 @@
import Page from '@ulixee/unblocked-agent/lib/Page';
import EventSubscriber from '@ulixee/commons/lib/EventSubscriber';
import IDevtoolsSession from '@ulixee/unblocked-specification/agent/browser/IDevtoolsSession';
import IDevtoolsSession, {Protocol} from '@ulixee/unblocked-specification/agent/browser/IDevtoolsSession';
import { IBrowserContextHooks } from '@ulixee/unblocked-specification/agent/hooks/IBrowserHooks';
import IChromeAliveSessionEvents from '@ulixee/desktop-interfaces/events/IChromeAliveSessionEvents';
import Resolvable from '@ulixee/commons/lib/Resolvable';
import BridgeToExtension from './bridges/BridgeToExtension';
import DevtoolsBackdoorModule from './app-extension-modules/DevtoolsBackdoorModule';
import ElementsModule from './app-extension-modules/ElementsModule';
import AppDevtoolsConnection from './AppDevtoolsConnection';
import TargetInfo = Protocol.Target.TargetInfo;

export default class ChromeAliveWindowController implements IBrowserContextHooks {
// TODO: support multiple replay tabs for finder
Expand Down Expand Up @@ -67,16 +68,16 @@ export default class ChromeAliveWindowController implements IBrowserContextHooks
return await this.pendingPagePromisesByTabId.get(heroTabId).promise;
}

public onDevtoolsPanelAttached(devtoolsSession: IDevtoolsSession): Promise<any> {
return this.devtoolsBackdoorModule.onDevtoolsPanelAttached(devtoolsSession);
public onDevtoolsPanelAttached(devtoolsSession: IDevtoolsSession, targetInfo: TargetInfo): Promise<any> {
return this.devtoolsBackdoorModule.onDevtoolsPanelAttached(devtoolsSession, targetInfo);
}

public onDevtoolsPanelDetached(devtoolsSession: IDevtoolsSession): Promise<any> {
this.devtoolsBackdoorModule.onDevtoolsPanelDetached(devtoolsSession);
return Promise.resolve();
}

public async onDevtoolsOpened(target: {
public async onDevtoolsOpenedInApp(target: {
targetId: string;
browserContextId: string;
isReconnect?: boolean;
Expand Down
65 changes: 39 additions & 26 deletions desktop/core/lib/ResourceSearch.ts
Expand Up @@ -5,16 +5,29 @@ import FuseJs from 'fuse.js';
import { ISearchContext } from '@ulixee/desktop-interfaces/ISessionSearchResult';
import IResourceMeta from '@ulixee/unblocked-specification/agent/net/IResourceMeta';
import IResourceType from '@ulixee/unblocked-specification/agent/net/IResourceType';
import SessionDb from '@ulixee/hero-core/dbs/SessionDb';
import IHttpHeaders from '@ulixee/unblocked-specification/agent/net/IHttpHeaders';

const Fuse = require('fuse.js/dist/fuse.common.js');

export default class ResourceSearch {
private static allowedResourceTypes = new Set<IResourceType>([
'Document',
'XHR',
'Fetch',
'Script',
'Websocket',
'Other',
]);

private searchIndexByTabId: {
[tabId: number]: FuseJs<{ id: number; body: string; url: string }>;
} = {};

constructor(private heroSession: HeroSession, private events: EventSubscriber) {
this.events.on(this.heroSession, 'tab-created', this.onTabCreated.bind(this));
constructor(private db: SessionDb, private events: EventSubscriber) {}

public close(): void {
this.events.close();
}

public search(query: string, context: ISearchContext): IResourceSearchResult[] {
Expand All @@ -33,12 +46,12 @@ export default class ResourceSearch {

const searchResults = this.searchIndexByTabId[tabId].search(finalQuery, { limit: 10 });
for (const result of searchResults) {
const resource = this.heroSession.resources.get(result.item.id);
const resource = this.db.resources.get(result.item.id);
// must match document url
if (documentUrl && resource.documentUrl !== documentUrl) continue;
// allow an exception for the actual document
const isPageLoad = resource.url === documentUrl;
const timestamp = resource.response.browserLoadedTime ?? resource.response.timestamp;
const isPageLoad = resource.requestUrl === documentUrl;
const timestamp = resource.browserLoadedTimestamp ?? resource.responseTimestamp;
if (!isPageLoad && (timestamp < startTime || timestamp > endTime)) continue;

const matchIndices: IResourceSearchResult['matchIndices'] = [];
Expand All @@ -53,8 +66,8 @@ export default class ResourceSearch {
results.push({
id: resource.id,
documentUrl: resource.documentUrl,
statusCode: resource.response.statusCode,
url: resource.url,
statusCode: resource.statusCode,
url: resource.requestUrl,
body: result.item.body,
type: resource.type,
matchIndices,
Expand All @@ -64,7 +77,11 @@ export default class ResourceSearch {
}

public onTabCreated(event: HeroSession['EventTypes']['tab-created']): void {
this.searchIndexByTabId[event.tab.id] = new Fuse([], {
this.events.on(event.tab, 'resource', this.onTabResource.bind(this, event.tab.id));
}

public async onTabResource(tabId: number, resource: IResourceMeta): Promise<void> {
this.searchIndexByTabId[tabId] ??= new Fuse([], {
isCaseSensitive: false,
findAllMatches: true,
useExtendedSearch: true,
Expand All @@ -74,30 +91,14 @@ export default class ResourceSearch {
ignoreFieldNorm: true,
includeMatches: true,
});
this.events.on(event.tab, 'resource', this.onTabResource.bind(this, event.tab.id));
}

private async onTabResource(tabId: number, resource: IResourceMeta): Promise<void> {
const allowedResourceTypes = new Set<IResourceType>([
'Document',
'XHR',
'Fetch',
'Script',
'Websocket',
'Other',
]);

if (!resource.response?.statusCode) return;
if (!allowedResourceTypes.has(resource.type)) return;
if (!this.matchesFilter(resource.type, resource.response?.headers)) return;

const headers = resource.response?.headers ?? {};
const contentType = headers['content-type'] ?? headers['Content-Type'] ?? '';

if (resource.type === 'Other') {
if (!contentType.includes('text') && !contentType.includes('json')) return;
}
// search for terms
const body = await this.heroSession.db.resources.getResourceBodyById(resource.id, true);
const body = await this.db.resources.getResourceBodyById(resource.id, true);

let formattedBody = body.toString();
try {
Expand All @@ -112,4 +113,16 @@ export default class ResourceSearch {
url: resource.url,
});
}

public matchesFilter(resourceType: IResourceType, responseHeaders: IHttpHeaders = {}): boolean {
if (!ResourceSearch.allowedResourceTypes.has(resourceType)) return false;

if (resourceType === 'Other') {
const contentType = responseHeaders['content-type'] ?? responseHeaders['Content-Type'] ?? '';
if (!contentType.includes('text') && !contentType.includes('json')) {
return false;
}
}
return true;
}
}
5 changes: 3 additions & 2 deletions desktop/core/lib/SelectorRecommendations.ts
Expand Up @@ -30,10 +30,11 @@ export default class SelectorRecommendations {
const filename = this.getFilename();
const selectorMapsPath = `${configDirectory}/selectors/${filename}.json`;
let selectorMaps: ISavedSelectors = {};
if (!(await existsAsync(selectorMapsPath))) {
if (!(await existsAsync(Path.dirname(selectorMapsPath)))) {
Fs.mkdirSync(Path.dirname(selectorMapsPath));
} else {
selectorMaps = (await readFileAsJson<ISavedSelectors>(selectorMapsPath)) ?? {};
selectorMaps =
(await readFileAsJson<ISavedSelectors>(selectorMapsPath).catch(() => null)) ?? {};
}
selectorMaps[url] ??= {};
selectorMaps[url][map.nodePath] = map;
Expand Down

0 comments on commit 521d21f

Please sign in to comment.