Skip to content

Commit

Permalink
service: make TypeScript strict
Browse files Browse the repository at this point in the history
  • Loading branch information
throwaway96 committed Mar 5, 2024
1 parent 487db7c commit 341ee21
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 53 deletions.
18 changes: 14 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@babel/preset-env": "^7.23.5",
"@babel/preset-typescript": "^7.23.3",
"@types/bluebird": "^3.5.42",
"@types/node": "^20.10.4",
"@types/node-fetch": "^2.6.9",
"@types/progress-stream": "^2.0.5",
"@types/webos-service": "^0.4.6",
Expand Down
1 change: 1 addition & 0 deletions services/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fetch.Promise = Bluebird.Promise;
// Sadly these need to be manually typed according to
// https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node
// since types infered from Bluebird.Promise.promisify are wrong.
// @ts-ignore
export const asyncPipeline: (
...args: ReadonlyArray<NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream>
) => Promise<void> = Bluebird.Promise.promisify(pipeline);
Expand Down
4 changes: 2 additions & 2 deletions services/elevate-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { existsSync, statSync, readFileSync, writeFileSync } from 'fs';
import { execFile } from 'child_process';
import { dirname, resolve } from 'path';

process.env.PATH = `/usr/sbin:${process.env.PATH}`;
process.env['PATH'] = `/usr/sbin:${process.env['PATH']}`;

function isFile(path: string): boolean {
try {
Expand Down Expand Up @@ -40,7 +40,7 @@ function patchServiceFile(serviceFile: string): boolean {
serviceFileNew = serviceFileNew.replace(/^Exec=\/usr\/bin\/run-js-service/gm, `Exec=${runJsServicePath}`);
} else if (serviceFileNew.indexOf('/jailer') !== -1) {
console.info(`[ ] ${serviceFile} is a native service`);
serviceFileNew = serviceFileNew.replace(/^Exec=\/usr\/bin\/jailer .* ([^ ]*)$/gm, (match, binaryPath) => `Exec=${binaryPath}`);
serviceFileNew = serviceFileNew.replace(/^Exec=\/usr\/bin\/jailer .* ([^ ]*)$/gm, (_, binaryPath) => `Exec=${binaryPath}`);
} else if (serviceFileNew.indexOf('Exec=/media') === -1) {
// Ignore elevated native services...
console.info(`[~] ${serviceFile}: unknown service type, this may cause some troubles`);
Expand Down
12 changes: 6 additions & 6 deletions services/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
type Response<T extends Record<string, any>> = T & {
interface Response {
returnValue: true;
};
}

type ErrorResponse<T extends Record<string, any>> = T & {
interface ErrorResponse {
returnValue: false;
errorText: string;
};
}

export function makeSuccess<T>(payload: T): Response<T> {
export function makeSuccess(payload: Record<string, any>): Response {
return { returnValue: true, ...payload };
}

export function makeError<T>(error: string, payload?: T): ErrorResponse<T> {
export function makeError(error: string, payload?: Record<string, any>): ErrorResponse {
return { returnValue: false, errorText: error, ...payload };
}
96 changes: 58 additions & 38 deletions services/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ const homebrewBaseDir = ((): string | null => {
})();

// Maps internal setting field name with filesystem flag name.
type FlagName = string;
const availableFlags = {
telnetDisabled: 'webosbrew_telnet_disabled',
failsafe: 'webosbrew_failsafe',
sshdEnabled: 'webosbrew_sshd_enabled',
blockUpdates: 'webosbrew_block_updates',
} as Record<string, FlagName>;
} as const;
type FlagName = keyof typeof availableFlags;
type FlagFileName = (typeof availableFlags)[FlagName];

const runningAsRoot: boolean = (() => {
if (typeof process.getuid === 'undefined') {
Expand All @@ -55,10 +56,16 @@ const runningAsRoot: boolean = (() => {
return process.getuid() === 0;
})();

function assertNodeError(error: Error | unknown): asserts error is NodeJS.ErrnoException {
if (!(error instanceof Error)) {
throw error;
}
}

function asyncCall<T extends Record<string, any>>(srv: Service, uri: string, args: Record<string, any>): Promise<T> {
return new Promise((resolve, reject) => {
srv.call(uri, args, ({ payload }) => {
if (payload.returnValue) {
if (payload['returnValue']) {
resolve(payload as T);
} else {
reject(payload);
Expand All @@ -82,7 +89,7 @@ function createToast(message: string, service: Service, extras: Record<string, a
async function isFile(targetPath: string): Promise<boolean> {
try {
return (await asyncStat(targetPath)).isFile();
} catch (err) {
} catch (err: unknown) {
return false;
}
}
Expand Down Expand Up @@ -136,33 +143,34 @@ async function elevateService(pkg: string): Promise<boolean> {
/**
* Returns the file path for a flag.
*/
function flagPath(flag: FlagName): string {
return `/var/luna/preferences/${flag}`;
function flagFilePath(flagFile: FlagFileName): string {
return `/var/luna/preferences/${flagFile}`;
}

/**
* Returns whether a flag is set or not.
*/
async function flagRead(flag: FlagName): Promise<boolean> {
return asyncExists(flagPath(flag));
async function flagFileRead(flagFile: FlagFileName): Promise<boolean> {
return asyncExists(flagFilePath(flagFile));
}

/**
* Sets the value of a flag.
*/
async function flagSet(flag: FlagName, enabled: boolean): Promise<boolean> {
async function flagFileSet(flagFile: FlagFileName, enabled: boolean): Promise<boolean> {
if (enabled) {
// The file content is ignored, file presence is what matters. Writing '1' acts as a hint.
await asyncWriteFile(flagPath(flag), '1');
await asyncWriteFile(flagFilePath(flagFile), '1');
} else {
try {
await asyncUnlink(flagPath(flag));
} catch (err) {
await asyncUnlink(flagFilePath(flagFile));
} catch (err: unknown) {
assertNodeError(err);
// Already deleted is not a fatal error.
if (err.code !== 'ENOENT') throw err;
}
}
return flagRead(flag);
return flagFileRead(flagFile);
}

/**
Expand All @@ -177,7 +185,7 @@ async function packageInfo(filePath: string): Promise<Record<string, string>> {
.filter((m) => m.length)
.map((p) => [p.slice(0, p.indexOf(': ')), p.slice(p.indexOf(': ') + 2)]),
);
if (!resp.Package) {
if (!resp['Package']) {
throw new Error(`Invalid package info: ${JSON.stringify(resp)}`);
}
return resp;
Expand Down Expand Up @@ -315,16 +323,17 @@ function tryRespond<T extends Record<string, any>>(runner: (message: Message) =>
try {
const reply: T = await runner(message);
message.respond(makeSuccess(reply));
} catch (err) {
} catch (err: unknown) {
assertNodeError(err);
message.respond(makeError(err.message));
} finally {
message.cancel({});
}
};
}

function runService() {
const service = new Service(serviceInfo.id, null, { idleTimer: 30 });
function runService(): void {
const service = new Service(serviceInfo.id, undefined, { idleTimer: 30 });
const serviceRemote = new ServiceRemote();

function getInstallerService(): Service {
Expand Down Expand Up @@ -362,7 +371,7 @@ function runService() {
throw new Error(res.statusText);
}
const progressReporter = progress({
length: parseInt(res.headers.get('content-length'), 10),
length: parseInt(res.headers.get('content-length') ?? '0', 10),
time: 300 /* ms */,
});
progressReporter.on('progress', (p) => {
Expand All @@ -378,11 +387,12 @@ function runService() {
throw new Error(`Invalid file checksum (${payload.ipkHash} expected, got ${checksum}`);
}

let pkginfo: Record<string, string> = { Package: payload.id };
let pkginfo: Record<string, string | undefined> = { Package: payload.id };

try {
pkginfo = await packageInfo(targetPath);
} catch (err) {
} catch (err: unknown) {
assertNodeError(err);
await createToast(`Package info fetch failed: ${err.message}`, service);
}

Expand All @@ -399,7 +409,7 @@ function runService() {
// If reelevation fails for some reason the service should still be
// reelevated on reboot on devices with persistent autostart hooks (since
// we launch elevate-service in startup.sh script)
if (runningAsRoot && pkginfo && pkginfo.Package === kHomebrewChannelPackageId) {
if (runningAsRoot && pkginfo && pkginfo['Package'] === kHomebrewChannelPackageId) {
message.respond({ statusText: 'Self-update…' });
await createToast('Performing self-update...', service);

Expand All @@ -414,8 +424,8 @@ function runService() {

try {
const appInfo = await getAppInfo(installedPackageId);
await createToast(`Application installed: ${appInfo.title}`, service);
} catch (err) {
await createToast(`Application installed: ${appInfo['title']}`, service);
} catch (err: unknown) {
console.warn('appinfo fetch failed:', err);
await createToast(`Application installed: ${installedPackageId}`, service);
}
Expand All @@ -430,10 +440,12 @@ function runService() {
/**
* Removes existing package.
*/
type UninstallPayload = { id: string };
service.register(
'uninstall',
tryRespond(async (message: Message) => {
await removePackage(message.payload.id, getInstallerService());
const payload = message.payload as UninstallPayload;
await removePackage(payload.id, getInstallerService());
return { statusText: 'Finished.' };
}),
);
Expand All @@ -445,11 +457,11 @@ function runService() {
'getConfiguration',
tryRespond(async () => {
const futureFlags = Object.entries(availableFlags).map(
async ([field, flagName]) => [field, await flagRead(flagName)] as [string, boolean],
async ([flag, flagFile]) => [flag, await flagFileRead(flagFile)] as [FlagName, boolean],
);
const flags = Object.fromEntries(await Promise.all(futureFlags));
return {
root: process.getuid() === 0,
root: runningAsRoot,
homebrewBaseDir,
...flags,
};
Expand All @@ -462,12 +474,13 @@ function runService() {
type SetConfigurationPayload = Record<string, boolean>;
service.register(
'setConfiguration',
tryRespond(async (message) => {
tryRespond(async (message: Message) => {
const payload = message.payload as SetConfigurationPayload;
// TODO: Use destructuring again once it works with type predicates.
// See https://github.com/microsoft/TypeScript/issues/41173
const futureFlagSets = Object.entries(payload)
.map(([field, value]) => [field, availableFlags[field], value] as [string, FlagName | undefined, boolean])
.filter(([, flagName]) => flagName !== undefined)
.map(async ([field, flagName, value]) => [field, await flagSet(flagName, value)]);
.filter((pair: [string, boolean]): pair is [FlagName, boolean] => pair[0] in availableFlags)
.map(async ([flagName, value]) => [flagName, await flagFileSet(availableFlags[flagName], value)]);
return Object.fromEntries(await Promise.all(futureFlagSets));
}),
);
Expand Down Expand Up @@ -567,7 +580,8 @@ function runService() {
messages.push(`${startDevmode} has been manually modified!`);
}
}
} catch (err) {
} catch (err: unknown) {
assertNodeError(err);
console.log(`Startup script update failed: ${err.stack}`);
messages = ['Startup script update failed!', ...messages, `Error: ${err.toString()}`];
await createToast(messages.join('<br/>'), service);
Expand All @@ -590,7 +604,7 @@ function runService() {
type GetAppInfoPayload = { id: string };
service.register(
'getAppInfo',
tryRespond(async (message) => {
tryRespond(async (message: Message) => {
const payload = message.payload as GetAppInfoPayload;
const appId: string = payload.id;
if (!appId) throw new Error('missing `id` string field');
Expand Down Expand Up @@ -652,7 +666,7 @@ function runService() {
type GetDrmStatusPayload = { appId: string };
service.register(
'getDrmStatus',
tryRespond(async (message) => ({
tryRespond(async (message: Message) => ({
appId: (message.payload as GetDrmStatusPayload).appId,
drmType: 'NCG DRM',
installBasePath: '/media/cryptofs',
Expand All @@ -668,7 +682,7 @@ function runService() {

service.register(
'autostart',
tryRespond(async (message) => {
tryRespond(async (message: Message) => {
if (!runningAsRoot) {
return { message: 'Not running as root.', returnValue: true };
}
Expand Down Expand Up @@ -699,7 +713,7 @@ function runService() {
});

// Register activity if autostart was triggered in traditional way
if (message.payload?.reason !== 'activity') {
if (message.payload['reason'] !== 'activity') {
await registerActivity(service);
}

Expand Down Expand Up @@ -745,14 +759,20 @@ if (process.argv[2] === 'self-update') {
(async () => {
const service = new ServiceRemote() as Service;
try {
const packagePath = process.argv[3];
if (typeof packagePath !== 'string') {
throw new Error('missing package path');
}

await createToast('Performing self-update (inner)', service);
const installedPackageId = await installPackage(process.argv[3], service);
const installedPackageId = await installPackage(packagePath, service);
await createToast('Elevating...', service);
await elevateService(`${installedPackageId}.service`);
await createToast('Self-update finished!', service);
process.exit(0);
} catch (err) {
console.info(err);
} catch (err: unknown) {
console.error(err);
assertNodeError(err);
await createToast(`Self-update failed: ${err.message}`, service);
process.exit(1);
}
Expand Down
Loading

0 comments on commit 341ee21

Please sign in to comment.