Skip to content

Commit

Permalink
Final changes before study
Browse files Browse the repository at this point in the history
  • Loading branch information
nickbradley committed Jul 23, 2019
1 parent edfa8b9 commit 87ae1d6
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 102 deletions.
36 changes: 33 additions & 3 deletions src/backend/ContextModel.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
import { getManager } from "typeorm";
import { getManager, getRepository } from "typeorm";
import { Platform } from "../common/Platform";
import Log from "electron-log";
import { Application } from "./entities/Application";

export class ContextModel {
public project: string;
public projects: {[project: string]: {[attr: string]: any}};

constructor(projects: {[project: string]: {}}) {
Log.info(`ContextModel() - Initializing with projects: ${projects}`);
Log.info(`ContextModel() - Initializing with projects: ${JSON.stringify(projects)}`);
this.project = "";
this.projects = projects;
}


public async init() {
Log.info(`ContextModel::init() - Initializing context model: Adding host applications to database.`);
const applications = Platform.listApplications();
const appEntities: Application[] = [];

// TODO There's probably a less destructive way to do this
await getRepository(Application).clear();

for (const app of applications) {
const application = new Application();
application.identifier = app.id;
application.name = app.name;
application.icon = ""; // app.icon;
application.path = app.path;

const duplicates = appEntities.filter(e => e.identifier === application.identifier);
if (duplicates.length >= 1) {
Log.warn(`ContextModel::init() - Skipping duplicate application { id: ${application.identifier}, name: ${application.name} }`);
} else {
appEntities.push(application);
try {
await getRepository(Application).insert(application);
} catch (err) {
Log.warn(`ContextModel::init() - Failed to insert application { id: ${application.identifier}, name: ${application.name} }. ${err}`);
}
}
}
}

public async search(opts: {searchTerm: string, project: string}) {
Log.info(`ContextModel::search(..) - Searching for term '${opts.searchTerm}' in '${opts.project}' project.`);
if (opts.project && !Object.keys(this.projects).includes(opts.project)) {
Expand Down
85 changes: 23 additions & 62 deletions src/backend/Daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@ import { Database } from "./Database";
import { Server } from "./Server";
import { ChildProcess, spawn } from "child_process";
import * as path from "path";
// import { ContextModel } from "./ContextModel";
// import {ipcRenderer} from "electron";
import * as fs from "fs-extra";
import Log from "electron-log";
// import { Platform } from "../common/Platform";
// import { Application } from "./entities/Application";
// import { getRepository } from "typeorm";
import { ProjectWatcher } from "./ProjectWatcher";
import { ContextModel } from "./ContextModel";

export class Daemon {
public readonly restPort: number;
public readonly dbPath: string;
public readonly awPath: string;
public readonly projects: {[name: string]: any};
public readonly projectWatcher: ProjectWatcher;
public readonly contextModel: ContextModel;

private db: Database;
// private model: ContextModel;
private server: Server;
private windowWatcher!: ChildProcess;
private afkWatcher!: ChildProcess;
Expand All @@ -26,13 +25,6 @@ export class Daemon {
constructor() {
Log.info(`Daemon::init() - Initializing process.`);

// // TODO make this configurable from the env
// const projects = {
// "helm": {
// "root": "/Users/ncbrad/do/helm"
// }
// };

this.restPort = Number(process.env.PORT) || 5600;

if (!process.env.DB_PATH) {
Expand All @@ -44,10 +36,24 @@ export class Daemon {
throw new Error("Required env var AW_PATH is not set.");
}
this.awPath = process.env.AW_PATH;
// TODO make this configurable from the env
this.projects = {
helm: {
root: "/Users/Shared/helm",
},
kanboard: {
root: "/Users/studyparticipant/projects/kanboard",
},
teammates: {
root: "/Users/studyparticipant/project/teammates",
},
};

this.db = new Database(this.dbPath);
// this.model = new ContextModel(projects);
this.server = new Server("HelmWatcher");
this.server = new Server(this, "HelmWatcher");
// TODO ProjectWatcher can probably be merged with ContextModel...
this.projectWatcher = new ProjectWatcher(Object.keys(this.projects));
this.contextModel = new ContextModel(this.projects);

this.canCollectData = true;
}
Expand All @@ -60,33 +66,23 @@ export class Daemon {
Log.info(`Daemon::start() - Connecting to database.`);
await this.db.connect();

// Log.info(`Daemon::start() - Getting information about installed applications.`);
// await this.loadHostApplications();
await this.contextModel.init();

Log.info(`Daemon::start() - Starting ActivityWatch-compatible REST server.`);
await this.server.start(this.restPort);

Log.info(`Daemon::start() - Starting hosted watchers.`);
await this.startWatchers();

// ipcRenderer.on("search", async (event: any, args: any) => {
// const results = await this.model.search(args);
// ipcRenderer.sendTo(event.senderId, "search-results", results);
// });
}

public async stop() {
Log.info(`Daemon::stop() - Stopping hosted watchers.`);
// TODO Check if there was an error that already kill process (so we don't
// inadvertently kill some random process
// TODO Check if there was an error that already kill process (so we don't inadvertently kill some random process)
this.windowWatcher.kill("SIGINT");
this.afkWatcher.kill("SIGINT");

Log.info(`Daemon::stop() - Stopping REST server.`);
await this.server.stop();

// Log.info("Daemon::stop() - Removing event listeners.");
// ipcRenderer.removeAllListeners("search");
}

public pauseDataCollection() {
Expand Down Expand Up @@ -119,40 +115,5 @@ export class Daemon {
// Log.error(`aw-watcher-afk failed unexpectedly with code ${code}.`);
// });
};

// /* TODO Report this as a TypeORM bug...
// After considerable time I figured out why `getRepository(Application).insert(appEntities)` causes a seg fault: it freakin' escapes column names with double-quotes instead of single-quotes!
// Not sure why the sqlite3 doesn't catch this (or convert them).
//
// If you turn on logging and try to insert a single record you'll see the generate query is `INSERT INTO "application"("identifier", "name", "icon", "path") VALUES (?, ?, ?, ?) -- PARAMETERS: ["com.apple.Notes","Notes","","/Applications/Notes.app"]`
// You can reproduce using `getRepository(Application).query(`insert into "application"("identifier", "name", "icon", "path") values (?,?,?,?)`, [application.identifier, application.name, application.icon, application.path])`
// */
// private async loadHostApplications() {
// const applications = Platform.listApplications();
// const appEntities: Application[] = [];
// const appRepo = getRepository(Application);
//
// // Log.verbose(`loadHostApplications() - Clearing existing application data...`);
// // await appRepo.clear();
// // await appRepo.query(`delete from 'application'`);
//
//
// for (const app of applications) {
// const application = new Application();
// application.identifier = app.id;
// application.name = app.name;
// application.icon = app.icon;
// application.path = app.path;
//
// const duplicates = appEntities.filter(e => e.identifier === application.identifier);
// if (duplicates.length >= 1) {
// Log.warn(`loadHostApplications() - Skipping duplicate application. Inserted: ${JSON.stringify(duplicates[0])}; Duplicate: ${application}`);
// } else {
// Log.verbose(`loadingHostApplications() - Loading application data: ${application}`);
// await appRepo.query(`insert into 'application'('identifier', 'name', 'icon', 'path') values (?,?,?,?)`, [application.identifier, application.name, application.icon, application.path]);
// appEntities.push(application);
// }
// }
// }
}

4 changes: 3 additions & 1 deletion src/backend/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Tracker } from "./entities/Tracker";
import { ProjectSession } from "./entities/ProjectSession";
import Log from "electron-log";
import { ActiveProject} from "./entities/ActiveProject";
import { Usage } from "./entities/Usage";

export class Database {
public readonly path: string;
Expand All @@ -32,7 +33,8 @@ export class Database {
Shell,
Tracker,
Window,
ProjectSession
ProjectSession,
Usage,
],
"synchronize": true,
"logging": false,
Expand Down
11 changes: 1 addition & 10 deletions src/backend/Entry.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import Log from "electron-log";
import { Daemon } from "./Daemon";
// import * as path from "path";

(async () => {
const appName = "helmd";
// const home = process.env.HOME;
// if (!home) {
// console.error("<FATAL> HOME env var not set.");
// process.exit(1);
// }
// const appData = path.join(home!, "Library/Application Support");
// const userData = path.join(appData, appName);

let daemon: Daemon;

Log.transports.file.fileName = `${appName}.log`;
Expand All @@ -37,5 +28,5 @@ import { Daemon } from "./Daemon";
Log.error(`<FATAL> Failed to start the daemon process. ${err}`);
process.exit(2);
}
Log.info(`Helm daemon process (${process.title} [${process.pid}]) started successfully.`);
Log.info(`Helm daemon process started successfully: ${process.title} [${process.pid}]`);
})();
64 changes: 39 additions & 25 deletions src/backend/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,17 @@ import { Tracker } from "./entities/Tracker";
import { Window } from "./entities/Window";
import { ObjectLiteral } from "typeorm";
import Log from "electron-log";
import { ProjectWatcher } from "./ProjectWatcher";
import { ContextModel } from "./ContextModel";
import { Daemon } from "./Daemon";
import { Usage } from "./entities/Usage";

export class Server {
private readonly rest: restify.Server;
private readonly daemon: Daemon;

private projectWatcher: ProjectWatcher;
private contextModel: ContextModel;

constructor(name: string) {
constructor(daemon: Daemon, name: string) {
Log.info(`Server() - Creating REST server '${name}'`);
this.rest = restify.createServer({ name });
this.projectWatcher = new ProjectWatcher(["kanboard", "teammates", "helm"]);
this.contextModel = new ContextModel({
helm: {
root: "/Users/ncbrad/do/helm",
},
kanboard: {
root: "fakepath",
},
teammates: {
root: "jksdfsd",
},
});
this.daemon = daemon;
}

public async start(port: number) {
Expand Down Expand Up @@ -65,20 +52,47 @@ export class Server {
// });


// Launcher interaction endpoints
this.rest.post("/api/0/usage/launch", async (req: restify.Request, res: restify.Response, next: restify.Next) => {
Log.info(`POST /api/0/usage/launch - ${JSON.stringify(req.body)}`);
try {
await Usage.insert(req.body);
} catch (err) {
Log.error(`POST /api/0/usage/launch - ${err}`);
res.send(500);
}

return next();
});


// Artifact endpoints
this.rest.get("/api/0/artifacts", restify.plugins.queryParser(), async (req: restify.Request, res: restify.Response, next: restify.Next) => {
Log.info(`GET /api/0/artifacts - contains: ${req.query.contains}; project: ${req.query.project}`);
let contains = "";
let project = "";
try {
const contains = req.query.contains || "";
const project = req.query.project || this.projectWatcher.activeProject!.name;
contains = req.query.contains || "";
project = req.query.project || this.daemon.projectWatcher.activeProject!.name;

const results = await this.contextModel.search({ project, searchTerm: contains });
const results = await this.daemon.contextModel.search({ project, searchTerm: contains });
res.send(200, results);
} catch (err) {
Log.error();
res.send(400);
}

try {
const usage = {
created: new Date(),
kind: "search",
action: contains,
resource: project,
};
await Usage.insert(usage);
} catch (err) {
Log.warn("Failed to insert usage record");
}
return next();
});

Expand Down Expand Up @@ -277,20 +291,20 @@ export class Server {
case "web.tab.current":
// @ts-ignore
const url = (record as Browser).url.toLowerCase();
project = this.projectWatcher.extractProjectFromUrl(url);
project = this.daemon.projectWatcher.extractProjectFromUrl(url);
break;
case "shell.command":
// @ts-ignore
const cwd = (record as Shell).cwd.toLowerCase();
project = this.projectWatcher.extractProjectFromPath(cwd);
project = this.daemon.projectWatcher.extractProjectFromPath(cwd);
break;
case "app.editor.activity":
// @ts-ignore
project = (record as Editor).project.toLowerCase();
}

if (project && this.projectWatcher.projectNames.includes(project)) {
await this.projectWatcher.update(project, record.created, record.duration * 1000);
if (project && this.daemon.projectWatcher.projectNames.includes(project)) {
await this.daemon.projectWatcher.update(project, record.created, record.duration * 1000);
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/backend/entities/Usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class Usage extends BaseEntity {
@PrimaryGeneratedColumn()
id!: number;

@Column("datetime")
created: Date = new Date();

@Column("text")
kind: string = "";

@Column("text")
action: string = "";

@Column("text")
resource: string = "";
}
Loading

0 comments on commit 87ae1d6

Please sign in to comment.