mirror of
https://github.com/5rahim/seanime
synced 2026-05-09 22:51:59 +02:00
async cmd
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
goja_bindings "seanime/internal/goja/goja_bindings"
|
||||
"seanime/internal/library/anime"
|
||||
"seanime/internal/plugin"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/5rahim/habari"
|
||||
@@ -38,6 +39,7 @@ func ShareBinds(vm *goja.Runtime, logger *zerolog.Logger) {
|
||||
|
||||
fm := goja_bindings.DefaultFieldMapper{}
|
||||
vm.SetFieldNameMapper(fm)
|
||||
goja.TagFieldNameMapper("json", true)
|
||||
|
||||
bindings := []struct {
|
||||
name string
|
||||
@@ -171,6 +173,25 @@ func ShareBinds(vm *goja.Runtime, logger *zerolog.Logger) {
|
||||
return anime.NewLocalFileWrapper(lfs)
|
||||
})
|
||||
vm.Set("$animeUtils", animeUtilsObj)
|
||||
|
||||
vm.Set("$waitGroup", func() *sync.WaitGroup {
|
||||
return &sync.WaitGroup{}
|
||||
})
|
||||
|
||||
// Run a function in a new goroutine
|
||||
// The Goja runtime is not thread safe, so nothing related to the VM should be done in this goroutine
|
||||
// You can use the $waitGroup to wait for multiple goroutines to finish
|
||||
// You can use $store to communicate with the main thread
|
||||
vm.Set("$unsafeGoroutine", func(fn func()) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Error().Err(fmt.Errorf("%v", r)).Msg("goroutine panic")
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// JSVMTypescriptToJS converts typescript to javascript
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/plugin"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -989,7 +990,7 @@ function init() {
|
||||
manager.PrintPluginPoolMetrics(opts.ID)
|
||||
}
|
||||
|
||||
// TestGojaPluginSystemDownloader tests the $downloader bindings in the Goja plugin system
|
||||
// TestGojaPluginSystemDownloader tests the ctx.downloader bindings in the Goja plugin system
|
||||
func TestGojaPluginSystemDownloader(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir := t.TempDir()
|
||||
@@ -1036,12 +1037,12 @@ func TestGojaPluginSystemDownloader(t *testing.T) {
|
||||
payload := `
|
||||
function init() {
|
||||
$ui.register((ctx) => {
|
||||
console.log("Testing $downloader bindings with large file");
|
||||
console.log("Testing ctx.downloader bindings with large file");
|
||||
|
||||
// Test download
|
||||
const downloadPath = "${TEMP_DIR}/large_download.bin";
|
||||
try {
|
||||
const downloadID = $downloader.download("${SERVER_URL}", downloadPath, {
|
||||
const downloadID = ctx.downloader.download("${SERVER_URL}", downloadPath, {
|
||||
timeout: 60 // 60 second timeout
|
||||
});
|
||||
console.log("Download started with ID:", downloadID);
|
||||
@@ -1052,7 +1053,7 @@ function init() {
|
||||
|
||||
// Wait for download to complete
|
||||
let downloadComplete = ctx.state(false);
|
||||
const cancelWatch = $downloader.watch(downloadID, (progress) => {
|
||||
const cancelWatch = ctx.downloader.watch(downloadID, (progress) => {
|
||||
// Store progress update
|
||||
progressUpdates.push({
|
||||
percentage: progress.percentage,
|
||||
@@ -1095,12 +1096,12 @@ function init() {
|
||||
}
|
||||
|
||||
// List downloads
|
||||
const downloads = $downloader.listDownloads();
|
||||
const downloads = ctx.downloader.listDownloads();
|
||||
console.log("Active downloads:", downloads.length);
|
||||
$store.set("downloadsCount", downloads.length);
|
||||
|
||||
// Get progress
|
||||
const progress = $downloader.getProgress(downloadID);
|
||||
const progress = ctx.downloader.getProgress(downloadID);
|
||||
if (progress) {
|
||||
console.log("Final download progress:", progress);
|
||||
$store.set("finalProgress", progress);
|
||||
@@ -1137,27 +1138,27 @@ function init() {
|
||||
},
|
||||
}
|
||||
|
||||
plugin, logger, manager, _, _, err := InitTestPlugin(t, opts)
|
||||
p, logger, manager, _, _, err := InitTestPlugin(t, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the plugin to execute and download to complete
|
||||
time.Sleep(12 * time.Second)
|
||||
|
||||
// Check the store values
|
||||
downloadID, ok := plugin.store.GetOk("downloadID")
|
||||
downloadID, ok := p.store.GetOk("downloadID")
|
||||
require.True(t, ok, "downloadID should be set in store")
|
||||
assert.NotEmpty(t, downloadID)
|
||||
|
||||
// Check if download completed or if there was an error
|
||||
downloadComplete, ok := plugin.store.GetOk("downloadComplete")
|
||||
downloadComplete, ok := p.store.GetOk("downloadComplete")
|
||||
if ok && downloadComplete.(bool) {
|
||||
// If download completed, check file size
|
||||
downloadedSize, ok := plugin.store.GetOk("downloadedSize")
|
||||
downloadedSize, ok := p.store.GetOk("downloadedSize")
|
||||
require.True(t, ok, "downloadedSize should be set in store")
|
||||
assert.Equal(t, int64(totalSize), downloadedSize)
|
||||
|
||||
// Check progress updates
|
||||
progressUpdates, ok := plugin.store.GetOk("progressUpdates")
|
||||
progressUpdates, ok := p.store.GetOk("progressUpdates")
|
||||
require.True(t, ok, "progressUpdates should be set in store")
|
||||
updates, ok := progressUpdates.([]interface{})
|
||||
require.True(t, ok, "progressUpdates should be a slice")
|
||||
@@ -1175,16 +1176,16 @@ function init() {
|
||||
}
|
||||
}
|
||||
|
||||
finalProgress, ok := plugin.store.GetOk("finalProgress")
|
||||
finalProgress, ok := p.store.GetOk("finalProgress")
|
||||
if ok {
|
||||
progressMap, ok := finalProgress.(map[string]interface{})
|
||||
require.True(t, ok, "finalProgress should be a map")
|
||||
assert.Equal(t, "completed", progressMap["status"])
|
||||
assert.InDelta(t, 100.0, progressMap["percentage"], 0.1)
|
||||
progressMap, ok := finalProgress.(*plugin.DownloadProgress)
|
||||
require.Truef(t, ok, "finalProgress should be a map, got %T", finalProgress)
|
||||
assert.Equal(t, "completed", progressMap.Status)
|
||||
assert.InDelta(t, 100.0, progressMap.Percentage, 0.1)
|
||||
}
|
||||
} else {
|
||||
// If download failed, check error
|
||||
downloadError, _ := plugin.store.GetOk("downloadError")
|
||||
downloadError, _ := p.store.GetOk("downloadError")
|
||||
t.Logf("Download error: %v", downloadError)
|
||||
// Don't fail the test if there was an error, just log it
|
||||
}
|
||||
@@ -1444,6 +1445,66 @@ function init() {
|
||||
console.log("Command execution error:", e.message);
|
||||
$store.set("commandError", e.message);
|
||||
}
|
||||
|
||||
// Test executing an async command
|
||||
//try {
|
||||
// // Create a command to list files
|
||||
// const asyncCmd = $osExtra.asyncCmd("ls", "-la", "${TEMP_DIR}");
|
||||
//
|
||||
//
|
||||
// asyncCmd.run((data, err, exitCode, signal) => {
|
||||
// // console.log(data, err, exitCode, signal)
|
||||
// if (data) {
|
||||
// console.log("Async command data:", $toString(data));
|
||||
// }
|
||||
// if (err) {
|
||||
// console.log("Async command error:", $toString(err));
|
||||
// }
|
||||
// if (exitCode) {
|
||||
// console.log("Async command exit code:", exitCode);
|
||||
// }
|
||||
// if (signal) {
|
||||
// console.log("Async command signal:", signal);
|
||||
// }
|
||||
// });
|
||||
//} catch (e) {
|
||||
// console.log("Command execution error:", e.message);
|
||||
// $store.set("asyncCommandError", e.message);
|
||||
//}
|
||||
|
||||
// Try unsafe goroutine
|
||||
try {
|
||||
// Create a command to list files
|
||||
const cmd = $os.cmd("ls", "-la", "${TEMP_DIR}");
|
||||
|
||||
$store.watch("unsafeGoroutineOutput", (output) => {
|
||||
console.log("Unsafe goroutine output:", output);
|
||||
});
|
||||
|
||||
// Read the output using scanner
|
||||
$unsafeGoroutine(function() {
|
||||
// Set up stdout capture
|
||||
const stdoutPipe = cmd.stdoutPipe();
|
||||
|
||||
console.log("Starting unsafe goroutine");
|
||||
const output = $io.readAll(stdoutPipe);
|
||||
$store.set("unsafeGoroutineOutput", $toString(output));
|
||||
console.log("Unsafe goroutine output set", $toString(output));
|
||||
|
||||
cmd.wait();
|
||||
});
|
||||
|
||||
// Start the command
|
||||
cmd.start();
|
||||
|
||||
// Check exit code
|
||||
const exitCode = cmd.processState.exitCode();
|
||||
console.log("Command exit code:", exitCode);
|
||||
|
||||
} catch (e) {
|
||||
console.log("Command execution error:", e.message);
|
||||
$store.set("unsafeGoroutineError", e.message);
|
||||
}
|
||||
|
||||
// Test executing a command with combined output
|
||||
try {
|
||||
@@ -1598,6 +1659,122 @@ function init() {
|
||||
manager.PrintPluginPoolMetrics(opts.ID)
|
||||
}
|
||||
|
||||
func TestGojaPluginSystemAsyncCommand(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create a test file to use with commands
|
||||
testFilePath := filepath.Join(tempDir, "test.txt")
|
||||
err := os.WriteFile(testFilePath, []byte("Hello, world!"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
payload := `
|
||||
function init() {
|
||||
$ui.register((ctx) => {
|
||||
console.log("Testing async command execution");
|
||||
|
||||
// Test executing an async command
|
||||
try {
|
||||
// Create a command to list files
|
||||
let asyncCmd = $osExtra.asyncCmd("ls", "-la", "${TEMP_DIR}");
|
||||
|
||||
let output = "";
|
||||
asyncCmd.run((data, err, exitCode, signal) => {
|
||||
// console.log(data, err, exitCode, signal)
|
||||
if (data) {
|
||||
// console.log("Async command data:", $toString(data));
|
||||
output += $toString(data) + "\n";
|
||||
$store.set("asyncCommandData", $toString(output));
|
||||
}
|
||||
if (err) {
|
||||
console.log("Async command error:", $toString(err));
|
||||
$store.set("asyncCommandError", $toString(err));
|
||||
}
|
||||
if (exitCode !== undefined) {
|
||||
console.log("output 1", output)
|
||||
console.log("Async command exit code:", exitCode);
|
||||
$store.set("asyncCommandExitCode", exitCode);
|
||||
console.log("Async command signal:", signal);
|
||||
$store.set("asyncCommandSignal", signal);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Running second command")
|
||||
|
||||
let asyncCmd2 = $osExtra.asyncCmd("ls", "-la", "${TEMP_DIR}");
|
||||
|
||||
let output2 = "";
|
||||
asyncCmd2.run((data, err, exitCode, signal) => {
|
||||
// console.log(data, err, exitCode, signal)
|
||||
if (data) {
|
||||
// console.log("Async command data:", $toString(data));
|
||||
output2 += $toString(data) + "\n";
|
||||
$store.set("asyncCommandData", $toString(output2));
|
||||
}
|
||||
if (err) {
|
||||
console.log("Async command error:", $toString(err));
|
||||
$store.set("asyncCommandError", $toString(err));
|
||||
}
|
||||
if (exitCode !== undefined) {
|
||||
console.log("output 2", output2)
|
||||
console.log("Async command exit code:", exitCode);
|
||||
$store.set("asyncCommandExitCode", exitCode);
|
||||
console.log("Async command signal:", signal);
|
||||
$store.set("asyncCommandSignal", signal);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.log("Command execution error:", e.message);
|
||||
$store.set("asyncCommandError", e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
`
|
||||
|
||||
// Replace placeholders with actual paths
|
||||
payload = strings.ReplaceAll(payload, "${TEMP_DIR}", tempDir)
|
||||
payload = strings.ReplaceAll(payload, "${TEST_FILE_PATH}", testFilePath)
|
||||
|
||||
opts := DefaultTestPluginOptions()
|
||||
opts.Payload = payload
|
||||
opts.Permissions = extension.PluginPermissions{
|
||||
Scopes: []extension.PluginPermissionScope{
|
||||
extension.PluginPermissionSystem,
|
||||
},
|
||||
Allow: extension.PluginAllowlist{
|
||||
ReadPaths: []string{tempDir + "/**/*"},
|
||||
WritePaths: []string{tempDir + "/**/*"},
|
||||
CommandScopes: []extension.CommandScope{
|
||||
{
|
||||
Command: "ls",
|
||||
Args: []extension.CommandArg{
|
||||
{Value: "-la"},
|
||||
{Validator: "$PATH"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin, _, manager, _, _, err := InitTestPlugin(t, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the plugin to execute
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Check the store values for the ls command
|
||||
asyncCommandData, ok := plugin.store.GetOk("asyncCommandData")
|
||||
require.True(t, ok, "asyncCommandData should be set in store")
|
||||
assert.Contains(t, asyncCommandData.(string), "test.txt", "Command output should contain the test file")
|
||||
|
||||
asyncCommandExitCode, ok := plugin.store.GetOk("asyncCommandExitCode")
|
||||
require.True(t, ok, "asyncCommandExitCode should be set in store")
|
||||
assert.Equal(t, int64(0), asyncCommandExitCode, "Command exit code should be 0")
|
||||
|
||||
manager.PrintPluginPoolMetrics(opts.ID)
|
||||
}
|
||||
|
||||
// TestGojaPluginSystemUnzip tests the unzip functionality in the osExtra module
|
||||
func TestGojaPluginSystemUnzip(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
|
||||
@@ -283,8 +283,6 @@ declare namespace $ui {
|
||||
*/
|
||||
playNextEpisode(): void
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
interface PlaybackEvent {
|
||||
@@ -1239,4 +1237,4 @@ declare namespace $habari {
|
||||
* @returns The metadata
|
||||
*/
|
||||
function parse(filename: string): Metadata
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ declare namespace $os {
|
||||
* @param args The arguments to pass to the command
|
||||
* @returns A command object or an error if the command is not authorized
|
||||
*/
|
||||
function cmd(name: string, ...args: string[]): $os.Cmd;
|
||||
function cmd(name: string, ...args: string[]): $os.Cmd
|
||||
|
||||
/**
|
||||
* Reads the entire file specified by path.
|
||||
@@ -24,7 +24,7 @@ declare namespace $os {
|
||||
* @returns The file contents as a byte array
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function readFile(path: string): Uint8Array;
|
||||
function readFile(path: string): Uint8Array
|
||||
|
||||
/**
|
||||
* Writes data to the named file, creating it if necessary.
|
||||
@@ -34,7 +34,7 @@ declare namespace $os {
|
||||
* @param perm The file mode (permissions)
|
||||
* @throws Error if the path is not authorized for writing
|
||||
*/
|
||||
function writeFile(path: string, data: Uint8Array, perm: number): void;
|
||||
function writeFile(path: string, data: Uint8Array, perm: number): void
|
||||
|
||||
/**
|
||||
* Reads a directory, returning a list of directory entries.
|
||||
@@ -42,35 +42,35 @@ declare namespace $os {
|
||||
* @returns An array of directory entries
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function readDir(path: string): $os.DirEntry[];
|
||||
function readDir(path: string): $os.DirEntry[]
|
||||
|
||||
/**
|
||||
* Returns the default directory to use for temporary files.
|
||||
* @returns The temporary directory path or empty string if not authorized
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function tempDir(): string;
|
||||
function tempDir(): string
|
||||
|
||||
/**
|
||||
* Returns the user's configuration directory.
|
||||
* @returns The configuration directory path or empty string if not authorized
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function configDir(): string;
|
||||
function configDir(): string
|
||||
|
||||
/**
|
||||
* Returns the user's home directory.
|
||||
* @returns The home directory path or empty string if not authorized
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function homeDir(): string;
|
||||
function homeDir(): string
|
||||
|
||||
/**
|
||||
* Returns the user's cache directory.
|
||||
* @returns The cache directory path or empty string if not authorized
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function cacheDir(): string;
|
||||
function cacheDir(): string
|
||||
|
||||
/**
|
||||
* Changes the size of the named file.
|
||||
@@ -78,7 +78,7 @@ declare namespace $os {
|
||||
* @param size The new size of the file
|
||||
* @throws Error if the path is not authorized for writing
|
||||
*/
|
||||
function truncate(path: string, size: number): void;
|
||||
function truncate(path: string, size: number): void
|
||||
|
||||
/**
|
||||
* Creates a new directory with the specified name and permission bits.
|
||||
@@ -86,7 +86,7 @@ declare namespace $os {
|
||||
* @param perm The permission bits
|
||||
* @throws Error if the path is not authorized for writing
|
||||
*/
|
||||
function mkdir(path: string, perm: number): void;
|
||||
function mkdir(path: string, perm: number): void
|
||||
|
||||
/**
|
||||
* Creates a directory named path, along with any necessary parents.
|
||||
@@ -94,7 +94,7 @@ declare namespace $os {
|
||||
* @param perm The permission bits
|
||||
* @throws Error if the path is not authorized for writing
|
||||
*/
|
||||
function mkdirAll(path: string, perm: number): void;
|
||||
function mkdirAll(path: string, perm: number): void
|
||||
|
||||
/**
|
||||
* Renames (moves) oldpath to newpath.
|
||||
@@ -102,21 +102,21 @@ declare namespace $os {
|
||||
* @param newpath The destination path
|
||||
* @throws Error if either path is not authorized for writing
|
||||
*/
|
||||
function rename(oldpath: string, newpath: string): void;
|
||||
function rename(oldpath: string, newpath: string): void
|
||||
|
||||
/**
|
||||
* Removes the named file or (empty) directory.
|
||||
* @param path The path to remove
|
||||
* @throws Error if the path is not authorized for writing
|
||||
*/
|
||||
function remove(path: string): void;
|
||||
function remove(path: string): void
|
||||
|
||||
/**
|
||||
* Removes path and any children it contains.
|
||||
* @param path The path to remove recursively
|
||||
* @throws Error if the path is not authorized for writing
|
||||
*/
|
||||
function removeAll(path: string): void;
|
||||
function removeAll(path: string): void
|
||||
|
||||
/**
|
||||
* Returns a FileInfo describing the named file.
|
||||
@@ -124,7 +124,7 @@ declare namespace $os {
|
||||
* @returns Information about the file
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function stat(path: string): $os.FileInfo;
|
||||
function stat(path: string): $os.FileInfo
|
||||
|
||||
/**
|
||||
* Opens a file for reading and writing.
|
||||
@@ -133,36 +133,54 @@ declare namespace $os {
|
||||
* @param perm The file mode (permissions)
|
||||
* @returns A file object or an error if the file is not authorized for writing
|
||||
*/
|
||||
function openFile(path: string, flag: number, perm: number): $os.File;
|
||||
function openFile(path: string, flag: number, perm: number): $os.File
|
||||
|
||||
|
||||
interface File {
|
||||
chmod(mode: $os.FileMode): void;
|
||||
chown(uid: number, gid: number): void;
|
||||
close(): void;
|
||||
fd(): number;
|
||||
name(): string;
|
||||
read(b: Uint8Array): number;
|
||||
readAt(b: Uint8Array, off: number): number;
|
||||
chmod(mode: $os.FileMode): void
|
||||
|
||||
readDir(n: number): $os.DirEntry[];
|
||||
readFrom(r: io.Reader): number;
|
||||
chown(uid: number, gid: number): void
|
||||
|
||||
readdir(n: number): $os.FileInfo[];
|
||||
readdirnames(n: number): string[];
|
||||
seek(offset: number, whence: number): number;
|
||||
setDeadline(t: Date): void;
|
||||
setReadDeadline(t: Date): void;
|
||||
setWriteDeadline(t: Date): void;
|
||||
close(): void
|
||||
|
||||
stat(): $os.FileInfo;
|
||||
sync(): void;
|
||||
syscallConn(): any; /* Not documented */
|
||||
truncate(size: number): void;
|
||||
write(b: Uint8Array): number;
|
||||
writeAt(b: Uint8Array, off: number): number;
|
||||
writeString(s: string): number;
|
||||
writeTo(w: io.Writer): number;
|
||||
fd(): number
|
||||
|
||||
name(): string
|
||||
|
||||
read(b: Uint8Array): number
|
||||
|
||||
readAt(b: Uint8Array, off: number): number
|
||||
|
||||
readDir(n: number): $os.DirEntry[]
|
||||
|
||||
readFrom(r: io.Reader): number
|
||||
|
||||
readdir(n: number): $os.FileInfo[]
|
||||
|
||||
readdirnames(n: number): string[]
|
||||
|
||||
seek(offset: number, whence: number): number
|
||||
|
||||
setDeadline(t: Date): void
|
||||
|
||||
setReadDeadline(t: Date): void
|
||||
|
||||
setWriteDeadline(t: Date): void
|
||||
|
||||
stat(): $os.FileInfo
|
||||
|
||||
sync(): void
|
||||
|
||||
syscallConn(): any /* Not documented */
|
||||
truncate(size: number): void
|
||||
|
||||
write(b: Uint8Array): number
|
||||
|
||||
writeAt(b: Uint8Array, off: number): number
|
||||
|
||||
writeString(s: string): number
|
||||
|
||||
writeTo(w: io.Writer): number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,128 +193,179 @@ declare namespace $os {
|
||||
* If the Args field is empty or nil, Run uses {Path}.
|
||||
* In typical use, both Path and Args are set by calling Command.
|
||||
*/
|
||||
args: string[];
|
||||
args: string[]
|
||||
|
||||
/**
|
||||
* If Cancel is non-nil, the command must have been created with CommandContext
|
||||
* and Cancel will be called when the command's Context is done.
|
||||
*/
|
||||
cancel: (() => void);
|
||||
cancel: () => void
|
||||
|
||||
/**
|
||||
* Dir specifies the working directory of the command.
|
||||
* If Dir is the empty string, Run runs the command in the calling process's current directory.
|
||||
*/
|
||||
dir: string;
|
||||
dir: string
|
||||
|
||||
/**
|
||||
* Env specifies the environment of the process.
|
||||
* Each entry is of the form "key=value".
|
||||
* If Env is nil, the new process uses the current process's environment.
|
||||
*/
|
||||
env: string[];
|
||||
env: string[]
|
||||
|
||||
/** Error information if the command failed */
|
||||
err: Error;
|
||||
err: Error
|
||||
|
||||
/**
|
||||
* ExtraFiles specifies additional open files to be inherited by the new process.
|
||||
* It does not include standard input, standard output, or standard error.
|
||||
*/
|
||||
extraFiles: any[];
|
||||
extraFiles: $os.File[]
|
||||
|
||||
/**
|
||||
* Path is the path of the command to run.
|
||||
* This is the only field that must be set to a non-zero value.
|
||||
*/
|
||||
path: string;
|
||||
path: string
|
||||
|
||||
/** Process is the underlying process, once started. */
|
||||
process?: any;
|
||||
process?: $os.Process
|
||||
|
||||
/** ProcessState contains information about an exited process. */
|
||||
processState?: any;
|
||||
processState?: $os.ProcessState
|
||||
|
||||
/** Standard error of the command */
|
||||
stderr: any;
|
||||
stderr: any
|
||||
|
||||
/** Standard input of the command */
|
||||
stdin: any;
|
||||
stdin: any
|
||||
|
||||
/** Standard output of the command */
|
||||
stdout: any;
|
||||
stdout: any
|
||||
|
||||
/** SysProcAttr holds optional, operating system-specific attributes. */
|
||||
sysProcAttr?: any;
|
||||
sysProcAttr?: any
|
||||
|
||||
/**
|
||||
* If WaitDelay is non-zero, it bounds the time spent waiting on two sources of
|
||||
* unexpected delay in Wait: a child process that fails to exit after the associated
|
||||
* Context is canceled, and a child process that exits but leaves its I/O pipes unclosed.
|
||||
*/
|
||||
waitDelay: number;
|
||||
waitDelay: number
|
||||
|
||||
/**
|
||||
* CombinedOutput runs the command and returns its combined standard output and standard error.
|
||||
* @returns The combined output as a string or byte array
|
||||
*/
|
||||
combinedOutput(): string | number[];
|
||||
combinedOutput(): string | number[]
|
||||
|
||||
/**
|
||||
* Environ returns a copy of the environment in which the command would be run as it is currently configured.
|
||||
* @returns The environment variables as an array of strings
|
||||
*/
|
||||
environ(): string[];
|
||||
environ(): string[]
|
||||
|
||||
/**
|
||||
* Output runs the command and returns its standard output.
|
||||
* @returns The standard output as a string or byte array
|
||||
*/
|
||||
output(): string | number[];
|
||||
output(): string | number[]
|
||||
|
||||
/**
|
||||
* Run starts the specified command and waits for it to complete.
|
||||
* The returned error is nil if the command runs, has no problems copying stdin, stdout,
|
||||
* and stderr, and exits with a zero exit status.
|
||||
*/
|
||||
run(): void;
|
||||
run(): void
|
||||
|
||||
/**
|
||||
* Start starts the specified command but does not wait for it to complete.
|
||||
* If Start returns successfully, the c.Process field will be set.
|
||||
*/
|
||||
start(): void;
|
||||
start(): void
|
||||
|
||||
/**
|
||||
* StderrPipe returns a pipe that will be connected to the command's standard error when the command starts.
|
||||
* @returns A readable stream for the command's standard error
|
||||
*/
|
||||
stderrPipe(): any;
|
||||
stderrPipe(): any
|
||||
|
||||
/**
|
||||
* StdinPipe returns a pipe that will be connected to the command's standard input when the command starts.
|
||||
* @returns A writable stream for the command's standard input
|
||||
*/
|
||||
stdinPipe(): any;
|
||||
stdinPipe(): any
|
||||
|
||||
/**
|
||||
* StdoutPipe returns a pipe that will be connected to the command's standard output when the command starts.
|
||||
* @returns A readable stream for the command's standard output
|
||||
*/
|
||||
stdoutPipe(): any;
|
||||
stdoutPipe(): any
|
||||
|
||||
/**
|
||||
* String returns a human-readable description of the command.
|
||||
* It is intended only for debugging.
|
||||
* @returns A string representation of the command
|
||||
*/
|
||||
string(): string;
|
||||
string(): string
|
||||
|
||||
/**
|
||||
* Wait waits for the command to exit and waits for any copying to stdin or copying from stdout or stderr to complete.
|
||||
* The command must have been started by Start.
|
||||
*/
|
||||
wait(): void;
|
||||
wait(): void
|
||||
}
|
||||
|
||||
interface Process {
|
||||
kill(): void
|
||||
|
||||
wait(): void
|
||||
|
||||
signal(sig: Signal): void
|
||||
}
|
||||
|
||||
interface Signal {
|
||||
string(): string
|
||||
|
||||
signal(): void
|
||||
}
|
||||
|
||||
const Kill: Signal
|
||||
const Interrupt: Signal
|
||||
|
||||
interface ProcessState {
|
||||
pid(): number
|
||||
|
||||
string(): string
|
||||
|
||||
exitCode(): number
|
||||
}
|
||||
|
||||
/**
|
||||
* AsyncCmd represents an external command being prepared or run asynchronously.
|
||||
*/
|
||||
interface AsyncCmd {
|
||||
/**
|
||||
* Get the underlying $os.Cmd.
|
||||
* To start the command, call start() on the underlying command, not run().
|
||||
* @returns The underlying $os.Cmd
|
||||
*/
|
||||
getCommand(): $os.Cmd
|
||||
|
||||
/**
|
||||
* Run the command
|
||||
* @param callback The callback to call each time data is available from the command's stdout or stderr
|
||||
* @param data The data from the command's stdout
|
||||
* @param err The data from the command's stderr
|
||||
* @param exitCode The exit code of the command
|
||||
* @param signal The signal that terminated the command
|
||||
*/
|
||||
run(callback: (data: Uint8Array | undefined,
|
||||
err: Uint8Array | undefined,
|
||||
exitCode: number | undefined,
|
||||
signal: string | undefined,
|
||||
) => void): void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,22 +373,22 @@ declare namespace $os {
|
||||
*/
|
||||
interface FileInfo {
|
||||
/** Base name of the file */
|
||||
name(): string;
|
||||
name(): string
|
||||
|
||||
/** Length in bytes for regular files; system-dependent for others */
|
||||
size(): number;
|
||||
/** Length in bytes for regular files system-dependent for others */
|
||||
size(): number
|
||||
|
||||
/** File mode bits */
|
||||
mode(): $os.FileMode;
|
||||
mode(): $os.FileMode
|
||||
|
||||
/** Modification time */
|
||||
modTime(): Date;
|
||||
modTime(): Date
|
||||
|
||||
/** Abbreviation for mode().isDir() */
|
||||
isDir(): boolean;
|
||||
isDir(): boolean
|
||||
|
||||
/** Underlying data source (can return null) */
|
||||
sys(): any;
|
||||
sys(): any
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,22 +396,22 @@ declare namespace $os {
|
||||
*/
|
||||
interface DirEntry {
|
||||
/** Returns the name of the file (or subdirectory) described by the entry */
|
||||
name(): string;
|
||||
name(): string
|
||||
|
||||
/** Reports whether the entry describes a directory */
|
||||
isDir(): boolean;
|
||||
isDir(): boolean
|
||||
|
||||
/** Returns the type bits for the entry */
|
||||
type(): $os.FileMode;
|
||||
type(): $os.FileMode
|
||||
|
||||
/** Returns the FileInfo for the file or subdirectory described by the entry */
|
||||
info(): $os.FileInfo;
|
||||
info(): $os.FileInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* FileMode represents a file's mode and permission bits.
|
||||
*/
|
||||
type FileMode = number;
|
||||
type FileMode = number
|
||||
|
||||
/**
|
||||
* Constants for file mode bits
|
||||
@@ -407,35 +476,35 @@ declare namespace $filepath {
|
||||
* @param path The path to get the base name from
|
||||
* @returns The base name of the path
|
||||
*/
|
||||
function base(path: string): string;
|
||||
function base(path: string): string
|
||||
|
||||
/**
|
||||
* Cleans the path by applying a series of rules.
|
||||
* @param path The path to clean
|
||||
* @returns The cleaned path
|
||||
*/
|
||||
function clean(path: string): string;
|
||||
function clean(path: string): string
|
||||
|
||||
/**
|
||||
* Returns all but the last element of path.
|
||||
* @param path The path to get the directory from
|
||||
* @returns The directory containing the file
|
||||
*/
|
||||
function dir(path: string): string;
|
||||
function dir(path: string): string
|
||||
|
||||
/**
|
||||
* Returns the file extension of path.
|
||||
* @param path The path to get the extension from
|
||||
* @returns The file extension (including the dot)
|
||||
*/
|
||||
function ext(path: string): string;
|
||||
function ext(path: string): string
|
||||
|
||||
/**
|
||||
* Converts path from slash-separated to OS-specific separator.
|
||||
* @param path The path to convert
|
||||
* @returns The path with OS-specific separators
|
||||
*/
|
||||
function fromSlash(path: string): string;
|
||||
function fromSlash(path: string): string
|
||||
|
||||
/**
|
||||
* Returns a list of files matching the pattern in the base directory.
|
||||
@@ -444,21 +513,21 @@ declare namespace $filepath {
|
||||
* @returns An array of matching file paths
|
||||
* @throws Error if the base path is not authorized for reading
|
||||
*/
|
||||
function glob(basePath: string, pattern: string): string[];
|
||||
function glob(basePath: string, pattern: string): string[]
|
||||
|
||||
/**
|
||||
* Reports whether the path is absolute.
|
||||
* @param path The path to check
|
||||
* @returns True if the path is absolute
|
||||
*/
|
||||
function isAbs(path: string): boolean;
|
||||
function isAbs(path: string): boolean
|
||||
|
||||
/**
|
||||
* Joins any number of path elements into a single path.
|
||||
* @param paths The path elements to join
|
||||
* @returns The joined path
|
||||
*/
|
||||
function join(...paths: string[]): string;
|
||||
function join(...paths: string[]): string
|
||||
|
||||
/**
|
||||
* Reports whether name matches the shell pattern.
|
||||
@@ -466,7 +535,7 @@ declare namespace $filepath {
|
||||
* @param name The string to check
|
||||
* @returns True if name matches pattern
|
||||
*/
|
||||
function match(pattern: string, name: string): boolean;
|
||||
function match(pattern: string, name: string): boolean
|
||||
|
||||
/**
|
||||
* Returns the relative path from basepath to targpath.
|
||||
@@ -474,28 +543,28 @@ declare namespace $filepath {
|
||||
* @param targpath The target path
|
||||
* @returns The relative path
|
||||
*/
|
||||
function rel(basepath: string, targpath: string): string;
|
||||
function rel(basepath: string, targpath: string): string
|
||||
|
||||
/**
|
||||
* Splits path into directory and file components.
|
||||
* @param path The path to split
|
||||
* @returns An array with [directory, file]
|
||||
*/
|
||||
function split(path: string): [string, string];
|
||||
function split(path: string): [string, string]
|
||||
|
||||
/**
|
||||
* Splits a list of paths joined by the OS-specific ListSeparator.
|
||||
* @param path The path list to split
|
||||
* @returns An array of paths
|
||||
*/
|
||||
function splitList(path: string): string[];
|
||||
function splitList(path: string): string[]
|
||||
|
||||
/**
|
||||
* Converts path from OS-specific separator to slash-separated.
|
||||
* @param path The path to convert
|
||||
* @returns The path with forward slashes
|
||||
*/
|
||||
function toSlash(path: string): string;
|
||||
function toSlash(path: string): string
|
||||
|
||||
/**
|
||||
* Walks the file tree rooted at the specificed path, calling walkFn for each file or directory.
|
||||
@@ -504,7 +573,7 @@ declare namespace $filepath {
|
||||
* @param walkFn The function to call for each file or directory
|
||||
* @throws Error if the root path is not authorized for reading
|
||||
*/
|
||||
function walk(root: string, walkFn: (path: string, info: $os.FileInfo, err: any | null | undefined) => any): void;
|
||||
function walk(root: string, walkFn: (path: string, info: $os.FileInfo, err: any | null | undefined) => any): void
|
||||
|
||||
/**
|
||||
* Walks the file tree rooted at the specificed path, calling walkDirFn for each file or directory.
|
||||
@@ -512,7 +581,7 @@ declare namespace $filepath {
|
||||
* @param walkDirFn The function to call for each file or directory
|
||||
* @throws Error if the root path is not authorized for reading
|
||||
*/
|
||||
function walkDir(root: string, walkDirFn: (path: string, d: $os.DirEntry, err: any | null | undefined) => any): void;
|
||||
function walkDir(root: string, walkDirFn: (path: string, d: $os.DirEntry, err: any | null | undefined) => any): void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,7 +594,7 @@ declare namespace $osExtra {
|
||||
* @param dest The destination directory
|
||||
* @throws Error if either path is not authorized for writing
|
||||
*/
|
||||
function unwrapAndMove(src: string, dest: string): void;
|
||||
function unwrapAndMove(src: string, dest: string): void
|
||||
|
||||
/**
|
||||
* Extracts a ZIP archive to the destination directory.
|
||||
@@ -533,7 +602,7 @@ declare namespace $osExtra {
|
||||
* @param dest The destination directory
|
||||
* @throws Error if either path is not authorized for writing
|
||||
*/
|
||||
function unzip(src: string, dest: string): void;
|
||||
function unzip(src: string, dest: string): void
|
||||
|
||||
/**
|
||||
* Extracts a RAR archive to the destination directory.
|
||||
@@ -541,28 +610,37 @@ declare namespace $osExtra {
|
||||
* @param dest The destination directory
|
||||
* @throws Error if either path is not authorized for writing
|
||||
*/
|
||||
function unrar(src: string, dest: string): void;
|
||||
function unrar(src: string, dest: string): void
|
||||
|
||||
/**
|
||||
* Returns the user's desktop directory.
|
||||
* @returns The desktop directory path or empty string if not authorized
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function desktopDir(): string;
|
||||
function desktopDir(): string
|
||||
|
||||
/**
|
||||
* Returns the user's documents directory.
|
||||
* @returns The documents directory path or empty string if not authorized
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function documentsDir(): string;
|
||||
function documentsDir(): string
|
||||
|
||||
/**
|
||||
* Returns the user's downloads directory.
|
||||
* @returns The downloads directory path or empty string if not authorized
|
||||
* @throws Error if the path is not authorized for reading
|
||||
*/
|
||||
function downloadDir(): string;
|
||||
function downloadDir(): string
|
||||
|
||||
/**
|
||||
* Creates a new AsyncCmd instance.
|
||||
* Get the underlying $os.Cmd with getCommand().
|
||||
* @param name The name of the command to execute
|
||||
* @param arg The arguments to pass to the command
|
||||
* @returns A new AsyncCmd instance
|
||||
*/
|
||||
function asyncCmd(name: string, arg: string[]): $os.AsyncCmd
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -579,27 +657,27 @@ declare namespace $downloader {
|
||||
*/
|
||||
interface DownloadProgress {
|
||||
/** Unique download identifier */
|
||||
id: string;
|
||||
id: string
|
||||
/** Source URL */
|
||||
url: string;
|
||||
url: string
|
||||
/** Destination file path */
|
||||
destination: string;
|
||||
destination: string
|
||||
/** Number of bytes downloaded so far */
|
||||
totalBytes: number;
|
||||
totalBytes: number
|
||||
/** Total file size in bytes (if known) */
|
||||
totalSize: number;
|
||||
totalSize: number
|
||||
/** Download speed in bytes per second */
|
||||
speed: number;
|
||||
speed: number
|
||||
/** Download completion percentage (0-100) */
|
||||
percentage: number;
|
||||
percentage: number
|
||||
/** Current download status */
|
||||
status: DownloadStatus;
|
||||
status: DownloadStatus
|
||||
/** Error message if status is ERROR */
|
||||
error?: string;
|
||||
error?: string
|
||||
/** Time of the last progress update */
|
||||
lastUpdate: Date;
|
||||
lastUpdate: Date
|
||||
/** Time when the download started */
|
||||
startTime: Date;
|
||||
startTime: Date
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -607,9 +685,9 @@ declare namespace $downloader {
|
||||
*/
|
||||
interface DownloadOptions {
|
||||
/** Timeout in seconds */
|
||||
timeout?: number;
|
||||
timeout?: number
|
||||
/** HTTP headers to send with the request */
|
||||
headers?: Record<string, string>;
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -620,7 +698,7 @@ declare namespace $downloader {
|
||||
* @returns A unique download ID
|
||||
* @throws Error if the destination path is not authorized for writing
|
||||
*/
|
||||
function download(url: string, destination: string, options?: DownloadOptions): string;
|
||||
function download(url: string, destination: string, options?: DownloadOptions): string
|
||||
|
||||
/**
|
||||
* Watches a download for progress updates.
|
||||
@@ -628,33 +706,33 @@ declare namespace $downloader {
|
||||
* @param callback Function to call with progress updates
|
||||
* @returns A function to cancel the watch
|
||||
*/
|
||||
function watch(downloadId: string, callback: (progress: DownloadProgress | undefined) => void): () => void;
|
||||
function watch(downloadId: string, callback: (progress: DownloadProgress | undefined) => void): () => void
|
||||
|
||||
/**
|
||||
* Gets the current progress of a download.
|
||||
* @param downloadId The download ID to check
|
||||
* @returns The current download progress
|
||||
*/
|
||||
function getProgress(downloadId: string): DownloadProgress | undefined;
|
||||
function getProgress(downloadId: string): DownloadProgress | undefined
|
||||
|
||||
/**
|
||||
* Lists all active downloads.
|
||||
* @returns An array of download progress objects
|
||||
*/
|
||||
function listDownloads(): DownloadProgress[];
|
||||
function listDownloads(): DownloadProgress[]
|
||||
|
||||
/**
|
||||
* Cancels a specific download.
|
||||
* @param downloadId The download ID to cancel
|
||||
* @returns True if the download was cancelled
|
||||
*/
|
||||
function cancel(downloadId: string): boolean;
|
||||
function cancel(downloadId: string): boolean
|
||||
|
||||
/**
|
||||
* Cancels all active downloads.
|
||||
* @returns The number of downloads cancelled
|
||||
*/
|
||||
function cancelAll(): number;
|
||||
function cancelAll(): number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -667,5 +745,5 @@ declare namespace $mime {
|
||||
* @returns An object containing the media type and parameters
|
||||
* @throws Error if parsing fails
|
||||
*/
|
||||
function parse(contentType: string): { mediaType: string; parameters: Record<string, string> };
|
||||
function parse(contentType: string): { mediaType: string; parameters: Record<string, string> }
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"seanime/internal/library/playbackmanager"
|
||||
"seanime/internal/mediaplayers/mediaplayer"
|
||||
"seanime/internal/platforms/platform"
|
||||
"seanime/internal/util/goja"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -55,6 +55,9 @@ type AppContext interface {
|
||||
|
||||
// BindCronToContextObj binds 'cron' to the UI context object
|
||||
BindCronToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) *Cron
|
||||
|
||||
// BindDownloaderToContextObj binds 'downloader' to the UI context object
|
||||
BindDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler)
|
||||
}
|
||||
|
||||
var GlobalAppContext = NewAppContext()
|
||||
|
||||
@@ -27,7 +27,7 @@ const (
|
||||
DownloadStatusError DownloadStatus = "error"
|
||||
)
|
||||
|
||||
type downloadProgress struct {
|
||||
type DownloadProgress struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
Destination string `json:"destination"`
|
||||
@@ -40,11 +40,11 @@ type downloadProgress struct {
|
||||
LastUpdateTime time.Time `json:"lastUpdate"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
|
||||
lastBytes int64
|
||||
lastBytes int64 `json:"-"`
|
||||
}
|
||||
|
||||
// IsFinished returns true if the download has completed, errored, or been cancelled
|
||||
func (p *downloadProgress) IsFinished() bool {
|
||||
func (p *DownloadProgress) IsFinished() bool {
|
||||
return p.Status == string(DownloadStatusCompleted) || p.Status == string(DownloadStatusCancelled) || p.Status == string(DownloadStatusError)
|
||||
}
|
||||
|
||||
@@ -55,37 +55,7 @@ type progressSubscriber struct {
|
||||
LastSent time.Time
|
||||
}
|
||||
|
||||
// BindDownload binds the download module to the Goja runtime.
|
||||
//
|
||||
// Example
|
||||
//
|
||||
// // Start multiple downloads
|
||||
// const id1 = $downloader.download("https://example.com/file1.zip", "/downloads/file1.zip", {
|
||||
// timeout: 300 // 5 minutes
|
||||
// });
|
||||
// const id2 = $downloader.download("https://example.com/file2.zip", "/downloads/file2.zip", {
|
||||
// timeout: 300
|
||||
// });
|
||||
//
|
||||
// // List all active downloads
|
||||
// const downloads = $downloader.listDownloads();
|
||||
// console.log(`Active downloads: ${downloads.length}`);
|
||||
//
|
||||
// // Track specific download
|
||||
// const progress = $downloader.getProgress(id1);
|
||||
// console.log(`Download ${progress.id}: ${progress.percentage}%, Speed: ${progress.speed} bytes/s`);
|
||||
//
|
||||
// // Watch download progress
|
||||
// const cancelWatch = $downloader.watch(id1, (progress) => {
|
||||
// console.log(`Download ${progress.id}: ${progress.percentage}%`);
|
||||
// });
|
||||
//
|
||||
// // Cancel specific download
|
||||
// $downloader.cancel(id1);
|
||||
//
|
||||
// // Cancel all downloads
|
||||
// $downloader.cancelAll();
|
||||
func (a *AppContextImpl) bindDownloader(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
func (a *AppContextImpl) BindDownloaderToContextObj(vm *goja.Runtime, obj *goja.Object, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
downloadObj := vm.NewObject()
|
||||
|
||||
progressMap := sync.Map{}
|
||||
@@ -126,7 +96,7 @@ func (a *AppContextImpl) bindDownloader(vm *goja.Runtime, logger *zerolog.Logger
|
||||
case <-ctx.Done():
|
||||
// If download is complete/cancelled/errored, send one last update and stop
|
||||
if progress, ok := progressMap.Load(downloadID); ok {
|
||||
p := progress.(*downloadProgress)
|
||||
p := progress.(*DownloadProgress)
|
||||
scheduler.ScheduleAsync(func() error {
|
||||
p.Speed = 0
|
||||
callback(goja.Undefined(), vm.ToValue(p))
|
||||
@@ -136,7 +106,7 @@ func (a *AppContextImpl) bindDownloader(vm *goja.Runtime, logger *zerolog.Logger
|
||||
return
|
||||
case <-ticker.C:
|
||||
if progress, ok := progressMap.Load(downloadID); ok {
|
||||
p := progress.(*downloadProgress)
|
||||
p := progress.(*DownloadProgress)
|
||||
scheduler.ScheduleAsync(func() error {
|
||||
callback(goja.Undefined(), vm.ToValue(p))
|
||||
return nil
|
||||
@@ -183,7 +153,7 @@ func (a *AppContextImpl) bindDownloader(vm *goja.Runtime, logger *zerolog.Logger
|
||||
|
||||
// Initialize progress tracking
|
||||
now := time.Now()
|
||||
progress := &downloadProgress{
|
||||
progress := &DownloadProgress{
|
||||
ID: downloadID,
|
||||
URL: url,
|
||||
Destination: destination,
|
||||
@@ -316,17 +286,17 @@ func (a *AppContextImpl) bindDownloader(vm *goja.Runtime, logger *zerolog.Logger
|
||||
return downloadID, nil
|
||||
})
|
||||
|
||||
_ = downloadObj.Set("getProgress", func(downloadID string) *downloadProgress {
|
||||
_ = downloadObj.Set("getProgress", func(downloadID string) *DownloadProgress {
|
||||
if progress, ok := progressMap.Load(downloadID); ok {
|
||||
return progress.(*downloadProgress)
|
||||
return progress.(*DownloadProgress)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = downloadObj.Set("listDownloads", func() []*downloadProgress {
|
||||
downloads := make([]*downloadProgress, 0)
|
||||
_ = downloadObj.Set("listDownloads", func() []*DownloadProgress {
|
||||
downloads := make([]*DownloadProgress, 0)
|
||||
progressMap.Range(func(key, value interface{}) bool {
|
||||
downloads = append(downloads, value.(*downloadProgress))
|
||||
downloads = append(downloads, value.(*DownloadProgress))
|
||||
return true
|
||||
})
|
||||
return downloads
|
||||
@@ -354,5 +324,5 @@ func (a *AppContextImpl) bindDownloader(vm *goja.Runtime, logger *zerolog.Logger
|
||||
})
|
||||
})
|
||||
|
||||
_ = vm.Set("$downloader", downloadObj)
|
||||
_ = obj.Set("downloader", downloadObj)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package plugin
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
util "seanime/internal/util"
|
||||
goja_util "seanime/internal/util/goja"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/dop251/goja"
|
||||
@@ -32,6 +34,14 @@ const (
|
||||
AllowPathWrite = 1
|
||||
)
|
||||
|
||||
type AsyncCmd struct {
|
||||
cmd *exec.Cmd
|
||||
|
||||
appContext *AppContextImpl
|
||||
scheduler *goja_util.Scheduler
|
||||
vm *goja.Runtime
|
||||
}
|
||||
|
||||
// BindSystem binds the system module to the Goja runtime.
|
||||
// Permissions needed: system + allowlist
|
||||
func (a *AppContextImpl) BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
||||
@@ -60,8 +70,12 @@ func (a *AppContextImpl) BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ex
|
||||
return nil, fmt.Errorf("command (%s) not authorized", fmt.Sprintf("%s %s", name, strings.Join(arg, " ")))
|
||||
}
|
||||
|
||||
return exec.Command(name, arg...), nil
|
||||
return util.NewCmdCtx(context.Background(), name, arg...), nil
|
||||
})
|
||||
|
||||
_ = osObj.Set("Interrupt", os.Interrupt)
|
||||
_ = osObj.Set("Kill", os.Kill)
|
||||
|
||||
_ = osObj.Set("readFile", func(path string) ([]byte, error) {
|
||||
if !a.isAllowedPath(ext, path, AllowPathRead) {
|
||||
return nil, fmt.Errorf("$os.readFile: path (%s) not authorized for read", path)
|
||||
@@ -208,7 +222,7 @@ func (a *AppContextImpl) BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ex
|
||||
fileModeObj.Set("ModeIrregular", os.ModeIrregular)
|
||||
fileModeObj.Set("ModeType", os.ModeType)
|
||||
fileModeObj.Set("ModePerm", os.ModePerm)
|
||||
_ = vm.Set("$os.FileMode", fileModeObj)
|
||||
_ = osObj.Set("FileMode", fileModeObj)
|
||||
|
||||
_ = vm.Set("$os", osObj)
|
||||
|
||||
@@ -324,12 +338,6 @@ func (a *AppContextImpl) BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ex
|
||||
|
||||
_ = vm.Set("$bytes", bytesObj)
|
||||
|
||||
//////////////////////////////////////
|
||||
// Downloader
|
||||
//////////////////////////////////////
|
||||
|
||||
a.bindDownloader(vm, logger, ext, scheduler)
|
||||
|
||||
//////////////////////////////////////
|
||||
// Filepath
|
||||
//////////////////////////////////////
|
||||
@@ -444,6 +452,15 @@ func (a *AppContextImpl) BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ex
|
||||
return libraryDirs, nil
|
||||
})
|
||||
|
||||
_ = osExtraObj.Set("asyncCmd", func(name string, arg ...string) *AsyncCmd {
|
||||
return &AsyncCmd{
|
||||
cmd: util.NewCmdCtx(context.Background(), name, arg...),
|
||||
appContext: a,
|
||||
scheduler: scheduler,
|
||||
vm: vm,
|
||||
}
|
||||
})
|
||||
|
||||
_ = vm.Set("$osExtra", osExtraObj)
|
||||
|
||||
//////////////////////////////////////
|
||||
@@ -503,7 +520,7 @@ func (a *AppContextImpl) resolveEnvironmentPaths(name string) []string {
|
||||
return []string{}
|
||||
}
|
||||
return []string{configDir}
|
||||
case "DOWNLOADS":
|
||||
case "DOWNLOAD":
|
||||
downloadDir, err := util.DownloadDir()
|
||||
if err != nil {
|
||||
return []string{}
|
||||
@@ -515,7 +532,7 @@ func (a *AppContextImpl) resolveEnvironmentPaths(name string) []string {
|
||||
return []string{}
|
||||
}
|
||||
return []string{desktopDir}
|
||||
case "DOCUMENTS":
|
||||
case "DOCUMENT":
|
||||
documentsDir, err := util.DocumentsDir()
|
||||
if err != nil {
|
||||
return []string{}
|
||||
@@ -824,3 +841,153 @@ func (a *AppContextImpl) validateCommandArgs(ext *extension.Extension, allowedAr
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// GetCommand returns the underlying exec.Cmd
|
||||
func (c *AsyncCmd) GetCommand() *exec.Cmd {
|
||||
return c.cmd
|
||||
}
|
||||
|
||||
func (c *AsyncCmd) Run(callback goja.Callable) error {
|
||||
|
||||
stdout, err := c.cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stderr, err := c.cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start the command
|
||||
err = c.cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use WaitGroup to ensure all goroutines complete before Wait() is called
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2) // One for stdout, one for stderr
|
||||
|
||||
// Handle stdout
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
data := scanner.Bytes()
|
||||
dataBytes := make([]byte, len(data))
|
||||
copy(dataBytes, data)
|
||||
|
||||
c.scheduler.ScheduleAsync(func() error {
|
||||
_, err := callback(goja.Undefined(), c.vm.ToValue(dataBytes), goja.Undefined(), goja.Undefined(), goja.Undefined())
|
||||
return err
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle stderr
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
data := scanner.Bytes()
|
||||
dataBytes := make([]byte, len(data))
|
||||
copy(dataBytes, data)
|
||||
|
||||
c.scheduler.ScheduleAsync(func() error {
|
||||
_, err := callback(goja.Undefined(), goja.Undefined(), c.vm.ToValue(dataBytes), goja.Undefined(), goja.Undefined())
|
||||
return err
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for both stdout and stderr to be fully processed in a separate goroutine
|
||||
go func() {
|
||||
// Wait for stdout and stderr goroutines to complete
|
||||
wg.Wait()
|
||||
|
||||
// Now wait for the command to finish
|
||||
err := c.cmd.Wait()
|
||||
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
|
||||
// Process exit code and signal
|
||||
exitCode := 0
|
||||
signal := ""
|
||||
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
exitCode = exitErr.ExitCode()
|
||||
signal = exitErr.String()
|
||||
} else if c.cmd.ProcessState != nil {
|
||||
exitCode = c.cmd.ProcessState.ExitCode()
|
||||
signal = c.cmd.ProcessState.String()
|
||||
}
|
||||
|
||||
// Call callback with exit code and signal
|
||||
c.scheduler.ScheduleAsync(func() error {
|
||||
_, err = callback(goja.Undefined(), goja.Undefined(), goja.Undefined(), c.vm.ToValue(exitCode), c.vm.ToValue(signal))
|
||||
return err
|
||||
})
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// // OnData registers a callback to be called when data is available from the command's stdout
|
||||
// func (c *AsyncCmd) OnData(callback func(data []byte)) error {
|
||||
// stdout, err := c.cmd.StdoutPipe()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// go func() {
|
||||
// scanner := bufio.NewScanner(stdout)
|
||||
// for scanner.Scan() {
|
||||
// c.scheduler.ScheduleAsync(func() error {
|
||||
// callback(scanner.Bytes())
|
||||
// return nil
|
||||
// })
|
||||
// }
|
||||
// }()
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// // OnError registers a callback to be called when data is available from the command's stderr
|
||||
// func (c *AsyncCmd) OnError(callback func(data []byte)) error {
|
||||
// stderr, err := c.cmd.StderrPipe()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// go func() {
|
||||
// scanner := bufio.NewScanner(stderr)
|
||||
// for scanner.Scan() {
|
||||
// c.scheduler.ScheduleAsync(func() error {
|
||||
// callback(scanner.Bytes())
|
||||
// return nil
|
||||
// })
|
||||
// }
|
||||
// }()
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// // OnExit registers a callback to be called when the command exits
|
||||
// func (c *AsyncCmd) OnExit(callback func(code int, signal string)) error {
|
||||
// go func() {
|
||||
// err := c.cmd.Wait()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// c.scheduler.ScheduleAsync(func() error {
|
||||
// callback(c.cmd.ProcessState.ExitCode(), c.cmd.ProcessState.String())
|
||||
// return nil
|
||||
// })
|
||||
// }()
|
||||
// return nil
|
||||
// }
|
||||
|
||||
@@ -147,6 +147,8 @@ func (c *Context) createAndBindContextObject(vm *goja.Runtime) {
|
||||
case extension.PluginPermissionPlayback:
|
||||
// Bind playback to the context object
|
||||
plugin.GlobalAppContext.BindPlaybackToContextObj(vm, obj, c.logger, c.ext, c.scheduler)
|
||||
case extension.PluginPermissionSystem:
|
||||
plugin.GlobalAppContext.BindDownloaderToContextObj(vm, obj, c.logger, c.ext, c.scheduler)
|
||||
case extension.PluginPermissionCron:
|
||||
// Bind cron to the context object
|
||||
cron := plugin.GlobalAppContext.BindCronToContextObj(vm, obj, c.logger, c.ext, c.scheduler)
|
||||
|
||||
Reference in New Issue
Block a user