Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/src/util/cmd.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { option } from "../index.js";
import * as option from "../option/index.js";
import { ClangID } from "../option/clang.js";

/**
Expand Down
159 changes: 1 addition & 158 deletions doc/api.md
Original file line number Diff line number Diff line change
@@ -1,158 +1 @@
```typescript
// ------------------------------------------------------------
// Internal API, not for plugin developers
// ------------------------------------------------------------
const ActionKind = ['skip', 'drop', 'abort', 'modify'] as const;

/**
* @type skip - skip this command, but execute the original command
* @type drop - drop this command, do not execute the original command
* @type abort - abort the whole execution, and return an error
* @type modify - modify this command, and execute the modified command
*/
type ActionType = (typeof ActionKind)[number];

type Action = {
// for modify
data?: CommandData;
type: ActionType;
};

const EventKind = ['finish', 'output'] as const;
type EventType = (typeof EventKind)[number];

type ExecutionEvent = {
// for output
stdout?: string;
stderr?: string;
code: number;
type: EventType;
};

type CatterRuntime = {
supportActions: ActionType[];
supportEvents: EventType[];
// eslogger: only in mac
// env: eg. CC=catter-proxy, then proxy report this cmd
type: 'inject' | 'eslogger' | 'env';
supportParentId: boolean;
};

/**
* @field scriptArgs - the arguments of this script
* @field scriptPath - the path of this script
* @field buildSystemCommand - the command to execute this script in build system, eg. ['bazel', 'build', '//:target']
* @field isScriptSupported - defaults to true, if false, catter will instantly abort the execution and return an error.
* @field runtime - the runtime environment of this script, can be used to determine which actions and events are supported
* @field options - the options of catter, can be used to enable some features of catter, eg. log
*/
type CatterConfig = {
scriptPath: string;
scriptArgs: string[];
buildSystemCommand: string[];
runtime: CatterRuntime;
options: {
log: boolean;
};
isScriptSupported: boolean;
};

type CatterErr = {
//...
};

/**
* @field parent - When supportParentId is true at runtime, this field is the ID of the parent command that generated this command; otherwise, this field is undefined.
* @field env - the environment variables of this command, in the format of ["KEY=VALUE", ...]
*/
type CommandData = {
cwd: string;
exe: string;
argv: string[];
env: string[];
runtime: CatterRuntime;
parent?: number;
};

export function service_on_start(cb: (config: CatterConfig) => CatterConfig): void;
export function service_on_finish(cb: () => void): void;
export function service_on_command(cb: (id: number, data: CommandData | CatterErr) => Action): void;
export function service_on_execution(cb: (id: number, event: ExecutionEvent) => void): void;


// ------------------------------------------------------------
// Plugin API, for plugin developers
// ------------------------------------------------------------

import { service_on_start, service_on_finish, service_on_command, service_on_execution } from 'catter-c';

/**
* @method onStart - called when catter start, can modify config
* @method onFinish - called when catter finish
* @method onCommand - called when a command being captured
* @param onCommand.id - a unique identifier for this command, can be used to correlate with onExecution
* @param onCommand.data - the data of this command, if there is an error during capturing, this will be a CatterErr object
* @method onExecution - called when a command being executed, can listen on its output and finish event.
* @param onExecution.id - the unique identifier for this command, same as onCommand
* @param onExecution.event - the event of this command, if there is an error during execution, the code field will be non-zero and stdout/stderr may be undefined
*/
interface CatterService {
onStart: (config: CatterConfig) => CatterConfig;
onFinish: () => void;
onCommand: (id: number, data: CommandData | CatterErr) => Action;
onExecution: (id: number, event: ExecutionEvent) => void;
}

export function onStart(cb: (config: CatterConfig) => CatterConfig): void{
service_on_start(cb);
}
export function onFinish(cb: () => void): void {
service_on_finish(cb);
}
export function onCommand(cb: (id: number, data: CommandData | CatterErr) => Action): void {
service_on_command(cb);
}
export function onExecution(cb: (id: number, event: ExecutionEvent) => void): void {
service_on_execution(cb);
}


export function register(service: CatterService) {
onStart(service.onStart);
onFinish(service.onFinish);
onCommand(service.onCommand);
onExecution(service.onExecution);
}

// ------------------------------------------------------------
// Example
// ------------------------------------------------------------
class MyCatterPlugin implements CatterService {
dataMap: Map<number, CommandData | CatterErr> = new Map();
eventMap: Map<number, ExecutionEvent> = new Map();
onStart(config: CatterConfig): CatterConfig {
// modify config
return config;
}

onFinish(): void {
for (const id of this.dataMap.keys()) {
const data = this.dataMap.get(id);
const event = this.eventMap.get(id);
console.log(`Command ${id} data:`, data);
console.log(`Command ${id} event:`, event);
}
}

onCommand(id: number, data: CommandData | CatterErr): Action {
this.dataMap.set(id, data);
return { type: 'skip' };
}

onExecution(id: number, event: ExecutionEvent): void {
this.eventMap.set(id, event);
}
}

register(new MyCatterPlugin());
```
TODO
6 changes: 3 additions & 3 deletions src/catter/core/apitool.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,20 @@ static R invoke_with_log(const std::string& args_s, CallArgs&&... call_args) {
try {
if constexpr(std::is_void_v<R>) {
V(std::forward<CallArgs>(call_args)...);
LOG_INFO("Invoke C API [{}]:\n -> args = {}\n -> ret = <void>",
LOG_INFO("Invoke C API `{}`:\n -> args = {}\n -> ret = <void>",
capi_name<V>(),
args_s);
return;
} else {
auto ret = V(std::forward<CallArgs>(call_args)...);
LOG_INFO("Invoke C API [{}]:\n -> args = {}\n -> ret = {}",
LOG_INFO("Invoke C API `{}`:\n -> args = {}\n -> ret = {}",
capi_name<V>(),
args_s,
serialize_value(ret));
return ret;
}
} catch(const std::exception& e) {
LOG_INFO("Invoke C API [{}]:\n -> args = {}\n -> throw = {}",
LOG_INFO("Invoke C API `{}`:\n -> args = {}\n -> throw = {}",
capi_name<V>(),
args_s,
e.what());
Expand Down
2 changes: 1 addition & 1 deletion src/catter/core/capi/type.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ T read_property(const qjs::Object& object, std::string_view property_name) {
if constexpr(is_optional_v<T>) {
using value_type = typename is_optional<T>::value_type;
auto optional_value = object.get_optional_property(std::string(property_name));
if(!optional_value.has_value() || optional_value->is_nothing()) {
if(!optional_value.has_value()) {
return std::nullopt;
}
return from_property_value<value_type>(*optional_value);
Expand Down
71 changes: 10 additions & 61 deletions src/catter/core/js.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,65 +33,6 @@ struct Self {

namespace {
Self self{};

std::optional<std::string> property_to_string(JSContext* ctx, JSValueConst value, const char* key) {
JSValue prop = JS_GetPropertyStr(ctx, value, key);
if(JS_IsException(prop)) {
JS_FreeValue(ctx, JS_GetException(ctx));
return std::nullopt;
}

std::optional<std::string> result = std::nullopt;
if(!JS_IsUndefined(prop) && !JS_IsNull(prop)) {
if(const char* str = JS_ToCString(ctx, prop)) {
result = str;
JS_FreeCString(ctx, str);
} else if(JS_HasException(ctx)) {
JS_FreeValue(ctx, JS_GetException(ctx));
}
}

JS_FreeValue(ctx, prop);
return result;
}

std::string stringify_value(JSContext* ctx, JSValueConst value) {
if(const char* str = JS_ToCString(ctx, value)) {
std::string result = str;
JS_FreeCString(ctx, str);
return result;
}

if(JS_HasException(ctx)) {
JS_FreeValue(ctx, JS_GetException(ctx));
}
return "<unprintable value>";
}

std::string format_rejection_reason(const qjs::Value& value) {
auto* ctx = value.context();
auto message = property_to_string(ctx, value.value(), "message");
auto stack = property_to_string(ctx, value.value(), "stack");

if(message.has_value() || stack.has_value()) {
std::string result;
if(message.has_value()) {
result += std::format("Error Message: {}\n", *message);
}
if(stack.has_value()) {
result += std::format("Stack Trace:\n{}\n", *stack);
}
return result;
}

return std::format("{}\n", stringify_value(ctx, value.value()));
}

void append_rejection_trace(std::string& error_trace, const qjs::Parameters& args) {
for(const auto& arg: args) {
error_trace += format_rejection_reason(arg);
}
}
} // namespace

CatterConfig on_start(CatterConfig config) {
Expand Down Expand Up @@ -164,7 +105,9 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) {

auto reject = CallBack::from(js_ctx, [&](qjs::Parameters args) {
state = Rejected;
append_rejection_trace(error_strace, args);
for(auto& arg: args) {
error_strace += arg.as<qjs::Error>().format() + "\n";
}
});

auto then_promise = promise_obj["then"].as<Then>().invoke(promise_obj,
Expand All @@ -173,7 +116,13 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) {

auto catch_fn = CallBack::from(js_ctx, [&](qjs::Parameters args) {
state = Rejected;
append_rejection_trace(error_strace, args);
try {
for(auto& arg: args) {
error_strace += arg.as<qjs::Error>().format() + "\n";
}
} catch(const std::exception& e) {
error_strace += std::format("Exception: {}\n", e.what());
}
});

then_promise["catch"].as<Catch>().invoke(then_promise, qjs::Object::from(catch_fn));
Expand Down
Loading
Loading