Commit 07aa6ec8 authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #7216 from ethereum-optimism/aj/cannon-atomic-write

cannon: Use atomic write pattern to avoid leaving partially written files
parents 174bfbd4 05d2352d
...@@ -65,7 +65,7 @@ cannon: ...@@ -65,7 +65,7 @@ cannon:
cannon-prestate: op-program cannon cannon-prestate: op-program cannon
./cannon/bin/cannon load-elf --path op-program/bin/op-program-client.elf --out op-program/bin/prestate.json --meta op-program/bin/meta.json ./cannon/bin/cannon load-elf --path op-program/bin/op-program-client.elf --out op-program/bin/prestate.json --meta op-program/bin/meta.json
./cannon/bin/cannon run --proof-at '=0' --stop-at '=1' --input op-program/bin/prestate.json --meta op-program/bin/meta.json --proof-fmt 'op-program/bin/%d.json' --output /dev/null ./cannon/bin/cannon run --proof-at '=0' --stop-at '=1' --input op-program/bin/prestate.json --meta op-program/bin/meta.json --proof-fmt 'op-program/bin/%d.json' --output ""
mv op-program/bin/0.json op-program/bin/prestate-proof.json mv op-program/bin/0.json op-program/bin/prestate-proof.json
mod-tidy: mod-tidy:
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"github.com/ethereum-optimism/optimism/op-service/ioutil" "github.com/ethereum-optimism/optimism/op-service/ioutil"
) )
...@@ -27,19 +28,27 @@ func loadJSON[X any](inputPath string) (*X, error) { ...@@ -27,19 +28,27 @@ func loadJSON[X any](inputPath string) (*X, error) {
return &state, nil return &state, nil
} }
func writeJSON[X any](outputPath string, value X, outIfEmpty bool) error { func writeJSON[X any](outputPath string, value X) error {
if outputPath == "" {
return nil
}
var out io.Writer var out io.Writer
if outputPath != "" { finish := func() error { return nil }
f, err := ioutil.OpenCompressed(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) if outputPath != "-" {
// Write to a tmp file but reserve the file extension if present
tmpPath := outputPath + "-tmp" + path.Ext(outputPath)
f, err := ioutil.OpenCompressed(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil { if err != nil {
return fmt.Errorf("failed to open output file: %w", err) return fmt.Errorf("failed to open output file: %w", err)
} }
defer f.Close() defer f.Close()
out = f out = f
} else if outIfEmpty { finish = func() error {
out = os.Stdout // Rename the file into place as atomically as the OS will allow
return os.Rename(tmpPath, outputPath)
}
} else { } else {
return nil out = os.Stdout
} }
enc := json.NewEncoder(out) enc := json.NewEncoder(out)
if err := enc.Encode(value); err != nil { if err := enc.Encode(value); err != nil {
...@@ -49,5 +58,8 @@ func writeJSON[X any](outputPath string, value X, outIfEmpty bool) error { ...@@ -49,5 +58,8 @@ func writeJSON[X any](outputPath string, value X, outIfEmpty bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to append new-line: %w", err) return fmt.Errorf("failed to append new-line: %w", err)
} }
if err := finish(); err != nil {
return fmt.Errorf("failed to finish write: %w", err)
}
return nil return nil
} }
...@@ -13,7 +13,7 @@ func TestRoundTripJSON(t *testing.T) { ...@@ -13,7 +13,7 @@ func TestRoundTripJSON(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
file := filepath.Join(dir, "test.json") file := filepath.Join(dir, "test.json")
data := &jsonTestData{A: "yay", B: 3} data := &jsonTestData{A: "yay", B: 3}
err := writeJSON(file, data, false) err := writeJSON(file, data)
require.NoError(t, err) require.NoError(t, err)
// Confirm the file is uncompressed // Confirm the file is uncompressed
...@@ -32,7 +32,7 @@ func TestRoundTripJSONWithGzip(t *testing.T) { ...@@ -32,7 +32,7 @@ func TestRoundTripJSONWithGzip(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
file := filepath.Join(dir, "test.json.gz") file := filepath.Join(dir, "test.json.gz")
data := &jsonTestData{A: "yay", B: 3} data := &jsonTestData{A: "yay", B: 3}
err := writeJSON(file, data, false) err := writeJSON(file, data)
require.NoError(t, err) require.NoError(t, err)
// Confirm the file isn't raw JSON // Confirm the file isn't raw JSON
......
...@@ -24,7 +24,7 @@ var ( ...@@ -24,7 +24,7 @@ var (
} }
LoadELFOutFlag = &cli.PathFlag{ LoadELFOutFlag = &cli.PathFlag{
Name: "out", Name: "out",
Usage: "Output path to write JSON state to. State is dumped to stdout if set to empty string.", Usage: "Output path to write JSON state to. State is dumped to stdout if set to -. Not written if empty.",
Value: "state.json", Value: "state.json",
Required: false, Required: false,
} }
...@@ -66,10 +66,10 @@ func LoadELF(ctx *cli.Context) error { ...@@ -66,10 +66,10 @@ func LoadELF(ctx *cli.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to compute program metadata: %w", err) return fmt.Errorf("failed to compute program metadata: %w", err)
} }
if err := writeJSON[*mipsevm.Metadata](ctx.Path(LoadELFMetaFlag.Name), meta, false); err != nil { if err := writeJSON[*mipsevm.Metadata](ctx.Path(LoadELFMetaFlag.Name), meta); err != nil {
return fmt.Errorf("failed to output metadata: %w", err) return fmt.Errorf("failed to output metadata: %w", err)
} }
return writeJSON[*mipsevm.State](ctx.Path(LoadELFOutFlag.Name), state, true) return writeJSON[*mipsevm.State](ctx.Path(LoadELFOutFlag.Name), state)
} }
var LoadELFCommand = &cli.Command{ var LoadELFCommand = &cli.Command{
......
...@@ -28,7 +28,7 @@ var ( ...@@ -28,7 +28,7 @@ var (
} }
RunOutputFlag = &cli.PathFlag{ RunOutputFlag = &cli.PathFlag{
Name: "output", Name: "output",
Usage: "path of output JSON state. Stdout if left empty.", Usage: "path of output JSON state. Not written if empty, use - to write to Stdout.",
TakesFile: true, TakesFile: true,
Value: "out.json", Value: "out.json",
Required: false, Required: false,
...@@ -42,7 +42,7 @@ var ( ...@@ -42,7 +42,7 @@ var (
} }
RunProofFmtFlag = &cli.StringFlag{ RunProofFmtFlag = &cli.StringFlag{
Name: "proof-fmt", Name: "proof-fmt",
Usage: "format for proof data output file names. Proof data is written to stdout if empty.", Usage: "format for proof data output file names. Proof data is written to stdout if -.",
Value: "proof-%d.json", Value: "proof-%d.json",
Required: false, Required: false,
} }
...@@ -66,7 +66,7 @@ var ( ...@@ -66,7 +66,7 @@ var (
} }
RunMetaFlag = &cli.PathFlag{ RunMetaFlag = &cli.PathFlag{
Name: "meta", Name: "meta",
Usage: "path to metadata file for symbol lookup for enhanced debugging info durign execution.", Usage: "path to metadata file for symbol lookup for enhanced debugging info during execution.",
Value: "meta.json", Value: "meta.json",
Required: false, Required: false,
} }
...@@ -324,7 +324,7 @@ func Run(ctx *cli.Context) error { ...@@ -324,7 +324,7 @@ func Run(ctx *cli.Context) error {
} }
if snapshotAt(state) { if snapshotAt(state) {
if err := writeJSON(fmt.Sprintf(snapshotFmt, step), state, false); err != nil { if err := writeJSON(fmt.Sprintf(snapshotFmt, step), state); err != nil {
return fmt.Errorf("failed to write state snapshot: %w", err) return fmt.Errorf("failed to write state snapshot: %w", err)
} }
} }
...@@ -360,7 +360,7 @@ func Run(ctx *cli.Context) error { ...@@ -360,7 +360,7 @@ func Run(ctx *cli.Context) error {
proof.OracleValue = witness.PreimageValue proof.OracleValue = witness.PreimageValue
proof.OracleOffset = witness.PreimageOffset proof.OracleOffset = witness.PreimageOffset
} }
if err := writeJSON(fmt.Sprintf(proofFmt, step), proof, true); err != nil { if err := writeJSON(fmt.Sprintf(proofFmt, step), proof); err != nil {
return fmt.Errorf("failed to write proof data: %w", err) return fmt.Errorf("failed to write proof data: %w", err)
} }
} else { } else {
...@@ -371,7 +371,7 @@ func Run(ctx *cli.Context) error { ...@@ -371,7 +371,7 @@ func Run(ctx *cli.Context) error {
} }
} }
if err := writeJSON(ctx.Path(RunOutputFlag.Name), state, true); err != nil { if err := writeJSON(ctx.Path(RunOutputFlag.Name), state); err != nil {
return fmt.Errorf("failed to write state output: %w", err) return fmt.Errorf("failed to write state output: %w", err)
} }
return nil return nil
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment