Skip to content

cli: streamline autostart ux #2251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 13, 2022
Merged

cli: streamline autostart ux #2251

merged 14 commits into from
Jun 13, 2022

Conversation

johnstcn
Copy link
Member

@johnstcn johnstcn commented Jun 10, 2022

This PR makes the following changes:

  • autostart enable|disable => autostart set|unset
  • autostart enable now accepts a more natual schedule format: <time> <days-of-week> <location>
  • autostart show now shows configured timezone
  • 🎉 automatic timezone detection across mac, windows, linux 🎉

Fixes #1647

@johnstcn johnstcn requested a review from a team June 10, 2022 17:21
@johnstcn johnstcn marked this pull request as ready for review June 10, 2022 17:21
@johnstcn johnstcn self-assigned this Jun 10, 2022
@johnstcn johnstcn requested a review from ammario June 10, 2022 18:08
Comment on lines 24 to 54
func TimezoneIANA() (*time.Location, error) {
if tzEnv, found := os.LookupEnv("TZ"); found {
if tzEnv == "" {
return time.UTC, nil
}
loc, err := time.LoadLocation(tzEnv)
if err != nil {
return nil, xerrors.Errorf("load location from TZ env: %w", err)
}
return loc, nil
}

lp, err := filepath.EvalSymlinks(etcLocaltime)
if err != nil {
return nil, xerrors.Errorf("read location of %s: %w", etcLocaltime, err)
}

// On Darwin, /var/db/timezone/zoneinfo is also a symlink
realZoneInfoPath, err := filepath.EvalSymlinks(zoneInfoPath)
if err != nil {
return nil, xerrors.Errorf("read location of %s: %w", zoneInfoPath, err)
}

stripped := strings.Replace(lp, realZoneInfoPath, "", -1)
stripped = strings.TrimPrefix(stripped, string(filepath.Separator))
loc, err := time.LoadLocation(stripped)
if err != nil {
return nil, xerrors.Errorf("invalid location %q guessed from %s: %w", stripped, lp, err)
}
return loc, nil
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: at a glance, the Darwin and Linux versions of these functions seems identical. Be assured they are not.

Comment on lines 26 to 71
func TimezoneIANA() (*time.Location, error) {
if tzEnv, found := os.LookupEnv("TZ"); found {
if tzEnv == "" {
return time.UTC, nil
}
loc, err := time.LoadLocation(tzEnv)
if err != nil {
return nil, xerrors.Errorf("load location from TZ env: %w", err)
}
return loc, nil
}

// https://superuser.com/a/1584968
cmd := exec.Command("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive")
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, xerrors.Errorf("run powershell: %w", err)
}

done := make(chan struct{})
go func() {
defer stdin.Close()
defer close(done)
_, _ = fmt.Fprintln(stdin, cmdTimezone)
}()

<-done

outBytes, err := cmd.CombinedOutput()
if err != nil {
return nil, xerrors.Errorf("execute powershell command %q: %w", cmdTimezone, err)
}

outLines := strings.Split(string(outBytes), "\n")
if len(outLines) < 2 {
return nil, xerrors.Errorf("unexpected output from powershell command %q: %q", cmdTimezone, outLines)
}
// What we want is the second line of output
locStr := strings.TrimSpace(outLines[1])
loc, err := time.LoadLocation(locStr)
if err != nil {
return nil, xerrors.Errorf("invalid location %q from powershell: %w", locStr, err)
}

return loc, nil
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: just don't stare too closely at this

@@ -74,7 +75,8 @@ func Weekly(raw string) (*Schedule, error) {
}

// Schedule represents a cron schedule.
// It's essentially a thin wrapper for robfig/cron/v3 that implements Stringer.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: This wrapper is no longer thin.

Comment on lines +143 to +161
func (s Schedule) DaysOfWeek() string {
dow := strings.Fields(s.cronStr)[4]
if dow == "*" {
return "daily"
}
for _, weekday := range []time.Weekday{
time.Sunday,
time.Monday,
time.Tuesday,
time.Wednesday,
time.Thursday,
time.Friday,
time.Saturday,
} {
dow = strings.Replace(dow, fmt.Sprintf("%d", weekday), weekday.String()[:3], 1)
}
return dow
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: For display purposes, this just replaces the day-of-week numbers with their abbreviated equivalents.

testCase := testCase
//nolint:paralleltest // t.Setenv
t.Run(testCase.name, func(t *testing.T) {
t.Setenv("TZ", testCase.tzEnv)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Doing setenv here to validate that the timezone specified in the schedule is used

Comment on lines +80 to +84
{
name: "TimezoneProvidedInsteadOfLocation",
input: []string{"09:00AM", "Sun-Sat", "CST"},
expectedError: errUnsupportedTimezone.Error(),
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Emyrk this one just for you :-)

Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few suggestions and questions, but in general looks good! 👍🏻

Default: * (every day)
* Location (optional) must be a valid location in the IANA timezone database.
If omitted, we will fall back to either the TZ environment variable or /etc/localtime.
You can check your corresponding location by visiting https://ipinfo.io - it shows in the demo widget on the right.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This help is ❤️.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is excellent.

_, _ = fmt.Fprintf(cmd.OutOrStdout(),
"%s will automatically start at %s %s (%s)\n",
workspace.Name,
schedNext.In(sched.Location()).Format(time.Kitchen),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we always want to show am/pm here, even if 24h was used to define it? Or perhaps vary depending on defaults for the users system or selected location? Or perhaps we should have a user preference down the line?

Copy link
Member Author

@johnstcn johnstcn Jun 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like an internationalization problem to me, which I'm going to place inside a tin can and kick down the road for later. 😅

But yes, yes we should.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think preference is very location-dependent. It would be unfortunate to add more state for am/pm.


var errInvalidScheduleFormat = xerrors.New("Schedule must be in the format Mon-Fri 09:00AM America/Chicago")
var errInvalidTimeFormat = xerrors.New("Start time must be in the format hh:mm[am|pm] or HH:MM")
var errUnsupportedTimezone = xerrors.New("The location you provided looks like a timezone. Check https://ipinfo.io for your location.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Go Errors should be lower case.

But technically these are help/documentation, so I understand the use of proper sentence structure.

(This is non-blocking as I think the right approach might require some more think-through, but writing down my thoughts.)

I have one suggestion, that could have some benefits down the line if we e.g. want to translate the CLI, etc:

func FormatCliError(err error) string {
	switch err {
	case errInvalidScheduleFormat:
		return "Schedule ..."
	// ...
	}
}

We could optionally have a separate type cliError struct{} type which we print in this special way.

Or perhaps rather we could have:

type cliError struct{
	err  string
	code clierror.Code
}

func newCliError(code clierror.Code, e string) error

var errInvalidScheduleFormat = newCliError(clierror.InvalidSchedule, "invalid schedule format")

Where we could keep an enum of cli error messages in e.g. a separate package. Just ideas of the top of my head.

We could also do something simpler like uppercasing the first letter of the error when we print it. I think it'd be helpful if error messages looked consistent across the CLI.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have something very similar inside another codebase which shall not be named ;-)
But yes, this needs more noodling.

if assert.NotEmpty(t, actualSchedule) {
assert.Equal(t, testCase.expectedSchedule, actualSchedule.String())
}
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are some beautiful tests! ❤️

Default: * (every day)
* Location (optional) must be a valid location in the IANA timezone database.
If omitted, we will fall back to either the TZ environment variable or /etc/localtime.
You can check your corresponding location by visiting https://ipinfo.io - it shows in the demo widget on the right.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is excellent.

_, _ = fmt.Fprintf(cmd.OutOrStdout(),
"%s will automatically start at %s %s (%s)\n",
workspace.Name,
schedNext.In(sched.Location()).Format(time.Kitchen),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think preference is very location-dependent. It would be unfortunate to add more state for am/pm.

@johnstcn johnstcn enabled auto-merge (squash) June 13, 2022 20:44
@johnstcn johnstcn disabled auto-merge June 13, 2022 21:00
@johnstcn johnstcn merged commit 0a949aa into main Jun 13, 2022
@johnstcn johnstcn deleted the cj/1647/autostart-set-unset branch June 13, 2022 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Streamline coder autostart UX
3 participants