diff --git a/modules/background_task/README.md b/modules/background_task/README.md index d95d374..d60e96c 100644 --- a/modules/background_task/README.md +++ b/modules/background_task/README.md @@ -1,85 +1,65 @@ -# Background task with pueue -make nushell "support" background task feature. +# Background tasks with pueue -# Prerequisite -Install [pueue](https://github.com/Nukesor/pueue) and make sure `pueued` is running with default config, and, `pueue` is in `PATH` -# Usage -```nushell -use job.nu +Makes Nushell "support" background tasks. -# spawn a new job in background. -> job spawn { sleep 3sec ; echo gg | save gg.txt } +## Prerequisite -# get job status -> job status +Install [pueue](https://github.com/Nukesor/pueue) and make sure `pueued` is running and that `pueue` is in `PATH`. -# get one job's log -> job log $job_id +## Usage -# kill one job -> job kill $job_id +You will get tab completions and suggestions when you install the module. +Please check those. -# clean pueued finished job -> job clean +To install the module, copy the `task.nu` to the `$env.NU_LIB_DIRS` directory, then do: + +```nu +use task.nu ``` -# Q&A -## How can I pass data to background job? +In your Nushell config under `~/.config/nushell`. -The easiest way to do is pass it by environment variable, and you can use these variables in job. +## Q&A -So don't do this, it doesn't work: -```nushell -let a = 3 -job spawn { - echo $a +### How can I pass data to a background task? + +You can use environment variables, since they +are inherited from the parent when spawning a process. + +```nu +$env.FOO = 123 + +task spawn { + echo $env.FOO } ``` -Instead, doing this: -```nushell -let a = 3 -with-env {"a": 3} { - job spawn { - echo $env.a - } +If you want to pass serialized data, you can do this: + +```nu +let foo = { a: 1 b: 2 c: 3 } + +with-env { FOO: ($foo | to json) } { + task spawn { + let foo = ($env.FOO | from json) + + echo $foo + } } ``` -But what if I want to pass nushell's structure data to background job? +## How can I reuse custom commands in a background task? -You can serialize it to env variable, and then deserialize it in background job. +You can define these commands in a seperate module, like so: -```nushell -let a = [1, 2, 3] -with-env {"a": ($a | to json)} { - job spawn { - let a = ($env.a | from json) - echo $a - } +```nu +# --- in foo.nu --- +export def bar [] { echo bar } + +# --- in main.nu --- +task spawn { + use foo.nu + + foo bar } ``` - -## How can I reuse custom commands in background job? - -You can defined these custom commands in a module, then you can use the module inside the job. - -So don't do this, it won't work: -```nushell -def custom [] { echo 3 } -job spawn { - custom # custom is not defined -} -``` - -Instead, do this: -```nushell -# a.nu -export def custom [] { echo 3 } - -# main.nu -job spawn { - use a.nu - a custom -} -``` \ No newline at end of file diff --git a/modules/background_task/job.nu b/modules/background_task/job.nu deleted file mode 100644 index 504b539..0000000 --- a/modules/background_task/job.nu +++ /dev/null @@ -1,78 +0,0 @@ -# spawn task to run in the background -# -# please note that a fresh nushell is spawned to execute the given command -# So it doesn't inherit current scope's variables, custom commands, alias definition, except env variables which value can convert to string. -# -# Note that the closure to spawn can't take arguments. And it only supports something like this: { echo 3 }, it have no parameter list. -# -# e.g: -# spawn { echo 3 } -export def spawn [ - command: closure # the command to spawn - --label(-l): string # the label of comand - --group(-g): string # the group name to spawn -] { - let config_path = $nu.config-path - let env_path = $nu.env-path - let source_code = (view source $command | str trim -l -c '{' | str trim -r -c '}') - mut args = [ - $"nu --config \"($config_path)\" --env-config \"($env_path)\" -c '($source_code)'", - ] - if $label != null { - $args = ($args | prepend ["--label", $label]) - } - if $group != null { - $args = ($args | prepend ["--group", $group]) - } - let job_id = (pueue add -p $args) - - {"job_id": $job_id} -} - -export def log [ - id: int # id to fetch log -] { - pueue log $id -f --json - | from json - | transpose -i info - | flatten --all - | flatten --all - | flatten status -} - -# get job's stdout. -export def output [ - id: int # id to fetch job's stdout -] { - log $id | get output.0 -} - -# get job running status -export def status [ - --detailed(-d) # need to get detailed stauts? -] { - let output = ( - pueue status --json - | from json - | get tasks - | transpose -i status - | flatten - | flatten status - ) - - if not $detailed { - $output | select id label group Done? status? start? end? - } else { - $output - } -} - -# kill specific job -export def kill (id: int) { - pueue kill $id -} - -# clean job log -export def clean () { - pueue clean -} diff --git a/modules/background_task/task.nu b/modules/background_task/task.nu new file mode 100644 index 0000000..2426387 --- /dev/null +++ b/modules/background_task/task.nu @@ -0,0 +1,431 @@ +# Spawn a task to run in the background, even when the shell is closed. +# +# Note that a fresh Nushell interpreter is spawned to execute the +# given task, so it won't inherit the current scope's variables, +# custom commands and alias definitions. +# +# It will only inherit environment variables which can be converted to a string. +# +# Note that the closure can't take arguments. +# +# Example usage: task spawn { echo "Hello, World!" } +export def spawn [ + command: closure # The closure to run. + --working-directory (-w): directory # Specify the working directory the task will be run in. + --immediate (-i) # Immediately start the task. + --stashed (-s) # Create the task in Stashed state. Useful to avoid immediate execution if the queue is empty + --delay (-d): duration # Queue the task for execution only after the duration. + --group (-g): string # The group to spawn the task under. + --after (-a): int # Start the task once all specified tasks have successfully finished. As soon as one of the dependencies fails, this task will fail as well. + --priority (-o): string # Start this task with a higher priority. The higher the number, the faster it will be processed. + --label (-l): string # Label the task. This string will be shown in the `status` column of `task status`. +] -> int { + mut args = [] + + if $working_directory != null { + $args = ($args | prepend ["--working-directory", $working_directory]) + } + if $immediate { + $args = ($args | prepend "--immediate") + } + if $stashed { + $args = ($args | prepend "--stashed") + } + if $delay != null { + $args = ($args | prepend ["--delay" ($delay | format duration sec | parse "{secs} {_}" | get 0.secs)]) + } + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + if $after != null { + $args = ($args | prepend ["--after" $after]) + } + if $priority != null { + $args = ($args | prepend ["--priority" $priority]) + } + if $label != null { + $args = ($args | prepend ["--label" $label]) + } + + let source_code = ( + view source $command + | str trim --left --char "{" + | str trim --right --char "}" + ) + + (pueue add --print-task-id $args $"nu --config '($nu.config-path)' --env-config '($nu.env-path)' --commands '($source_code)'") +} + +# Remove tasks from the queue. +# Running or paused tasks need to be killed first. +export def remove [ + ...ids: int # IDs of the tasks to remove from the status list. +] { + pueue remove $ids +} + +# Switches the queue position of two tasks. +# Only works for queued or stashed tasks. +export def switch [ + task_id_1: int # The first task ID. + task_id_2: int # The second task ID. +] { + pueue switch $task_id_1 $task_id_2 +} + +# Stash a task that is not currently running. +# +# Stashed tasks won't be automatically started. +# You will have to queue them or start them by hand. +export def stash [ + ...ids: int # IDs of the tasks to stash. +] { + pueue stash $ids +} + +# Queue stashed tasks for execution. +export def queue [ + ...ids: int # IDs of the tasks to queue. + --delay (-d): duration # Queue only after the specified delay. +] { + let args = if $delay != null { + ["--delay" ($delay | format duration sec | parse '{secs} {_}' | get 0.secs)] + } else { + [] + } + + pueue enqueue $args $ids +} + +# Resume operation of specific tasks or groups of tasks. +# +# By default, this resumes the default group and all its tasks. +# It can also be used force-start specific tasks or start whole groups. +export def start [ + ...ids: int # IDs of the tasks to start. By default all the tasks in the default group will be started. + --group (-g): string # Resume a specific group and all paused tasks in it. The group will be set to running and its paused tasks will be resumed. + --all (-a) # Resume all groups. All groups will be set to running and paused tasks will be resumed. +] { + mut args = [] + + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + if $all { + $args = ($args | prepend "--all") + } + + pueue start $args +} + +# Restart failed or successful task(s). +# +# By default, identical tasks will be created and +# enqueued, but it's possible to restart in-place. +# +# You can also edit a few properties, such as +# the path and the command of the task, before restarting. +export def restart [ + ...ids: int # IDs of the tasks to restart. + --all-failed (-a) # Restart all failed tasks across all groups. Nice to use in combination with `--in-place/i`. + --failed-in-group (-g): string # Like `--all-failed`, but only restart tasks failed tasks of a specific group. The group will be set to running and its paused tasks will be resumed. + --start-immediately (-k) # Immediately start the tasks, no matter how many open slots there are. This will ignore any dependencies tasks may have. + --stashed (-s) # Set the restarted task to a "Stashed" state. Useful to avoid immediate execution. + --in-place (-i) # Restart the tasks by reusing the already existing tasks. + --not-in-place (-n) # Opposite of `--in-place`. This is already the default unless you have `restart_in_place` set to true. + --edit (-e) # Edit the tasks' commands before restarting + --edit-path (-p) # Edit the tasks' paths before restarting + --edit-label (-l) # Edit the tasks' labels before restarting +] { + mut args = [] + + if $all_failed { + $args = ($args | prepend "--all-failed") + } + if $failed_in_group != null { + $args = ($args | prepend "--failed-in-group") + } + if $start_immediately { + $args = ($args | prepend "--start-immediately") + } + if $stashed { + $args = ($args | prepend "--stashed") + } + if $in_place { + $args = ($args | prepend "--in-place") + } + if $not_in_place { + $args = ($args | prepend "--not-in-place") + } + if $edit { + $args = ($args | prepend "--edit") + } + if $edit_path { + $args = ($args | prepend "--edit-path") + } + if $edit_label { + $args = ($args | prepend "--edit-label") + } + + pueue restart $args $ids +} + +# Either pause a running tasks or a specific groups of tasks. +# +# By default, pauses the default group and all its tasks. +# +# A paused group won't start any new tasks automatically. +export def pause [ + ...ids: int # IDs of the tasks to pause. + --group (-g) # Pause a specific group + --all (-a) # Pause all groups. + --wait (-w) # Only pause the specified group and let already running tasks finish by themselves +] { + mut args = [] + + if $group != null { + $args = ($args | prepend "--group") + } + if $all != null { + $args = ($args | prepend "--all") + } + if $wait != null { + $args = ($args | prepend "--wait") + } + + pueue pause $args $ids +} + +# Kill specific running tasks or whole task groups. +# +# Kills all tasks of the default group when no ids or a specific group are provided. +export def kill [ + ...ids: int # IDs of the tasks to kill. + --group (-g): string # Kill all running tasks in a group. This also pauses the group. + --all (-a) # Kill all running tasks across ALL groups. This also pauses all groups. + --signal (-s): string # Send a UNIX signal instead of simply killing the process. DISCLAIMER: This bypasses Pueue's process handling logic! You might enter weird invalid states, use at your own descretion. +] { + mut args = [] + + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + if $all { + $args = ($args | prepend "--all") + } + if $signal != null { + $args = ($args | prepend ["--signal" $signal]) + } + + pueue kill $args $ids +} + +# Send something to a task. Useful for sending confirmations such as "y\n". +export def send [ + id: int # ID of the task to send something to. + input: string # The input that should be sent to the process. +] { + pueue send $id $input +} + +# Edit the command, path or label of a stashed or queued task. +# +# By default only the command is edited. +# +# Multiple properties can be added in one go. +export def edit [ + id: int # ID of the task to edit. + --command (-c) # Edit the task's command + --path (-p) # Edit the task's path + --label (-l) # Edit the task's label +] { + mut args = [] + + if $command { + $args = ($args | prepend "--command") + } + if $path { + $args = ($args | prepend "--path") + } + if $label { + $args = ($args | prepend "--label") + } + + pueue edit $args $id +} + +# Use this to add or remove groups. +# +# By default, this will simply display all known groups. +export def group [] { + pueue group --json | from json +} + +# Create a new group with a name. +export def "group add" [ + name: string # The name of the new group. + --parallel (-p): int # The amount of parallel tasks the group can run at one time. +] { + let args = if $parallel != null { + ["--parallel" $parallel] + } else { + [] + } + + pueue group add $args $name +} + +# Remove a group with a name. +export def "group remove" [ + name: string # The name of the group to be removed. +] { + pueue group remove $name +} + +# Display the current status of all tasks. +export def status [ + --detailed (-d) # Return a table with more detailed information. +] { + let output = ( + pueue status --json + | from json + | get tasks + | transpose --ignore-titles status + | flatten + ) + + # TODO: Rename the Done column to done. + if not $detailed { + $output | select id label group Done? status? start? end? + } else { + $output + } +} + +# Display the log output of finished tasks. +# +# Only the last few lines will be shown by default for multiple tasks. +# If you want to follow the output, use `--tail/-t`. +export def log [ + ...ids: int # The tasks to check the outputs of. + --last (-l): int # Only print the last N lines of each task's output. This is done by default if you're looking at multiple tasks. + --tail (-t) # Follow the output as it is printing. Only works with 1 task. When used in conjunction with `--last`, the last N lines will be printed before starting to wait for output. + --detailed (-d) # Include all fields, don't simplify output. +] { + def process_raw [raw: string] { + let full = ( + $raw + | from json + | transpose -i info + | flatten --all + | flatten --all + ) + + if $detailed { + $full + } else { + $full | select id label group Done? status? start? end? + } + } + + if (($ids | length) == 1) { + if $tail { + let args = if $last != null { + ["--lines" $last] + } else { + [] + } + + pueue follow $ids + } else { + let args = if $last != null { + ["--lines" $last] + } else { + [] + } + + process_raw (pueue log --full --json $args $ids) + | first + } + } else { + if $tail { + echo $"(ansi red)--tail can only be used with one task.(ansi reset)" + return + } + + let args = if $last != null { + ["--lines" $last] + } else { + [] + } + + process_raw (pueue log --full --json $args $ids) + } +} + +# Wait until the provided tasks are finished. +# +# This is like join() or await in many languages. +export def wait [ + ...ids: int # IDs of the tasks to wait for. + --group (-g): string # Wait for all tasks in a specific group. + --all (-a) # Wait for all tasks across all groups and the default group. + --quiet (-q) # Don't show any log output while waiting. + --status (-s): string # Wait for tasks to reach a specific task status. +] { + mut args = [] + + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + if $all { + $args = ($args | prepend $all) + } + if $quiet { + $args = ($args | prepend $quiet) + } + if $status != null { + $args = ($args | prepend ["--status" $status]) + } + + pueue wait $args $ids +} + +# Remove tasks from the status list. +export def clean [ + --successful-only (-s) # Only clean tasks that finished successfully + --group (-g): string # Only clean tasks of a specific group +] { + mut args = [] + + if $successful_only { + $args = ($args | prepend "--successful-only") + } + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + + pueue clean $args +} + +# Shutdown pueue and thus this module. +export def shutdown [] { + pueue shutdown +} + +# Set the maximum parallel tasks for a group. +# +# Note that no tasks will be stopped if the number is lowered. +# The limit only applies when schelduing. +export def set-parallel-limit [ + max: int # The maximum parallel tasks allowed for a group when schelduing. + --group (-g): string # The group to set the limit for. By default this is `default`. +] { + let args = if $group != null { + ["--group" $group] + } else { + [] + } + + pueue parallel $args $max +}